@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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["DEFAULT_PAGE_SIZE"],"sources":["../src/types.ts","../src/message-bytes.ts","../src/in-memory-session.ts","../src/store.ts","../src/constants.ts","../src/memory-persistence.ts","../src/file-persistence.ts","../src/replacement-blob-codec.ts","../src/relational-bridge.ts","../src/replacement-hydrator.ts","../src/session-write-lease.ts","../src/relational-session-persistence.ts","../src/sqlite-session-repository.ts","../src/remote-persistence.ts"],"sourcesContent":["import type { PaginatedResult, PersistenceShape } from '@x-otto/persistence'\n\nexport type { PaginatedResult }\n\nexport type SessionEntry<T = unknown> =\n | { type: 'message'; id: string; parentId?: string; message: T; timestamp: number }\n | {\n type: 'compaction'\n id: string\n parentId?: string\n summary: string\n compactedCount: number\n /**\n * (replacement-history 模型):压缩后保留的\"近期消息\"快照。\n * 自动压缩(memory keep-recent)写回时填充;buildContext 命中即返回 {summary, [...replacement, ...其后]},\n * 重启原样恢复、不重算。缺省(手动 /compact 的 fold-all,或旧快照)走位置式语义(仅返回压缩条目之后的消息)。\n * **additive optional——不升 SESSION_SNAPSHOT_VERSION**(旧快照无此字段即回退位置式)。\n */\n replacement?: T[]\n /**\n * RFC-160 D1:**内存态专有**标记——本条目的 replacement 已被驻留治理剥离\n * (历史 compaction 的 replacement 仅末位边界被 buildContext 消费,非末位是纯死重)。\n * 剥离态严禁落库(appendEntry 首行断言拒写)、严禁进 checkpoint/fork 产物(出口断言)、\n * 严禁成为 buildContext 边界(branch() fail-fast);真身按需经 loadEntriesByIds hydrate。\n */\n replacementStripped?: true\n timestamp: number\n }\n /**\n * 清除边界(`/clear`)。其后 buildContext() 返回真空上下文(无 summary/无消息),\n * visibleMessages() 返回空——但 messages()(完整转录)保留全部,可时间旅行回溯。\n * 与 compaction 正交:compaction 只切投影(转录仍可滚回看),clear 同时切投影+可见转录。\n * **additive optional variant——不升 SESSION_SNAPSHOT_VERSION**(旧快照无此条目即 visibleMessages==messages)。\n */\n | { type: 'clear'; id: string; parentId?: string; timestamp: number }\n\nexport interface SessionContext<T = unknown> {\n summary?: string\n messages: T[]\n}\n\n/**\n * 随快照持久化的 per-session 配置子集(全 primitive,不依赖上层 ai 类型)。\n * restore 时优先于共享 configProvider 模板,缺失字段回退模板。**additive optional——不升\n * SESSION_SNAPSHOT_VERSION**(版本门会跳过旧快照;旧快照无此字段即全回退模板)。\n */\nexport interface PersistedSessionConfig {\n interactive?: boolean\n depth?: number\n agentName?: string\n thinkingLevel?: string\n maxToolTurnExtensions?: number\n transient?: boolean\n /** 仅记 model.id(forward-compat);restore 重解析需 model 注册表,暂保留模板 model。 */\n modelId?: string\n /**\n * 引擎 review Tier-2:会话工作目录必须持久化——否则 restore 退回宿主全局 cwd,文件工具\n * 读写错目录(子目录/子会话场景静默数据损坏)。\n */\n workspaceDir?: string\n /** 会话级工具回合上限;不存则 restore 退回模板默认。 */\n maxToolTurns?: number\n /**\n * 会话恒定的 volatile system 尾段(append 子代理角色块等)。随快照持久化——否则 sticky\n * 子代理会话重启后角色块蒸发。全 primitive(string[]),additive-optional 不升版本。\n * 注:基础 systemPrompt 仍不持久化(restore 从模板重建,仍退回 main——既有 restore 保真度限制,\n * 见 docs/design/rfc020-followup-hardening;本字段只补 systemTail 这一新增数据的丢失)。\n */\n systemTail?: string[]\n /**\n * 会话级「始终允许」工具名(弹窗点「始终允许」的运行时授权)。跟随会话持久化——\n * 一次性信任决策不再泄漏到项目其它会话/永久污染 config,--continue 时回灌 PermissionRegistry\n * 的会话级集合(见 hooks/policy-registry setSessionAlwaysAllow)。additive-optional 不升版本。\n */\n permissionAlwaysAllow?: string[]\n /**\n * 会话级「账号可用模型 id」集合(实时拉取/刷新确认)。跟随会话持久化、--continue 回灌;\n * list_models 工具据此标记/过滤可用模型供 subagent 选型。additive-optional 不升版本。\n */\n availableModels?: string[]\n /**\n * 持久化输入历史(PromptInput 的 ↑/↓ 历史导航条目)。\n * max 100 条(最近在末尾);历史搜索面板(/history · Ctrl+R)+ ↑/↓ 导航共用。\n * @deprecated RFC-088 M4b:移入 cli InputHistoryStore 独立落盘。\n * 仅保留读取以兼容旧快照(`--continue` 一次性迁移),新代码不写此字段。\n * additive optional——不升 SESSION_SNAPSHOT_VERSION。\n */\n inputHistory?: string[]\n /**\n * 持久化绝对回合总数(reduce currentTurn)。resume 时据此把\n * 派生的 turnBoundaries 末尾对齐到绝对回合号,并续接 currentTurn。**不存 turnBoundaries\n * 索引**(resume 从消息流派生,见 cli deriveTurnBoundaries)。additive optional——不升版本。\n */\n turnCount?: number\n}\n\n/**\n * 草稿持久化条目。全 primitive,不依赖上层 tui/ai 类型。\n */\nexport interface DraftEntry {\n id: string\n name: string\n text: string\n savedAt: number\n}\n\n/**\n * 持久化任务面板条目(原 PersistedSessionConfig.todoList 内联类型)。\n * RFC-108 D3:从 PersistedSessionConfig 分离为独立命名类型,供 Session 接口的 get/set 方法签名继续引用。\n */\nexport interface TodoItem {\n id?: string\n title?: string\n status?: 'pending' | 'in_progress' | 'done' | 'skipped' | 'failed'\n turn?: number\n}\n\n/**\n * 持久化编辑文件条目(原 PersistedSessionConfig.editedFiles 内联类型)。\n * RFC-108 D3:从 PersistedSessionConfig 分离为独立命名类型。\n */\nexport interface EditedFile {\n path: string\n operation: 'edit' | 'write' | 'delete'\n addedLines: number\n removedLines: number\n timestamp: number\n turnId?: string\n toolCallId?: string\n}\n\n/**\n * 子代理面板持久化条目(stat-only)。全 primitive,不依赖上层 tui/ai 类型。\n * status 取终态快照;resume 时真实运行态由 runtime 决定(持久化只为面板可见性,非可恢复子代理)。\n */\nexport interface SubagentEntry {\n id: string\n /** agentName / 作业 title。 */\n name: string\n status: 'running' | 'done' | 'error' | 'killed'\n /** 首次出现时间(ms)。 */\n startedAt: number\n /** 所属回合(供 UI 分组,可选)。 */\n turnId?: string\n}\n\n/**\n * 持久化回合完成摘要条目(TUI 底部\"✓ 完成 · N 个工具 · 耗时 · tokens · 模型\"行)。\n * 全 primitive,不依赖上层 tui/ai 类型;与 `messages()`/`AgentMessage[]`(发给模型的\n * 真实历史)物理隔离存储——只供 resume 时重绘对话流用,不进 provider 请求。\n * 每回合累加(不像 EditedFile/SubagentEntry 那样只留当前回合),因为 resume 需要在\n * 对话流里逐回合重现历史摘要行。\n */\nexport interface TurnSummaryEntry {\n turn: number\n status: 'done' | 'error' | 'interrupted' | 'incomplete'\n toolCount: number\n elapsedMs: number\n tokensUsed?: number\n cacheHitRatio?: number\n model?: string\n /** RFC-235:本回合实际 effort 档(纯数据,渲染层用 formatEffortLabel 拼 ·Effort;不含展示格式)。 */\n effort?: string\n tasksDone?: number\n tasksTotal?: number\n completedAt: number\n}\n\n/**\n * 标题来源等级(RFC-078 D1)。覆盖优先级 custom > ai > derived:\n * - `derived`:deriveSessionTitle 启发式(首条 user 消息)。\n * - `ai`:LLM 便宜模型生成(可覆盖 derived,不可覆盖 custom)。\n * - `custom`:用户 `/rename` 指定(永不被 ai/derived 覆盖)。\n * setTitle 据此实施等级守卫(见 InMemorySession.setTitle)。\n */\nexport type TitleSource = 'custom' | 'ai' | 'derived'\n\n/** 来源等级权重——越大越强,弱不可覆盖强(RFC-078 R2)。 */\nexport const TITLE_SOURCE_RANK: Record<TitleSource, number> = {\n derived: 0,\n ai: 1,\n custom: 2,\n}\n\nexport interface SessionMetadata {\n createdAt: number\n lastActiveAt: number\n messageCount: number\n compactionCount: number\n parentSessionId?: string\n forkPoint?: number\n tags: string[]\n title?: string\n /** 当前 title 的来源等级(RFC-078 D1)。缺省视为未设置(可被任意来源写入)。 */\n titleSource?: TitleSource\n /** 创建此会话的 SDK 版本(作为 additive optional 不升 SESSION_SNAPSHOT_VERSION)。 */\n sdkVersion?: string\n /**\n * 是否已归档(禁止后续输入)的持久化镜像。运行时唯一权威仍是 Agent 状态机的\n * `status==='archived'`;本字段只用于跨重启/驱逐恢复时还原状态机(见\n * `PersistenceSync.restore` 恢复后 `agent.archive()` 回放)。additive optional,\n * 不升 SESSION_SNAPSHOT_VERSION——旧快照缺此字段按 false 处理。\n */\n archived?: boolean\n /** 是否置顶(/resume 列表排序权重,不影响是否可输入)。additive optional。 */\n pinned?: boolean\n /** 置顶分组名(RFC-158)。仅 pinned=true 时生效;缺省落入默认组。additive optional。 */\n pinGroup?: string\n config?: PersistedSessionConfig\n}\n\nexport interface Session<T = unknown> {\n id: string\n readonly leafId: string | undefined\n append(message: T): void\n /** RFC-283 D2:移除最后一条 message entry(内存态回滚,prep 阶段失败时用)。 */\n removeLast(): void\n compact(summary: string, compactedCount: number): void\n /** 自动压缩(keep-recent)写回——记录携带 replacement 的压缩条目(见 SessionEntry.replacement)。 */\n recordCompaction(summary: string, replacement: T[]): void\n /**\n * `/clear`——append 一个 clear 边界条目。其后 buildContext() 真空、visibleMessages() 空;\n * messages()(完整转录)保留全部。不换 sessionId、不删历史。\n *\n * RFC-151 D2:可选 `{ purge: true }` 物理释放该边界之前的所有 message 条目。\n */\n clearContext(options?: { purge?: boolean }): void\n /**\n * RFC-194 D3:物理释放最后一个 clear 边界之前的内存驻留(message 条目 splice 移除 +\n * 非活跃 compaction 的 replacement 剥离;DB append-forever 不动)。幂等;无边界 no-op。\n * 调用前提由调用方保证:clear 边界已确认落盘成功(RFC-194 D2 顺序,防丢未落盘消息)。\n */\n purgeClearedHistory(): { removedCount: number; strippedReplacements: number }\n entries(): ReadonlyArray<SessionEntry<T>>\n branchEntries(): ReadonlyArray<SessionEntry<T>>\n buildContext(): SessionContext<T>\n messages(): ReadonlyArray<T>\n /**\n * 可见转录——最后一个 clear 边界**之后**的消息(无 clear 则等于 messages())。\n * 仅被 clear 边界切断;compaction 边界不影响可见性(compact 后转录仍可滚回看)。\n * 供 TUI / `--continue` 重绘:曾 `/clear` 的会话只重绘 clear 之后。\n */\n visibleMessages(): ReadonlyArray<T>\n metadata(): SessionMetadata\n branch(entryId: string): void\n\n /**\n * 设置标题(RFC-078 R1 单写路径 / R2 等级守卫)。\n * 仅当新来源等级 ≥ 当前 title 来源等级时才覆盖(custom 永不被 ai/derived 盖)。\n * @returns 是否实际写入(被等级守卫拦截时返回 false)。\n */\n setTitle(title: string, source: TitleSource): boolean\n\n /**\n * 归档状态的持久化镜像写入(`metadata.archived`)。运行时权威是 Agent 状态机,本方法\n * 仅供 `AgentSession.archive()/unarchive()` 落一份镜像值供 restore 时回放。\n */\n setArchived(archived: boolean): void\n /**\n * 置顶状态写入(`metadata.pinned`/`metadata.pinGroup`)。纯持久化字段,无状态机联动。\n * `group` 仅 `pinned=true` 时生效;`pinned=false` 时实现必须无条件清空 `pinGroup`\n * (不保留旧分组,防止\"未置顶但残留分组名\"的僵尸状态,见 RFC-158 §4)。\n */\n setPinned(pinned: boolean, group?: string): void\n\n /**\n * 持久化 config 字段存取器(取代 cli 侧 `as unknown as {...}` 裸 cast,给持久化契约真类型)。\n * RFC-108 D3:todoList/editedFiles/subagents/drafts 四个 UI 面板字段从 PersistedSessionConfig\n * 分离为独立存储——这些 get/set 方法签名(同步、同名)保持不变,但内部存储已从 metadata.config\n * 迁至独立 panelState 字段。绝大多数消费点因此无需修改。\n */\n getTodoList(): TodoItem[] | undefined\n setTodoList(todos: TodoItem[]): void\n getEditedFiles(): EditedFile[] | undefined\n setEditedFiles(files: EditedFile[]): void\n /**\n * 持久化输入历史(@deprecated RFC-088 M4b:移入 cli InputHistoryStore。\n * 仅保留读取以兼容旧快照一次性迁移)。\n */\n getInputHistory(): NonNullable<PersistedSessionConfig['inputHistory']> | undefined\n /**\n * @deprecated RFC-088 M4b:新代码应写 cli InputHistoryStore + persist(), 不再经 session 快照。\n */\n setInputHistory(entries: NonNullable<PersistedSessionConfig['inputHistory']>): void\n /** 子代理面板持久化(stat-only)。RFC-108 D3:存储迁至 panelState 独立字段。 */\n getSubagents(): SubagentEntry[] | undefined\n setSubagents(entries: SubagentEntry[]): void\n /** 绝对回合总数(resume 末尾对齐派生 turnBoundaries)。 */\n getTurnCount(): number | undefined\n setTurnCount(n: number): void\n /** 草稿面板持久化(DraftPane 的 Ctrl+S 保存/加载)。RFC-108 D3:存储迁至 panelState 独立字段。 */\n getDrafts(): DraftEntry[] | undefined\n setDrafts(entries: DraftEntry[]): void\n /**\n * 回合完成摘要面板持久化(TUI 底部摘要行\"✓ 完成 · N 个工具 · 耗时 · tokens · 模型\")。\n * 每回合累加写入(整表覆盖式 set,调用方 read-append-write),与 messages() 物理隔离,\n * 不进 provider 请求——只供 resume 时按 turn 插回对话流重绘。\n */\n getTurnSummaries(): TurnSummaryEntry[] | undefined\n setTurnSummaries(entries: TurnSummaryEntry[]): void\n}\n\nexport interface SessionStore<T = unknown> {\n createSession(id?: string): Promise<Session<T>>\n getSession(id: string): Session<T> | undefined\n listSessions(): ReadonlyArray<Session<T>>\n deleteSession(id: string): Promise<boolean>\n listSessionsPaginated(options: PaginationOptions): PaginatedResult<Session<T>>\n}\n\nexport interface SessionSnapshot<T = unknown> {\n version: number\n id: string\n entries: SessionEntry<T>[]\n metadata: SessionMetadata\n leafId?: string\n}\n\n/**\n * session 域持久化接口——`extends` @x-otto/persistence 的共享方法契约\n * `PersistenceShape`,固化为域信封 `SessionSnapshot<T>`(entries[]/leafId/SessionMetadata)+ 域\n * 分页 `PaginationOptions`(sortBy=lastActiveAt|createdAt)。**不**复用 `Persistence<T>` 本体:\n * 其信封 `Snapshot<T>`(data:T) 与分页 sortBy 是通用语义,与 session 域类型真异(详见 PersistenceShape doc)。\n */\nexport interface SessionPersistence<T = unknown>\n extends PersistenceShape<SessionSnapshot<T>, PaginationOptions> {\n /**\n * 只读排序 id 列表(RFC-154 D1)——不含 entries,供批量恢复类场景(如 restoreAll)在\n * 重量级 `load()` 前先选出候选集,避免对不会被选中的会话做全量反序列化。\n *\n * 契约(重要事项规则):返回结果必须按 lastActiveAt **降序**排列(新→旧),且排除已归档\n * 会话(D1×D3 接线——归档语义是\"不再被批量恢复路径主动拉起\")。调用方信任该顺序,不再做\n * 二次排序兜底;三个实现(Relational/File/Remote)必须各自保证该契约,并各自有独立的排序\n * 正确性单测覆盖(不能只测其中一个后端)。\n */\n listRecentMeta(limit: number): Promise<string[]>\n /**\n * RFC-305 M3:后端 turn-flush 能力声明——持久化后端决定 turn 级落库策略,而非用户配置。\n * - 'incremental'(缺省):游标增量 append、低延迟(本地 relational SQLite)→ 每 turn 落库,\n * 崩溃窗口毫秒级(RFC-305 M1)。\n * - 'snapshot':整份快照上传/覆写(远端 PUT、旧 blob 逃生口整份文件覆写)→ turn 级节流\n * 落库(避免每 turn 全量上传随会话线性膨胀),prompt.end 仍强制落库。\n * 由实现类静态声明(能力事实),装配层零配置;未来远端若提供 append-tail API,改声明即可\n * 自动升级为 every-turn,无需任何调用方改动。\n */\n turnFlush?: 'incremental' | 'snapshot'\n /**\n * RFC-325 M4:DB 冷归档能力(optional,只有 relational SQLite 后端实现)。\n * 将 entries 复制到外部 .jsonl.gz + 标 archived=1,**不删除 DB 行**(RFC-159 红线)。\n */\n archiveSession?: (sessionId: string) => Promise<string>\n /** 从外部归档恢复(幂等 append,不删除既有条目)。 */\n restoreArchivedSession?: (sessionId: string) => Promise<number>\n}\n\nexport interface PaginationOptions {\n page: number\n pageSize?: number\n order?: 'asc' | 'desc'\n sortBy?: 'lastActiveAt' | 'createdAt'\n}\n","/**\n * message-bytes.ts — RFC-178 D2:消息内容字节估算(会话驻留治理的字节维度口径)。\n *\n * 估算口径:文本 content 取 UTF-16 code unit 长度(`String.length`,纯 ASCII 下真实\n * JS 驻留 ≈ 估算 × 2,恒为下界,见 RFC-178 §3 D2 UTF-16 效应量化);image 块取 base64\n * data 长度(base64 字符串本身驻留内存,1 char ≈ 真实 1 byte 图片数据 × 4/3 的字符串);\n * tool_call/tool_use 块取 arguments/input 的 JSON 序列化长度(`JSON.stringify` 产生字符串,\n * 取 `.length`);顶层 `tool_calls` 数组取 `function.arguments` 字段长度(OpenAI 风格,\n * arguments 已是 JSON 字符串)。\n * 不追求 sizeof 精度(JS 无消息级真实内存测量),只求确定性 + 与内容体量单调相关。\n *\n * 兼容多种消息形态(AgentMessage 的 Anthropic content-block 与 OpenAI 风格均覆盖):\n * - `content: string`\n * - `content: Array<{ text?; source?: { data? }; data?; image_url?: { url? }; tool_call 块\n * (arguments: Record<string,unknown>); tool_use 块(input: unknown) }>`\n * - 顶层 `tool_calls` 数组(OpenAI/TUI 风格,`{ function: { arguments?: string } }`)\n */\nexport function estimateMessageContentBytes(message: unknown): number {\n if (message == null || typeof message !== 'object') return 0\n\n const msg = message as Record<string, unknown>\n\n let total = 0\n\n // 顶层 tool_calls 数组(OpenAI 风格,assistant 消息的工具调用)\n const topToolCalls = msg['tool_calls']\n if (Array.isArray(topToolCalls)) {\n for (const tc of topToolCalls) {\n if (tc == null || typeof tc !== 'object') continue\n const t = tc as Record<string, unknown>\n const func = t['function'] as Record<string, unknown> | null | undefined\n if (func && typeof func === 'object' && typeof func['arguments'] === 'string') {\n total += (func['arguments'] as string).length\n }\n }\n }\n\n const content = msg['content']\n if (typeof content === 'string') return total + content.length\n if (!Array.isArray(content)) return total\n\n for (const part of content) {\n if (part == null || typeof part !== 'object') continue\n const p = part as Record<string, unknown>\n if (typeof p['text'] === 'string') total += (p['text'] as string).length\n const source = p['source'] as Record<string, unknown> | null | undefined\n if (source && typeof source === 'object' && typeof source['data'] === 'string') {\n total += (source['data'] as string).length\n }\n if (typeof p['data'] === 'string') total += (p['data'] as string).length\n const imageUrl = p['image_url'] as Record<string, unknown> | null | undefined\n if (imageUrl && typeof imageUrl === 'object' && typeof imageUrl['url'] === 'string') {\n total += (imageUrl['url'] as string).length\n }\n // tool_call 块(interchange ToolCall, arguments: Record<string, unknown>)\n if (p['type'] === 'tool_call' && typeof p['arguments'] === 'object' && p['arguments'] != null) {\n total += JSON.stringify(p['arguments']).length\n }\n // tool_use 块(Anthropic wire 格式, input: unknown)\n if (p['type'] === 'tool_use' && p['input'] != null) {\n total += JSON.stringify(p['input']).length\n }\n }\n return total\n}\n","import type {\n Session,\n SessionContext,\n SessionEntry,\n SessionMetadata,\n TitleSource,\n TodoItem,\n EditedFile,\n SubagentEntry,\n DraftEntry,\n TurnSummaryEntry,\n} from './types'\nimport { TITLE_SOURCE_RANK } from './types'\nimport { estimateMessageContentBytes } from './message-bytes'\n\nexport class InMemorySession<T = unknown> implements Session<T> {\n private readonly sessionEntries: SessionEntry<T>[] = []\n private readonly entryMap = new Map<string, SessionEntry<T>>()\n\n /**\n * RFC-178 D2:会话消息内容字节估算(增量维护,避免每回合 O(n) 重算——lesson_54)。\n * 覆盖全部变更路径:append 累加 / cap splice 递减 / clearContext purge 递减 /\n * recordCompaction replacement 计入 / stripReplacementInPlace 剥离递减 /\n * restoreEntries 全量重算基准。估算口径见 message-bytes.ts(UTF-16 下界,确定性)。\n */\n private _estimatedContentBytes = 0\n\n /** RFC-178:当前消息内容字节估算(含活跃 compaction replacement 的驻留)。 */\n get estimatedContentBytes(): number {\n return this._estimatedContentBytes\n }\n\n /**\n * 终局 review(2026-07-21):当前消息条目数——全路径增量维护(append +1 / purgeBeforeBoundary\n * -=removed / capStoredHistory·capStoredHistoryByBytes 各自赋值 / restoreEntries 全量重算),\n * 与 `verifyEstimatedContentBytes` 同源纪律:热路径读此 getter 而非重新 O(n) 扫描 sessionEntries\n * 或调用 `metadata().messageCount`(后者对 tags/config 做防御性深拷贝,代价更高)。\n */\n get messageCount(): number {\n return this.meta.messageCount\n }\n\n /**\n * 计数器漂移校验(终局审查 2026-07-18 M2 守卫):从 sessionEntries 全量重算字节估算,\n * 与增量计数器比对。`_estimatedContentBytes` 是\"必须靠人力在所有 mutation 路径上手动\n * 同步\"的派生状态——新增 mutation 路径漏更新计数器不会崩溃,只会让字节驻留 cap 触发\n * 时机静默错位。本方法给测试(每个 mutation 路径测试的收尾断言)与 debug 排查提供\n * 唯一真源比对点。O(n) 全量遍历,**勿在热路径调用**(lesson_54)。\n *\n * @returns 漂移量(`recomputed - counter`;0 = 无漂移)\n */\n verifyEstimatedContentBytes(): { counter: number; recomputed: number; drift: number } {\n let recomputed = 0\n for (const entry of this.sessionEntries) {\n if (entry.type === 'message') {\n recomputed += estimateMessageContentBytes(entry.message)\n } else if (entry.type === 'compaction' && entry.replacement) {\n for (const m of entry.replacement) recomputed += estimateMessageContentBytes(m)\n }\n }\n return {\n counter: this._estimatedContentBytes,\n recomputed,\n drift: recomputed - this._estimatedContentBytes,\n }\n }\n\n private readonly meta: SessionMetadata\n\n /**\n * RFC-108 D3:五个 UI 面板字段(todoList/editedFiles/subagents/drafts/turnSummaries)的\n * 独立内存存储,不再纳入 SessionMetadata.config。字段类型从原 PersistedSessionConfig\n * 同名字段迁移。对外接口(Session 的 get/set 方法)签名完全保持不变,绝大多数消费点\n * 无需修改。turnSummaries 是后续新增字段(回合完成摘要行持久化,见 TurnSummaryEntry)。\n */\n private readonly panelState: {\n todoList?: TodoItem[]\n editedFiles?: EditedFile[]\n subagents?: SubagentEntry[]\n drafts?: DraftEntry[]\n turnSummaries?: TurnSummaryEntry[]\n } = {}\n\n constructor(\n public readonly id: string,\n parentSessionId?: string,\n forkPoint?: number,\n ) {\n const now = Date.now()\n this.meta = {\n createdAt: now,\n lastActiveAt: now,\n messageCount: 0,\n compactionCount: 0,\n parentSessionId,\n forkPoint,\n tags: [],\n }\n }\n\n private _leafId: string | undefined = undefined\n get leafId(): string | undefined {\n return this._leafId\n }\n\n append(message: T): void {\n const entry: SessionEntry<T> & { type: 'message' } = {\n type: 'message',\n id: crypto.randomUUID(),\n parentId: this._leafId,\n message,\n timestamp: Date.now(),\n }\n this._leafId = entry.id\n this.sessionEntries.push(entry)\n this.entryMap.set(entry.id, entry)\n this.meta.messageCount++\n this.meta.lastActiveAt = Date.now()\n this._estimatedContentBytes += estimateMessageContentBytes(message)\n }\n\n /**\n * RFC-283 D2:移除最后一条 message entry(内存态回滚)。\n * 用于 prompt prep 阶段失败时回滚从未到达 LLM 的 user message。\n * 只移除最后一条 type='message' entry,回退 _leafId 到其 parentId。\n * 不触碰 compaction/clear 等结构 entry。只在 prompt 执行期间(save 之前)调用。\n */\n removeLast(): void {\n // RFC-283 D2:防御性——只移除 _leafId 指向的 entry(即最后 append 的)。\n // prep 阶段失败时 _leafId 必指向刚 append 的 user message。\n if (!this._leafId) return\n const leafEntry = this.entryMap.get(this._leafId)\n if (!leafEntry || leafEntry.type !== 'message') return\n const idx = this.sessionEntries.lastIndexOf(leafEntry)\n if (idx === -1) return\n this.sessionEntries.splice(idx, 1)\n this.entryMap.delete(leafEntry.id)\n this._leafId = leafEntry.parentId\n this.meta.messageCount = Math.max(0, this.meta.messageCount - 1)\n this._estimatedContentBytes -= estimateMessageContentBytes(leafEntry.message)\n }\n\n compact(summary: string, compactedCount: number): void {\n const branchMessages = this.getBranchMessageEntries()\n if (compactedCount <= 0 || compactedCount > branchMessages.length) {\n return\n }\n\n const entry: SessionEntry<T> & { type: 'compaction' } = {\n type: 'compaction',\n id: crypto.randomUUID(),\n parentId: this._leafId,\n summary,\n compactedCount,\n timestamp: Date.now(),\n }\n\n this.sessionEntries.push(entry)\n this.entryMap.set(entry.id, entry)\n this.meta.compactionCount++\n this.meta.lastActiveAt = Date.now()\n this._leafId = entry.id\n }\n\n /**\n * 自动压缩(keep-recent)写回——记录一条携带 replacement 的压缩条目。\n * replacement = 压缩时保留的近期消息快照(replacement-history 模型)。buildContext\n * 命中即返回 {summary, [...replacement, ...其后新增消息]},重启原样恢复、不重算。\n * 与 compact()(fold-all 位置式,手动 /compact 用)并存,由 buildContext 据 replacement 是否在场分派。\n */\n recordCompaction(summary: string, replacement: T[]): void {\n // RFC-160 D2:新 compaction 即将成为末位边界——旧末位的 replacement 从此零读者,\n // 增量剥离(内存投影)。先定位旧末位(当前 leaf parent 链上最近的 compaction)。\n const previousBoundary = this.findActiveCompaction()\n\n const entry: SessionEntry<T> & { type: 'compaction' } = {\n type: 'compaction',\n id: crypto.randomUUID(),\n parentId: this._leafId,\n summary,\n compactedCount: replacement.length,\n replacement: [...replacement],\n timestamp: Date.now(),\n }\n\n // RFC-178 D2 路径 3:replacement 数组是消息的独立拷贝,真实驻留内存,计入字节估算\n // (对应的剥离路径 stripReplacementInPlace 会递减,保持计数器与真实驻留同步)。\n for (const m of replacement) this._estimatedContentBytes += estimateMessageContentBytes(m)\n\n this.sessionEntries.push(entry)\n this.entryMap.set(entry.id, entry)\n this.meta.compactionCount++\n this.meta.lastActiveAt = Date.now()\n this._leafId = entry.id\n\n // D2a:仅当剥离资格谓词在场且判定\"已持久化\"(查 entry_id 游标,不查 DB——save 是\n // 异步 fire-and-forget,查 DB 有假阴性)才剥;谓词缺席/不满足 → 跳过(合法时序,\n // 下次装载时 stripInactiveReplacements 会补剥),不断言炸。\n if (\n previousBoundary &&\n previousBoundary.replacement != null &&\n this.replacementStripEligibility?.(previousBoundary.id) === true\n ) {\n this.stripReplacementInPlace(previousBoundary.id)\n }\n }\n\n /** RFC-160 D2a:剥离资格谓词(组装根注入,如\"entry_id 已在持久化游标之内\")。 */\n private replacementStripEligibility: ((entryId: string) => boolean) | undefined\n\n /** 注入运行时增量剥离的资格谓词(缺省无谓词 = 运行时剥离禁用,仅装载剥离生效)。 */\n setReplacementStripEligibility(predicate: (entryId: string) => boolean): void {\n this.replacementStripEligibility = predicate\n }\n\n /** 当前 leaf parent 链上最近的 compaction(即 buildContext 的活跃边界),无则 undefined。 */\n private findActiveCompaction(): (SessionEntry<T> & { type: 'compaction' }) | undefined {\n return this._leafId ? this.findActiveCompactionFrom(this._leafId) : undefined\n }\n\n /**\n * RFC-160:就地剥离某 compaction 的 replacement(内存投影;entryMap 与 sessionEntries\n * 同步替换为剥离副本,不变更 DB)。幂等:已剥离/无 replacement 时为 no-op。\n */\n private stripReplacementInPlace(entryId: string): void {\n const existing = this.entryMap.get(entryId)\n if (!existing || existing.type !== 'compaction') return\n const c = existing as SessionEntry<T> & { type: 'compaction' }\n if (c.replacement == null) return\n // RFC-178 D2 路径 4(评审 P1-4):剥离即释放 replacement 驻留,字节估算同步递减——\n // 否则计数器虚高会导致字节触发器误触发。\n for (const m of c.replacement) {\n this._estimatedContentBytes -= estimateMessageContentBytes(m)\n }\n if (this._estimatedContentBytes < 0) this._estimatedContentBytes = 0\n const { replacement: _dropped, ...rest } = c\n const stripped = { ...rest, replacementStripped: true as const } as SessionEntry<T>\n this.entryMap.set(entryId, stripped)\n const idx = this.sessionEntries.findIndex((e) => e.id === entryId)\n if (idx >= 0) this.sessionEntries[idx] = stripped\n }\n\n /**\n * 清除边界。append 一个 type:'clear' 条目——其后 buildContext() 真空、visibleMessages() 空,\n * 但 messages()(完整转录)保留全部(时间旅行可回溯 clear 前)。不增 compactionCount(clear 非压缩)。\n *\n * RFC-151 D2:可选 purge 物理释放——purge:true 时从 sessionEntries 头部 splice 移除\n * 该边界之前的所有 message 类型条目,仅保留边界及之后的内容。\n */\n clearContext(options?: { purge?: boolean }): void {\n const entry: SessionEntry<T> & { type: 'clear' } = {\n type: 'clear',\n id: crypto.randomUUID(),\n parentId: this._leafId,\n timestamp: Date.now(),\n }\n this.sessionEntries.push(entry)\n this.entryMap.set(entry.id, entry)\n this.meta.lastActiveAt = Date.now()\n this._leafId = entry.id\n\n if (options?.purge === true) {\n this.purgeBeforeBoundary(entry.id)\n }\n }\n\n /**\n * RFC-194 D3:物理释放最后一个 clear 边界之前的内存驻留(DB append-forever 不动)。\n * 供持久化层在「clear 边界确认落盘成功后」调用(顺序保证见 RFC-194 D2——先 save\n * 后 purge,否则未落盘消息会永久丢失,违反 RFC-159 红线;本方法自身不感知持久化\n * 状态,保持 session 包零持久化依赖)。\n *\n * 行为:\n * - 无 clear 边界 → no-op(removedCount=0);\n * - 边界前 `type:'message'` 条目:splice 移除 + entryMap 删除 + 字节/条数账目递减;\n * - 边界前 `type:'compaction'` 条目:剥离 replacement(RFC-151 purge 分支的既有缺口\n * ——clear 后边界前所有 compaction 必然非活跃(findActiveCompactionFrom 遇 clear\n * 即断),其 replacement 是纯驻留,重工具会话的驻留大头恰在这里);\n * - 幂等:重复调用第二次 removedCount=0、strippedReplacements=0。\n *\n * 已知限制(RFC-194 R2):purge 后进程内 branch() 到 clear 前条目会抛 Entry not\n * found——DB 全量在,可经 loadEntriesByIds hydrate 恢复(RFC-160 既有机制);当前\n * 无生产调用路径跨 clear 边界 branch。\n */\n purgeClearedHistory(): { removedCount: number; strippedReplacements: number } {\n let boundaryId: string | undefined\n for (let i = this.sessionEntries.length - 1; i >= 0; i--) {\n if (this.sessionEntries[i]!.type === 'clear') {\n boundaryId = this.sessionEntries[i]!.id\n break\n }\n }\n if (!boundaryId) return { removedCount: 0, strippedReplacements: 0 }\n return this.purgeBeforeBoundary(boundaryId)\n }\n\n /** clearContext({purge:true}) 与 purgeClearedHistory() 的共享实现(RFC-194 D3 收编)。 */\n private purgeBeforeBoundary(\n boundaryId: string,\n ): { removedCount: number; strippedReplacements: number } {\n const boundaryIdx = this.sessionEntries.findIndex((e) => e.id === boundaryId)\n if (boundaryIdx < 0) return { removedCount: 0, strippedReplacements: 0 }\n\n let strippedReplacements = 0\n let i = 0\n let removed = 0\n let remaining = boundaryIdx\n while (i < remaining) {\n const e = this.sessionEntries[i]!\n if (e.type === 'message') {\n // RFC-178 D2:purge 物理释放同步递减字节估算。\n this._estimatedContentBytes -= estimateMessageContentBytes(e.message)\n this.entryMap.delete(e.id)\n this.sessionEntries.splice(i, 1)\n remaining--\n removed++\n } else {\n // RFC-194 D3:边界前 compaction 的 replacement 剥离(stripReplacementInPlace\n // 幂等——已剥离/无 replacement 时 no-op,账目在其内部递减)。\n if (e.type === 'compaction' && e.replacement != null) {\n this.stripReplacementInPlace(e.id)\n strippedReplacements++\n }\n i++\n }\n }\n if (this._estimatedContentBytes < 0) this._estimatedContentBytes = 0\n this.meta.messageCount -= removed\n return { removedCount: removed, strippedReplacements }\n }\n\n branch(entryId: string): void {\n if (!this.entryMap.has(entryId)) {\n throw new Error(`Entry \"${entryId}\" not found in session \"${this.id}\"`)\n }\n // RFC-160 D5(branch 收编):目标链上的活跃边界若是剥离态 compaction,buildContext\n // 会静默丢 replacement(上下文语义错误,违反 RFC-142 真身红线)——fail-fast 要求\n // 调用方先 hydrate。活跃链全保留策略使现有生产调用点(悬空父修复)天然不触发。\n const boundary = this.findActiveCompactionFrom(entryId)\n if (boundary && (boundary as { replacementStripped?: true }).replacementStripped) {\n throw new Error(\n `branch: target chain's active compaction boundary \"${boundary.id}\" has a stripped ` +\n `replacement (RFC-160); hydrate it via loadEntriesByIds before branching`,\n )\n }\n this._leafId = entryId\n this.meta.lastActiveAt = Date.now()\n }\n\n /** 从任意起点沿 parent 链找最近的 compaction 边界(clear 截断),branch() 收编用。 */\n private findActiveCompactionFrom(\n startId: string,\n ): (SessionEntry<T> & { type: 'compaction' }) | undefined {\n const visited = new Set<string>()\n let cur = this.entryMap.get(startId)\n while (cur && !visited.has(cur.id)) {\n visited.add(cur.id)\n if (cur.type === 'compaction') return cur as SessionEntry<T> & { type: 'compaction' }\n if (cur.type === 'clear') return undefined\n cur = cur.parentId ? this.entryMap.get(cur.parentId) : undefined\n }\n return undefined\n }\n\n entries(): ReadonlyArray<SessionEntry<T>> {\n return this.sessionEntries\n }\n\n branchEntries(): ReadonlyArray<SessionEntry<T>> {\n return this.walkBranch()\n }\n\n buildContext(): SessionContext<T> {\n const branch = this.walkBranch()\n\n const lastBoundaryIndex = this.lastBoundaryIndex(branch)\n\n const messagesAfter = branch\n .slice(lastBoundaryIndex + 1)\n .filter((e): e is SessionEntry<T> & { type: 'message' } => e.type === 'message')\n .map((e) => e.message)\n\n if (lastBoundaryIndex === -1) {\n return { messages: messagesAfter }\n }\n\n const boundary = branch[lastBoundaryIndex]!\n if (boundary.type === 'clear') {\n return { messages: messagesAfter }\n }\n\n const compactionEntry = boundary as SessionEntry<T> & { type: 'compaction' }\n return {\n summary: compactionEntry.summary,\n messages: compactionEntry.replacement\n ? [...compactionEntry.replacement, ...messagesAfter]\n : messagesAfter,\n }\n }\n\n messages(): ReadonlyArray<T> {\n return this.walkBranch()\n .filter((e): e is SessionEntry<T> & { type: 'message' } => e.type === 'message')\n .map((e) => e.message)\n }\n\n /**\n * 可见转录——最后一个 **clear** 边界之后的消息(compaction 边界不切可见)。\n * 无 clear → 等于 messages() 全量。供 `--continue`/TUI 重绘。\n */\n visibleMessages(): ReadonlyArray<T> {\n const branch = this.walkBranch()\n let lastClearIndex = -1\n for (let i = branch.length - 1; i >= 0; i--) {\n if (branch[i]?.type === 'clear') {\n lastClearIndex = i\n break\n }\n }\n return branch\n .slice(lastClearIndex + 1)\n .filter((e): e is SessionEntry<T> & { type: 'message' } => e.type === 'message')\n .map((e) => e.message)\n }\n\n metadata(): SessionMetadata {\n // 防御性快照:tags 已深拷;config(todoList/editedFiles/inputHistory/subagents 等可变\n // 嵌套)同样深拷——否则消费方改返回值即改活树、且后续 setTodoList/setEditedFiles 会\n // retroactively 改已发出的快照(共享引用)。config 缺省时不引入键,保留原\"惰性\"语义。\n return {\n ...this.meta,\n tags: [...this.meta.tags],\n ...(this.meta.config ? { config: structuredClone(this.meta.config) } : {}),\n }\n }\n\n /**\n * RFC-078 R1/R2:单写路径 + 等级守卫。弱来源不得覆盖强来源\n * (custom 永不被 ai/derived 盖;同级可覆盖,如 ai 重生覆盖旧 ai)。\n */\n setTitle(title: string, source: TitleSource): boolean {\n const currentRank =\n this.meta.titleSource != null ? TITLE_SOURCE_RANK[this.meta.titleSource] : -1\n if (TITLE_SOURCE_RANK[source] < currentRank) return false\n this.meta.title = title\n this.meta.titleSource = source\n this.meta.lastActiveAt = Date.now()\n return true\n }\n\n /** 设置创建此会话的 SDK 版本(仅在新会话创建时调用一次)。 */\n setSdkVersion(version: string): void {\n this.meta.sdkVersion = version\n }\n\n /** 归档状态持久化镜像写入(见 `Session.setArchived` 契约注释)。 */\n setArchived(archived: boolean): void {\n this.meta.archived = archived\n this.meta.lastActiveAt = Date.now()\n }\n\n /** 置顶状态写入。`pinned=false` 时无条件清空 `pinGroup`(不保留旧分组,见 RFC-158 §4)。 */\n setPinned(pinned: boolean, group?: string): void {\n this.meta.pinned = pinned\n this.meta.pinGroup = pinned ? group : undefined\n this.meta.lastActiveAt = Date.now()\n }\n\n /** 获取持久化的任务面板状态。RFC-108 D3:存储迁至 panelState 独立字段。 */\n getTodoList(): TodoItem[] | undefined {\n return this.panelState.todoList\n }\n\n /** 设置持久化的任务面板状态。RFC-108 D3:存储迁至 panelState 独立字段。 */\n setTodoList(todos: TodoItem[]): void {\n this.panelState.todoList = [...todos]\n this.meta.lastActiveAt = Date.now()\n }\n\n /** 获取持久化的编辑文件面板状态。RFC-108 D3:存储迁至 panelState 独立字段。 */\n getEditedFiles(): EditedFile[] | undefined {\n return this.panelState.editedFiles\n }\n\n /** 设置持久化的编辑文件面板状态。max 50 条(最新在前)。RFC-108 D3:存储迁至 panelState。 */\n setEditedFiles(files: EditedFile[]): void {\n this.panelState.editedFiles = files.slice(0, 50)\n this.meta.lastActiveAt = Date.now()\n }\n\n /** 获取持久化的输入历史。 */\n getInputHistory(): string[] | undefined {\n return this.meta.config?.inputHistory\n }\n\n /**\n * 设置持久化的输入历史。max 100 条(最近在末尾)。config 为惰性对象,首次写入时初始化。\n * @deprecated RFC-088 M4b:历史已移至 cli InputHistoryStore。本方法仅迁移路径调用——\n * 传空数组 = **删除字段**(快照不留 `inputHistory: []` 残迹,对齐 M4b 设计\"下次快照字段消失\")。\n */\n setInputHistory(entries: string[]): void {\n if (entries.length === 0) {\n if (this.meta.config?.inputHistory !== undefined) {\n delete this.meta.config.inputHistory\n this.meta.lastActiveAt = Date.now()\n }\n return\n }\n if (!this.meta.config) this.meta.config = {}\n this.meta.config.inputHistory = entries.slice(-100)\n this.meta.lastActiveAt = Date.now()\n }\n\n /** 获取持久化的子代理面板状态。RFC-108 D3:存储迁至 panelState 独立字段。 */\n getSubagents(): SubagentEntry[] | undefined {\n return this.panelState.subagents\n }\n\n /** 设置持久化的子代理面板状态(stat-only)。max 100 条。RFC-108 D3:存储迁至 panelState。 */\n setSubagents(entries: SubagentEntry[]): void {\n this.panelState.subagents = entries.slice(0, 100)\n this.meta.lastActiveAt = Date.now()\n }\n\n /** 获取持久化的回合完成摘要列表(TUI 底部摘要行)。 */\n getTurnSummaries(): TurnSummaryEntry[] | undefined {\n return this.panelState.turnSummaries\n }\n\n /**\n * 设置持久化的回合完成摘要列表。max 200 条(最新在末尾——调用方按 turn 递增顺序\n * read-append-write)。上限对齐 todoList/subagents 同类\"面板态防无界增长\"纪律;\n * 200 远超单会话正常回合数,仅作防御性上限,不做\"只留最近 N 条\"这类会丢历史的裁剪。\n */\n setTurnSummaries(entries: TurnSummaryEntry[]): void {\n this.panelState.turnSummaries = entries.slice(-200)\n this.meta.lastActiveAt = Date.now()\n }\n\n /** 获取持久化的绝对回合总数。 */\n getTurnCount(): number | undefined {\n return this.meta.config?.turnCount\n }\n\n /** 设置持久化的绝对回合总数。config 惰性初始化。 */\n setTurnCount(n: number): void {\n if (!this.meta.config) this.meta.config = {}\n this.meta.config.turnCount = n\n this.meta.lastActiveAt = Date.now()\n }\n\n /** 获取持久化的草稿列表。RFC-108 D3:存储迁至 panelState 独立字段。 */\n getDrafts(): DraftEntry[] | undefined {\n return this.panelState.drafts\n }\n\n /** 设置持久化的草稿列表。max 50 条(对齐迁移前 DraftStore 上限)。RFC-108 D3:存储迁至 panelState。 */\n setDrafts(entries: DraftEntry[]): void {\n this.panelState.drafts = entries.slice(0, 50)\n this.meta.lastActiveAt = Date.now()\n }\n\n /**\n * 把存储的会话历史封顶到最近 maxMessages 条(删最早的消息条目,compaction/clear 边界不参与\n * 计数、不被裁)。调用后 messageCount 重新派生。\n *\n * 这是**纯存储/内存防御**(条数维度),**不塑造模型上下文**——与 @x-otto/memory 的\n * `prune`(截短工具文本/token 维度)、`compaction`(摘要/token 维度)是完全不同的机制,勿混。\n * canStartHistory 谓词保证封顶切点工具边界对齐(保留区第一条非孤儿 tool_result)。\n *\n * @returns 本次是否真的从内存里删除了消息条目 + 删除条数(用户可观测性——调用方据此判断\n * 是否需要向用户提示\"历史已从内存视图移除\",见 lesson `resume 数据丢失`系列排查:此前该\n * 操作零 UI 反馈,用户长会话(>2000 条)resume 后发现前段历史消失却毫无线索。**本方法只\n * 操作内存**——持久层是否也删除取决于调用方使用的 persistence 实现:RFC-159 起默认的\n * `RelationalSessionPersistence`(SQLite)是 append-forever,被本方法裁掉的条目仍完整\n * 保留在 DB;仅已废弃的 `OTTO_SESSION_BLOB=1` 逃生口后端会因整份快照覆写而真实丢失。\n * 返回 `{capped:false}` 时未发生任何裁剪(正常场景,消息数未超上限)。\n *\n * **RFC-321 R5——返回语义是\"尽力裁\"而非\"裁到 maxMessages\"**:裁剪永不跨越活跃\n * compaction 边界(R4,见 `removeOldestMessages`),故 `removedCount` 可能小于所需,\n * 裁后条数可能仍 > maxMessages。调用方**必须容忍未达标,且不得据此重试或重复发事件**\n * (否则复现 2026-07-21 的 `history-capped` 刷屏病理)。\n *\n * @returns `capped`/`removedCount` 同前;`boundaryLimited` = 因边界保护提前停止;\n * `activeOversized` = 边界前死历史已裁尽、**活跃区自身仍超限**(RFC-321 D1-a′ 逃生阀\n * 信号,供上层请求强制压缩;M1 只产出信号,消费方接线属 M2/T320-08b)。\n */\n capStoredHistory(\n maxMessages: number,\n canStartHistory?: (msg: T) => boolean,\n ): { capped: boolean; removedCount: number; boundaryLimited: boolean; activeOversized: boolean } {\n let msgCount = 0\n for (const e of this.sessionEntries) {\n if (e.type === 'message') msgCount++\n }\n if (msgCount <= maxMessages) {\n return { capped: false, removedCount: 0, boundaryLimited: false, activeOversized: false }\n }\n\n const removeCount = msgCount - maxMessages\n const { removed, boundaryLimited } = this.removeOldestMessages(removeCount, canStartHistory)\n this.meta.messageCount = msgCount - removed\n return {\n capped: removed > 0,\n removedCount: removed,\n boundaryLimited,\n // 裁后仍超限 = 边界前死历史不足以降到目标,超出部分是活跃区自身体量。\n activeOversized: this.meta.messageCount > maxMessages,\n }\n }\n\n /**\n * RFC-321 R12(M5 压缩前置):**只读预判**——把历史裁到 `targetMessages` 条是否会\n * 触及活跃上下文(即边界前的死历史不够裁)。不做任何修改。\n *\n * 用途:`applyResidencyCaps` 在裁剪**之前**调用,若返回 true 则先请求 force 压缩建立\n * 新边界、本轮跳过裁剪;下一轮边界已在,裁剪只碰死历史(无损)。这修复了\"无压缩边界\n * 时 cap 直接裁掉模型仍在用的活跃上下文且无摘要替代\"的纯信息丢失(实证:100 条裁到\n * 20 条,模型上下文直接少 80 条,无任何摘要覆盖)。\n *\n * 判据:活跃边界之前的 message 条目数 < 需要移除的条数。无活跃边界时,边界前死历史\n * 视为 0——即任何裁剪都会触及活跃上下文,恒返回 true(这正是最该先压缩的场景)。\n */\n wouldTrimTouchActiveContext(targetMessages: number): boolean {\n const msgCount = this.meta.messageCount\n if (msgCount <= targetMessages) return false\n const needRemove = msgCount - targetMessages\n\n const boundaryId = this.findActiveCompaction()?.id\n if (boundaryId === undefined) return true // 无边界:裁的必然是活跃上下文\n\n let deadBefore = 0\n for (const e of this.sessionEntries) {\n if (e.id === boundaryId) break\n if (e.type === 'message') deadBefore++\n }\n return deadBefore < needRemove\n }\n\n /** 同上,字节口径(供 `capStoredHistoryByBytes` 的前置判定)。 */\n wouldByteTrimTouchActiveContext(targetBytes: number): boolean {\n if (targetBytes <= 0 || this._estimatedContentBytes <= targetBytes) return false\n\n const boundaryId = this.findActiveCompaction()?.id\n if (boundaryId === undefined) return true\n\n // 边界前死历史全裁掉后,剩余字节是否仍超预算 → 超则必然要动活跃区。\n let deadBytes = 0\n for (const e of this.sessionEntries) {\n if (e.id === boundaryId) break\n if (e.type === 'message') deadBytes += estimateMessageContentBytes(e.message)\n }\n return this._estimatedContentBytes - deadBytes > targetBytes\n }\n\n /**\n * RFC-178 D3 硬段:把存储的会话历史按**字节估算**收紧到 maxBytes 以内(从最早的消息条目\n * 开始裁,compaction/clear 边界不裁)。与 `capStoredHistory`(条数模式)共享同一裁剪\n * 循环与工具边界对齐谓词——两模式并存时语义为\"取更紧者\"(各自独立调用,各自只在超限时\n * 生效)。同为**纯内存防御**,DB append-forever 不受影响(RFC-159)。\n *\n * 保底护栏:无论多超限,保留区至少留 1 条消息(避免字节预算配置过小时清空整个会话视图)。\n *\n * **RFC-321 R4/R5**:同样受活跃 compaction 边界保护、同样是\"尽力裁\"语义,返回字段与\n * `capStoredHistory` 同构(`activeOversized` 此处按字节口径判定:裁后估算字节仍超预算)。\n */\n capStoredHistoryByBytes(\n maxBytes: number,\n canStartHistory?: (msg: T) => boolean,\n ): { capped: boolean; removedCount: number; boundaryLimited: boolean; activeOversized: boolean } {\n if (maxBytes <= 0 || this._estimatedContentBytes <= maxBytes) {\n return { capped: false, removedCount: 0, boundaryLimited: false, activeOversized: false }\n }\n let msgCount = 0\n for (const e of this.sessionEntries) {\n if (e.type === 'message') msgCount++\n }\n // 预演从头部裁剪:数出需要移除多少条才能降到预算内(至少保留 1 条)。\n let bytesAfter = this._estimatedContentBytes\n let removeCount = 0\n for (const e of this.sessionEntries) {\n if (bytesAfter <= maxBytes || removeCount >= msgCount - 1) break\n if (e.type !== 'message') continue\n bytesAfter -= estimateMessageContentBytes(e.message)\n removeCount++\n }\n if (removeCount === 0) {\n return { capped: false, removedCount: 0, boundaryLimited: false, activeOversized: false }\n }\n const { removed, boundaryLimited } = this.removeOldestMessages(removeCount, canStartHistory)\n this.meta.messageCount = msgCount - removed\n return {\n capped: removed > 0,\n removedCount: removed,\n boundaryLimited,\n activeOversized: this._estimatedContentBytes > maxBytes,\n }\n }\n\n /**\n * 共享裁剪循环(条数/字节两模式复用):从 sessionEntries 头部移除 removeCount 条 message\n * 条目,随后按 canStartHistory 谓词继续移除至工具边界对齐(保留区第一条非孤儿\n * tool_result)。每次移除同步递减字节估算(RFC-178 D2 路径 2)。\n *\n * **RFC-321 R4(边界即止,不可削弱)**:裁剪**永不跨越活跃 compaction 边界**——遇到\n * 该边界即停止,宁可少裁(`removed < removeCount`)也不破坏 branch 链完整性。\n *\n * 修复前的缺陷(RFC-321 P1-a,实证):本循环只按 `type === 'message'` 从头部 splice,\n * 既不感知 compaction 边界、也不修复被删条目后继的 `parentId`。一旦裁剪深度超过边界前的\n * 死历史条数,边界条目的 `parentId` 就会指向已删 id → `walkBranch` 回溯撞空提前截断 →\n * 边界不在 branch 上 → `buildContext` 走 `lastBoundaryIndex === -1` 分支 → **summary 静默\n * 丢失**(模型上下文降级,用户与日志均无信号)。主要可达面是 `ResidencyGovernor` 的\n * critical 档(cap 除以 4,`residency-governor.ts`)——内存压力下恰恰最可能触发。\n *\n * 边界之前的条目全是\"已被摘要替换的死历史\",边界之后是模型仍在用的活跃上下文;以边界\n * 为界即把\"裁剪只能裁死历史\"从时序假设变成**结构不变式**(不再依赖上游触发器配合)。\n *\n * @returns `removed` 实际移除条数(可能 < removeCount);`boundaryLimited` 是否因边界\n * 保护提前停止(诊断用,非错误)。\n */\n private removeOldestMessages(\n removeCount: number,\n canStartHistory?: (msg: T) => boolean,\n ): { removed: number; boundaryLimited: boolean } {\n // 活跃边界 = 当前 leaf parent 链上最近的 compaction(即 buildContext 取用的那个)。\n // 复用既有私有方法,不另起一套链遍历(防重复职责)。无边界时 boundaryId 为\n // undefined,下方判定恒 false,行为与修复前一致(纯条数/字节裁剪)。\n const boundaryId = this.findActiveCompaction()?.id\n let boundaryLimited = false\n let removed = 0\n let i = 0\n while (i < this.sessionEntries.length && removed < removeCount) {\n const e = this.sessionEntries[i]!\n if (boundaryId !== undefined && e.id === boundaryId) {\n boundaryLimited = true\n break\n }\n if (e.type === 'message') {\n this._estimatedContentBytes -= estimateMessageContentBytes(e.message)\n this.entryMap.delete(e.id)\n this.sessionEntries.splice(i, 1)\n removed++\n } else {\n i++\n }\n }\n // 工具边界对齐(RFC-053 规则 2):保留区首条不得是孤儿 tool_result。同样受边界保护\n // ——对齐扫描遇活跃边界即停(边界本身是合法切点,其后紧邻的是 replacement 快照重放\n // 的活跃消息,不存在跨边界的孤儿)。\n if (canStartHistory) {\n while (i < this.sessionEntries.length) {\n const e = this.sessionEntries[i]!\n if (boundaryId !== undefined && e.id === boundaryId) {\n boundaryLimited = true\n break\n }\n if (e.type !== 'message') {\n i++\n continue\n }\n if (canStartHistory(e.message)) break\n this._estimatedContentBytes -= estimateMessageContentBytes(e.message)\n this.entryMap.delete(e.id)\n this.sessionEntries.splice(i, 1)\n removed++\n }\n }\n if (this._estimatedContentBytes < 0) this._estimatedContentBytes = 0\n return { removed, boundaryLimited }\n }\n\n setTags(tags: string[]): void {\n this.meta.tags = [...tags]\n }\n\n addTag(tag: string): void {\n if (!this.meta.tags.includes(tag)) {\n this.meta.tags.push(tag)\n }\n }\n\n restoreEntries(entries: SessionEntry<T>[], meta: SessionMetadata, restoredLeafId?: string): void {\n this.sessionEntries.length = 0\n this.entryMap.clear()\n // RFC-178 D2 路径 5:bulk 装载后全量重算字节估算基准(一次性 O(窗口),非热路径),\n // 此后回到增量维护。覆盖 message 正文与未剥离 compaction 的 replacement 驻留。\n let bytes = 0\n for (const entry of entries) {\n this.sessionEntries.push(entry)\n this.entryMap.set(entry.id, entry)\n if (entry.type === 'message') {\n bytes += estimateMessageContentBytes(entry.message)\n } else if (entry.type === 'compaction' && entry.replacement) {\n for (const m of entry.replacement) bytes += estimateMessageContentBytes(m)\n }\n }\n this._estimatedContentBytes = bytes\n Object.assign(this.meta, meta)\n this._leafId =\n restoredLeafId && this.entryMap.has(restoredLeafId)\n ? restoredLeafId\n : entries.length > 0\n ? entries[entries.length - 1]?.id\n : undefined\n }\n\n /**\n * 契约:`entries` 为**浅拷贝**——数组是新的,但 entry 对象按引用共享。持久化 / 时间旅行\n * 调用方即刻序列化或 restore-verbatim,不变更 entry;深拷全量消息内容代价高且无收益。\n * **调用方不得变更返回的 entry 对象**。`metadata` 已深拷(含 config),与活树独立。\n */\n toSnapshot(): {\n entries: SessionEntry<T>[]\n metadata: SessionMetadata\n leafId?: string\n } {\n return {\n entries: [...this.sessionEntries],\n metadata: this.metadata(),\n leafId: this._leafId,\n }\n }\n\n private walkBranch(): SessionEntry<T>[] {\n if (!this._leafId) {\n return []\n }\n\n const path: SessionEntry<T>[] = []\n const visited = new Set<string>()\n let current = this.entryMap.get(this._leafId)\n\n while (current && !visited.has(current.id)) {\n visited.add(current.id)\n path.push(current)\n current = current.parentId ? this.entryMap.get(current.parentId) : undefined\n }\n\n path.reverse()\n return path\n }\n\n private getBranchMessageEntries(): Array<SessionEntry<T> & { type: 'message' }> {\n const branch = this.walkBranch()\n const lastBoundaryIndex = this.lastBoundaryIndex(branch)\n return branch\n .slice(lastBoundaryIndex + 1)\n .filter((e): e is SessionEntry<T> & { type: 'message' } => e.type === 'message')\n }\n\n /** 最后一个边界条目(compaction|clear)的 branch 下标,无则 -1。 */\n private lastBoundaryIndex(branch: SessionEntry<T>[]): number {\n for (let i = branch.length - 1; i >= 0; i--) {\n const t = branch[i]?.type\n if (t === 'compaction' || t === 'clear') return i\n }\n return -1\n }\n}\n","import { InMemorySession } from './in-memory-session'\nimport type { PaginatedResult, PaginationOptions, Session, SessionStore } from './types'\n\nconst DEFAULT_PAGE_SIZE = 50\n\nexport class InMemorySessionStore<T = unknown> implements SessionStore<T> {\n private readonly sessions = new Map<string, InMemorySession<T>>()\n\n async createSession(id: string = crypto.randomUUID()): Promise<Session<T>> {\n const session = new InMemorySession<T>(id)\n this.sessions.set(id, session)\n return session\n }\n\n getSession(id: string): Session<T> | undefined {\n return this.sessions.get(id)\n }\n\n listSessions(): ReadonlyArray<Session<T>> {\n return Array.from(this.sessions.values())\n }\n\n listSessionsPaginated(options: PaginationOptions): PaginatedResult<Session<T>> {\n const { page, sortBy = 'lastActiveAt', order = 'desc' } = options\n const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE\n\n const all = Array.from(this.sessions.values()) as Session<T>[]\n\n all.sort((a, b) => {\n const metaA = a.metadata()\n const metaB = b.metadata()\n const valA = sortBy === 'createdAt' ? metaA.createdAt : metaA.lastActiveAt\n const valB = sortBy === 'createdAt' ? metaB.createdAt : metaB.lastActiveAt\n return order === 'asc' ? valA - valB : valB - valA\n })\n\n const total = all.length\n const totalPages = Math.max(1, Math.ceil(total / pageSize))\n const safePage = Math.max(0, Math.min(page, totalPages - 1))\n const start = safePage * pageSize\n const items = all.slice(start, start + pageSize)\n\n return {\n items,\n total,\n page: safePage,\n pageSize,\n totalPages,\n hasNext: safePage < totalPages - 1,\n hasPrevious: safePage > 0,\n }\n }\n\n async deleteSession(id: string): Promise<boolean> {\n return this.sessions.delete(id)\n }\n\n async forkSession(\n sourceId: string,\n newId: string = crypto.randomUUID(),\n ): Promise<Session<T> | undefined> {\n const source = this.sessions.get(sourceId)\n if (!source) {\n return undefined\n }\n\n const snapshot = source.toSnapshot()\n const forked = new InMemorySession<T>(newId, sourceId, snapshot.entries.length)\n\n forked.restoreEntries(\n [...snapshot.entries],\n {\n ...snapshot.metadata,\n createdAt: Date.now(),\n lastActiveAt: Date.now(),\n parentSessionId: sourceId,\n forkPoint: snapshot.entries.length,\n tags: [...snapshot.metadata.tags, 'fork'],\n },\n snapshot.leafId,\n )\n\n this.sessions.set(newId, forked)\n return forked\n }\n}\n\nexport function createInMemorySessionStore<T = unknown>(): SessionStore<T> {\n return new InMemorySessionStore<T>()\n}\n","import { normalizeEnv } from '@x-otto/env'\n\n/**\n * 会话域容量/超时/快照版本调优常量(RFC-074 M8-06c / R-ENVCONST)。\n *\n * 从 @x-otto/env 的 god-constants 桶迁回 session——会话域调优常量归会话包。消费方:session 包内\n * (各 persistence)+ runtime(SessionPool/time-travel/sync,已依赖 session)+ coding(SessionPool 容量)。\n * 仍用 env 的 `normalizeEnv` 解析环境变量(真跨切面 helper 留 env)。\n *\n * **例外**:会话历史存储条数上限(`SESSION_MAX_HISTORY_MESSAGES`)**不在本文件**——它的真实\n * 消费方分处 `@x-otto/runtime`(`persistence-sync.ts`)与 `@x-otto/tui`(`context-store.ts`)两个\n * 不同包,而非 `@x-otto/session` 包内部,故归口在两者共同依赖的 `@x-otto/session-contract`\n * (见 `packages/session-contract/src/constants.ts`),供跨包共享单一真源。RFC-155 M155-1。\n */\nexport const SESSION_IDLE_TIMEOUT_MS = normalizeEnv(\n process.env['OTTO_SESSION_IDLE_TIMEOUT_MS'],\n 30 * 60 * 1000,\n)\nexport const SESSION_MAX = normalizeEnv(process.env['OTTO_SESSION_MAX'], 50)\nexport const SESSION_PAGE_SIZE = normalizeEnv(process.env['OTTO_SESSION_PAGE_SIZE'], 20)\nexport const SESSION_SNAPSHOT_VERSION = normalizeEnv(\n process.env['OTTO_SESSION_SNAPSHOT_VERSION'],\n 1,\n)\n","import { MemoryPersistenceBase } from '@x-otto/persistence'\nimport { SESSION_PAGE_SIZE } from './constants'\nimport type { SessionPersistence, SessionSnapshot } from './types'\n\n/**\n * 内存态会话持久化。复用内核抽象基类 {@link MemoryPersistenceBase},仅声明 `extractId` +\n * 按 `SessionMetadata`(lastActiveAt/createdAt)取排序值——与 File/Remote 同款薄子类模式。\n * 内核已统一 save/load/delete/list/listPaginated 实现(M2-02,路径 A)。\n */\nexport class InMemorySessionPersistence<T = unknown>\n extends MemoryPersistenceBase<SessionSnapshot<T>>\n implements SessionPersistence<T>\n{\n constructor() {\n super({ defaultPageSize: SESSION_PAGE_SIZE, defaultSortBy: 'lastActiveAt' })\n }\n\n protected extractId(snapshot: SessionSnapshot<T>): string {\n return snapshot.id\n }\n\n protected override getSortValue(snapshot: SessionSnapshot<T>, sortBy: string): number {\n return sortBy === 'createdAt' ? snapshot.metadata.createdAt : snapshot.metadata.lastActiveAt\n }\n\n /** RFC-154 D1:内存态实现——过滤归档 + 按 lastActiveAt 降序 + 截断,零 IO 开销。 */\n async listRecentMeta(limit: number): Promise<string[]> {\n return this.entries()\n .filter(([, snapshot]) => !snapshot.metadata.archived)\n .sort(([, a], [, b]) => b.metadata.lastActiveAt - a.metadata.lastActiveAt)\n .slice(0, limit)\n .map(([id]) => id)\n }\n}\n","import { readdir, stat } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { DiskPersistence } from '@x-otto/persistence'\nimport { SESSION_PAGE_SIZE } from './constants'\nimport type { SessionPersistence, SessionSnapshot } from './types'\n\nexport interface FileSessionPersistenceOptions {\n baseDir: string\n}\n\n/**\n * @deprecated blob session persistence replaced by RelationalSessionPersistence。\n * 仅保留为 OTTO_SESSION_BLOB=1 逃生口;关系路径线上跑稳后删除。\n */\nexport class FileSessionPersistence<T = unknown>\n extends DiskPersistence<SessionSnapshot<T>>\n implements SessionPersistence<T>\n{\n /**\n * RFC-305 M3:文件后端整份快照覆写(非游标增量)→ 'snapshot'。该后端仅为\n * OTTO_SESSION_BLOB=1 逃生口(M63 已废弃,跑稳后删除),节流语义与 remote 一致。\n */\n readonly turnFlush = 'snapshot' as const\n\n private readonly sessionBaseDir: string\n private readonly sessionExtension = '.session.json'\n\n constructor(options: FileSessionPersistenceOptions) {\n super({\n baseDir: options.baseDir,\n extension: '.session.json',\n defaultPageSize: SESSION_PAGE_SIZE,\n })\n this.sessionBaseDir = options.baseDir\n }\n\n protected override extractId(snapshot: SessionSnapshot<T>): string {\n return snapshot.id\n }\n\n /**\n * RFC-154 D1(blob 逃生口实现):单文件即整个 snapshot(entries+metadata 打包),无法像\n * SQLite 那样只读元数据列——只能先按 mtime(lastActiveAt 代理值)排序,再逐个 load() 直到\n * 凑够 limit 个非归档会话。比\"全量 load 后排序\"仍更省:mtime 排序在前,命中 limit 后即\n * 停止读取,不会读到排序靠后但从不会被选中的文件。该路径 M63 起标记弃用,不追求极致优化\n * (RFC-154 §3 D1)。\n */\n async listRecentMeta(limit: number): Promise<string[]> {\n let files: string[]\n try {\n files = await readdir(this.sessionBaseDir)\n } catch {\n return []\n }\n const matched = files.filter((f) => f.endsWith(this.sessionExtension))\n\n const withStats = await Promise.all(\n matched.map(async (f) => {\n const filePath = join(this.sessionBaseDir, f)\n const s = await stat(filePath).catch(() => null)\n return {\n id: decodeURIComponent(f.slice(0, -this.sessionExtension.length)),\n mtime: s?.mtimeMs ?? 0,\n }\n }),\n )\n withStats.sort((a, b) => b.mtime - a.mtime)\n\n const result: string[] = []\n for (const { id } of withStats) {\n if (result.length >= limit) break\n const snapshot = await this.load(id).catch(() => null)\n if (!snapshot) continue\n if (snapshot.metadata.archived) continue\n result.push(id)\n }\n return result\n }\n}\n","/**\n * replacement-blob-codec.ts —— compaction replacement 的内容寻址编解码(RFC-160 D7)。\n *\n * 写路径:replacement: T[] → 每条消息 canonical JSON → sha256 → blob 表落库(幂等),\n * entry data 只存 replacementHashes: string[](新格式)。\n * 读路径:replacementHashes → getBlobs 组装回 replacement(顺序 = hashes 数组序)。\n * 双格式兼容:旧格式行(内嵌 replacement 数组)原样直读,永久保留读能力\n * (防备份恢复/外部导入)。\n *\n * 实测背书(2026-07-13 生产库):replacement 内嵌消息 151,203 条仅 7% distinct,\n * 228.9MB→16.7MB(-93%)。\n */\nimport { createHash } from 'node:crypto'\n\nimport type { SessionRepository } from './session-repository'\n\n/** 单条消息的内容寻址哈希(sha256 hex,前缀标记算法便于未来演进)。 */\nexport function hashMessageContent(serialized: string): string {\n return `sha256:${createHash('sha256').update(serialized, 'utf-8').digest('hex')}`\n}\n\n/**\n * 写侧:把 data blob 里的内嵌 replacement 转为 hashes(新格式),消息本体落 blob 表。\n * 非 compaction / 无 replacement / 已是新格式 → 原样返回(幂等)。\n * 返回值是**新对象**(不变更入参)。\n */\nexport async function encodeReplacementToBlobs(\n entryType: string,\n data: Record<string, unknown>,\n repo: SessionRepository,\n): Promise<Record<string, unknown>> {\n if (entryType !== 'compaction') return data\n const replacement = data['replacement']\n if (!Array.isArray(replacement)) return data\n\n const serialized = replacement.map((m) => JSON.stringify(m))\n const hashes = serialized.map(hashMessageContent)\n await repo.putBlobs(serialized.map((content, i) => ({ hash: hashes[i]!, content })))\n\n const { replacement: _inlined, ...rest } = data\n return { ...rest, replacementHashes: hashes }\n}\n\n/**\n * 读侧:把新格式行的 replacementHashes 组装回内嵌 replacement(顺序保持)。\n * 旧格式(内嵌 replacement)/ 无 hashes → 原样返回。\n * @throws blob 缺席(理论上仅残缺导入)——fail-fast,不允许静默丢消息。\n */\nexport async function decodeReplacementFromBlobs(\n entryType: string,\n data: Record<string, unknown>,\n repo: SessionRepository,\n): Promise<Record<string, unknown>> {\n if (entryType !== 'compaction') return data\n const hashes = data['replacementHashes']\n if (!Array.isArray(hashes) || hashes.length === 0) {\n if (Array.isArray(hashes)) {\n // 空 hashes = 空 replacement(合法:压缩保留 0 条的极端情形)\n const { replacementHashes: _empty, ...rest } = data\n return { ...rest, replacement: [] }\n }\n return data\n }\n\n const blobs = await repo.getBlobs(hashes as string[])\n const replacement = (hashes as string[]).map((h) => {\n const content = blobs.get(h)\n if (content == null) {\n throw new Error(\n `decodeReplacementFromBlobs: blob ${h} missing from message_blobs (corrupted ` +\n 'import/partial backup?); refusing to assemble a lossy replacement (RFC-160 D7)',\n )\n }\n return JSON.parse(content) as unknown\n })\n const { replacementHashes: _decoded, ...rest } = data\n return { ...rest, replacement }\n}\n","/**\n * snapshot ↔ rows 桥(RFC-037 阶段A)。\n *\n * 把 blob `SessionSnapshot`(entries 树)炸开成 `SessionRepository` 的行,以及反向重组。\n * 放 @x-otto/session 侧(它知道 SessionSnapshot 领域形状);row 级仓储亦在本包(RFC-074 M5-02b 迁回)。\n * blob→关系迁移 = 读旧 snapshots blob(= SessionSnapshot)后调 {@link explodeSnapshot}。\n *\n * **R6 不变量(RFC-160 D7 修订为组装后等价)**:`reassembleSnapshot(explodeSnapshot(s)) ≡ s`\n * (逐字段,含 message/compaction/replacement/clear/parentId/leaf/config/metadata)。新格式行\n * (replacementHashes)经全量组装收敛回内嵌 replacement 后同样成立(组装序 = hashes 序 = 原序)。\n * messageCount/compactionCount 由 entries 派生(R4)。\n */\nimport { decodeReplacementFromBlobs } from './replacement-blob-codec'\n\nimport type {\n PersistedSessionConfig,\n SessionEntry,\n SessionMetadata,\n SessionSnapshot,\n} from './types'\nimport type { EntryInput, EntryRow, SessionRepository, SessionRow } from './session-repository'\n\n/** snapshot → sessions 行(供 explode / RelationalSessionPersistence 复用)。 */\nexport function sessionRowFromSnapshot<T>(\n snapshot: SessionSnapshot<T>,\n workspaceKey: string,\n): SessionRow {\n const m = snapshot.metadata\n return {\n id: snapshot.id,\n workspaceKey,\n ...(m.title != null ? { title: m.title } : {}),\n ...(m.titleSource != null ? { titleSource: m.titleSource } : {}),\n ...(m.parentSessionId != null ? { parentSessionId: m.parentSessionId } : {}),\n ...(m.forkPoint != null ? { forkPoint: m.forkPoint } : {}),\n ...(snapshot.leafId != null ? { leafId: snapshot.leafId } : {}),\n tags: m.tags ?? [],\n ...(m.config != null ? { config: m.config } : {}),\n createdAt: m.createdAt,\n lastActiveAt: m.lastActiveAt,\n ...(m.archived != null ? { archived: m.archived } : {}),\n ...(m.pinned != null ? { pinned: m.pinned } : {}),\n ...(m.pinGroup != null ? { pinGroup: m.pinGroup } : {}),\n }\n}\n\n/** 一条 SessionEntry → 仓储输入(id/parentId/type/timestamp 进列,其余进 data)。 */\nexport function entryToInput<T>(e: SessionEntry<T>): EntryInput {\n const { type, id, parentId, timestamp, ...payload } = e as SessionEntry<T> & {\n parentId?: string\n timestamp: number\n }\n return {\n entryId: id,\n ...(parentId != null ? { parentId } : {}),\n type,\n data: payload,\n createdAt: timestamp,\n }\n}\n\n/** 一行 → SessionEntry(列还原 + data 展开)。 */\nfunction rowToEntry<T>(r: EntryRow): SessionEntry<T> {\n return {\n type: r.type,\n id: r.entryId,\n ...(r.parentId != null ? { parentId: r.parentId } : {}),\n timestamp: r.createdAt,\n ...(r.data as object),\n } as SessionEntry<T>\n}\n\n/** 炸开 snapshot 到仓储行(createSession + 逐条 appendEntry,保序 + parentId/leaf/config)。 */\nexport async function explodeSnapshot<T>(\n snapshot: SessionSnapshot<T>,\n workspaceKey: string,\n repo: SessionRepository,\n): Promise<void> {\n await repo.createSession(sessionRowFromSnapshot(snapshot, workspaceKey))\n for (const e of snapshot.entries) {\n await repo.appendEntry(snapshot.id, entryToInput(e))\n }\n await repo.touchSession(\n snapshot.id,\n snapshot.leafId,\n snapshot.metadata.lastActiveAt,\n snapshot.metadata.title,\n snapshot.metadata.titleSource,\n snapshot.metadata.config,\n snapshot.metadata.archived,\n snapshot.metadata.pinned,\n snapshot.metadata.pinGroup,\n )\n}\n\n/** RFC-159 D4:尾窗口加载选项。 */\nexport interface ReassembleOptions<T> {\n /** 加载条数上限:DB 条目数超过时只取尾部窗口(缺省 = 全量,行为不变)。 */\n maxEntries?: number\n /**\n * 窗口切点对齐谓词(与 InMemorySession.capStoredHistory 同语义):窗口首条消息必须\n * 满足此谓词(如 role !== 'tool_result',防孤儿 tool_result 触发 provider 400)。\n * 不满足的前导消息被丢弃;非 message 条目(compaction/clear)天然是安全切点,保留。\n * 注:compaction 的 replacement 内嵌在单条 entry 的 data 内(非独立行),窗口切割\n * 不可能拆散 compaction↔replacement 结构。\n */\n canStartHistory?: (msg: T) => boolean\n /**\n * RFC-160 D2/D5:装载时剥离\"非活跃\"compaction 的 replacement(内存投影,DB 不动)。\n * 保守保留集 = 当前 leafId parent 链上的**全部** compaction + 加载集内 seq 最大的\n * compaction;其余置 replacementStripped 标记并丢弃 replacement 数组(summary 保留)。\n * 时序:尾窗口化**先于**剥离——剥离面对窗口化后的加载集。缺省 false(行为不变)。\n */\n stripInactiveReplacements?: boolean\n}\n\n/** 从仓储行重组 SessionSnapshot(metadata 计数派生,R4;窗口加载时计数走 DB 权威,RFC-159 D4)。 */\nexport async function reassembleSnapshot<T>(\n id: string,\n repo: SessionRepository,\n options?: ReassembleOptions<T>,\n): Promise<SessionSnapshot<T> | null> {\n const s = await repo.getSession(id)\n if (!s) return null\n\n let entries: SessionEntry<T>[]\n let windowed = false\n if (options?.maxEntries != null) {\n const total = await repo.getEntryCount(id)\n if (total > options.maxEntries) {\n entries = (await repo.loadTailEntries(id, options.maxEntries)).map((r) => rowToEntry<T>(r))\n windowed = true\n if (options.canStartHistory) {\n entries = alignWindowStart(entries, options.canStartHistory)\n }\n } else {\n entries = (await repo.loadEntries(id)).map((r) => rowToEntry<T>(r))\n }\n } else {\n entries = (await repo.loadEntries(id)).map((r) => rowToEntry<T>(r))\n }\n\n // RFC-160 D2/D5:剥离非活跃 compaction 的 replacement(尾窗口化之后执行)。\n // D7 协同:新格式行(replacementHashes)天然是\"剥离态存储\"——非活跃的直接标记(零 blob\n // 读取),仅活跃边界组装真身;旧格式行照旧剥离/保留。无剥离选项时全量组装(R6 组装后等价)。\n if (options?.stripInactiveReplacements) {\n entries = stripInactiveReplacements(entries, s.leafId ?? undefined)\n entries = await assembleHashedReplacements(entries, repo, { onlyUnstripped: true })\n } else {\n entries = await assembleHashedReplacements(entries, repo, { onlyUnstripped: false })\n }\n\n // 窗口加载时计数必须反映全量(DB 权威),不能用加载集 filter(那只是窗口)。\n const messageCount = windowed\n ? await repo.countEntriesByType(id, 'message')\n : entries.filter((e) => e.type === 'message').length\n const compactionCount = windowed\n ? await repo.countEntriesByType(id, 'compaction')\n : entries.filter((e) => e.type === 'compaction').length\n\n const metadata: SessionMetadata = {\n createdAt: s.createdAt,\n lastActiveAt: s.lastActiveAt,\n messageCount,\n compactionCount,\n tags: s.tags,\n ...(s.title != null ? { title: s.title } : {}),\n ...(s.titleSource != null ? { titleSource: s.titleSource } : {}),\n ...(s.parentSessionId != null ? { parentSessionId: s.parentSessionId } : {}),\n ...(s.forkPoint != null ? { forkPoint: s.forkPoint } : {}),\n ...(s.config != null ? { config: s.config as PersistedSessionConfig } : {}),\n ...(s.archived != null ? { archived: s.archived } : {}),\n ...(s.pinned != null ? { pinned: s.pinned } : {}),\n ...(s.pinGroup != null ? { pinGroup: s.pinGroup } : {}),\n }\n return {\n version: 1,\n id,\n entries,\n metadata,\n ...(s.leafId != null ? { leafId: s.leafId } : {}),\n }\n}\n\n/**\n * RFC-160 D7:新格式行(replacementHashes)按需组装回内嵌 replacement。\n * - onlyUnstripped: true(剥离模式)——只组装未被剥离的 compaction(活跃边界),\n * 剥离态的直接置 replacementStripped 标记(hashes 一并移除,零 blob 读取);\n * - onlyUnstripped: false(全量模式,R6 组装后等价)——全部组装。\n * 旧格式行(内嵌 replacement)不受影响。\n */\nasync function assembleHashedReplacements<T>(\n entries: SessionEntry<T>[],\n repo: SessionRepository,\n opts: { onlyUnstripped: boolean },\n): Promise<SessionEntry<T>[]> {\n const result: SessionEntry<T>[] = []\n for (const e of entries) {\n const raw = e as SessionEntry<T> & {\n type: string\n replacementHashes?: string[]\n replacementStripped?: true\n }\n if (raw.type !== 'compaction' || raw.replacementHashes == null) {\n result.push(e)\n continue\n }\n if (opts.onlyUnstripped && raw.replacementStripped) {\n // 剥离态新格式:hashes 移除(与旧格式剥离产物同构——只剩 summary + 标记)\n const { replacementHashes: _dropped, ...rest } = raw\n result.push(rest as SessionEntry<T>)\n continue\n }\n const decoded = await decodeReplacementFromBlobs(\n 'compaction',\n raw as unknown as Record<string, unknown>,\n repo,\n )\n // decode 后剥离标记不应残留(有真身即非剥离态)\n const { replacementStripped: _clear, ...clean } = decoded\n result.push(clean as unknown as SessionEntry<T>)\n }\n return result\n}\n\n/**\n * RFC-160 D2/D5:剥离非活跃 compaction 的 replacement(内存投影)。\n *\n * 保留集 = **活跃边界**(leafId parent 链上**最近**的一个 compaction,即 buildContext 的\n * lastBoundaryIndex 消费者;clear 截断)+ 加载集内序最大的 compaction(leafId 缺失/悬空\n * 时的兜底边界;通常与前者同一条)。其余 compaction 剥离 replacement、置标记、保留 summary。\n *\n * 修正记录(M1 探针发现):初版按评审建议\"活跃链**全部** compaction 保留\"——但线性会话\n * (绝大多数)中所有历史 compaction 都在 leaf 的 parent 链上,全保留 = 零剥离,收益归零\n * (fixture 实测 ratio 0.999)。buildContext 只消费链上**最近**边界,更早的链上 compaction\n * 是已翻页的历史——正确语义是只保留最近者;branch()/restore 复位旧边界由 fail-fast +\n * hydrate 兜底(D5/D3)。\n */\nfunction stripInactiveReplacements<T>(\n entries: SessionEntry<T>[],\n leafId: string | undefined,\n): SessionEntry<T>[] {\n const byId = new Map<string, SessionEntry<T>>()\n for (const e of entries) byId.set(e.id, e)\n\n // 活跃边界:从 leafId 沿 parentId 回溯遇到的第一个 compaction(clear 截断)。\n let activeBoundaryId: string | undefined\n const visited = new Set<string>()\n let cur = leafId ? byId.get(leafId) : undefined\n while (cur && !visited.has(cur.id)) {\n visited.add(cur.id)\n if (cur.type === 'compaction') {\n activeBoundaryId = cur.id\n break\n }\n if (cur.type === 'clear') break\n cur = cur.parentId ? byId.get(cur.parentId) : undefined\n }\n\n let lastCompactionIdx = -1\n for (let i = entries.length - 1; i >= 0; i--) {\n if (entries[i]!.type === 'compaction') {\n lastCompactionIdx = i\n break\n }\n }\n\n return entries.map((e, i) => {\n if (e.type !== 'compaction') return e\n if (e.id === activeBoundaryId || i === lastCompactionIdx) return e\n const c = e as SessionEntry<T> & {\n type: 'compaction'\n replacementHashes?: string[]\n }\n if (c.replacement != null) {\n // 旧格式(内嵌拷贝):丢弃数组、置标记\n const { replacement: _dropped, ...rest } = c\n return { ...rest, replacementStripped: true as const } as SessionEntry<T>\n }\n if (c.replacementHashes != null) {\n // 新格式(D7 hashes):置标记,hashes 由 assembleHashedReplacements 剥离模式移除\n return { ...c, replacementStripped: true as const } as SessionEntry<T>\n }\n return e // 位置式最旧格式,无可剥离\n })\n}\n\n/**\n * 窗口首部对齐(RFC-159 D4):丢弃窗口头部不满足 canStartHistory 的 message 条目\n * (典型 = 孤儿 tool_result,其配对的 assistant tool_call 在窗口之外),直到首条 message\n * 满足谓词。非 message 条目(compaction/clear 边界)是安全切点,原样保留。\n * 与 InMemorySession.capStoredHistory 的保留区对齐逻辑同语义。\n */\nfunction alignWindowStart<T>(\n entries: SessionEntry<T>[],\n canStartHistory: (msg: T) => boolean,\n): SessionEntry<T>[] {\n let firstValid = 0\n while (firstValid < entries.length) {\n const e = entries[firstValid]!\n if (e.type !== 'message') break\n if (canStartHistory((e as SessionEntry<T> & { type: 'message' }).message)) break\n firstValid++\n }\n return firstValid > 0 ? entries.slice(firstValid) : entries\n}\n","/**\n * replacement-hydrator.ts —— 剥离态 compaction 的按需真身取回(RFC-160 D3)。\n *\n * 给定含剥离态的 entries,点查 DB(loadEntriesByIds,唯一索引)回填 replacement、\n * 清除标记。调用方 = 三个危险出口(checkpoint 捕获前 / fork 前 / restore 后)与\n * branch() fail-fast 后的补救路径。hydrate 失败(DB 行不可达)fail-fast 抛错——\n * 剥离态严禁静默降级为正文(RFC-160 §5 规则 3)。\n */\nimport { decodeReplacementFromBlobs } from './replacement-blob-codec'\n\nimport type { SessionEntry } from './types'\nimport type { SessionRepository } from './session-repository'\n\n/**\n * 持久层可选能力接口(RFC-160 D3):支持剥离态 hydrate 的 persistence 实现它。\n * runtime 的三出口(checkpoint/fork/restore)按能力探测调用——非 Relational 后端\n * (file/memory/remote)不实现,出口断言 hasStrippedReplacements 恒 false(它们不剥离),\n * 天然跳过。\n */\nexport interface ReplacementHydrationCapable<T = unknown> {\n hydrateReplacements(\n sessionId: string,\n entries: readonly SessionEntry<T>[],\n ): Promise<SessionEntry<T>[]>\n}\n\n/** 能力探测(类型守卫)。 */\nexport function supportsReplacementHydration<T>(\n persistence: unknown,\n): persistence is ReplacementHydrationCapable<T> {\n return typeof (persistence as ReplacementHydrationCapable<T>)?.hydrateReplacements === 'function'\n}\n\n/** entries 中是否存在剥离态 compaction(D4 出口断言的判定原语)。 */\nexport function hasStrippedReplacements<T>(entries: readonly SessionEntry<T>[]): boolean {\n return entries.some(\n (e) => e.type === 'compaction' && (e as { replacementStripped?: true }).replacementStripped,\n )\n}\n\n/**\n * 回填 entries 中全部剥离态 compaction 的 replacement 真身。\n * 返回新数组(原数组不变更);无剥离态时原样返回(零拷贝快路径)。\n * @throws DB 行不可达时(理论上仅跨机导入残缺数据)——fail-fast,不允许剥离态继续。\n */\nexport async function hydrateStrippedReplacements<T>(\n sessionId: string,\n entries: readonly SessionEntry<T>[],\n repo: SessionRepository,\n): Promise<SessionEntry<T>[]> {\n const strippedIds = entries\n .filter(\n (e) => e.type === 'compaction' && (e as { replacementStripped?: true }).replacementStripped,\n )\n .map((e) => e.id)\n if (strippedIds.length === 0) return [...entries]\n\n const rows = await repo.loadEntriesByIds(sessionId, strippedIds)\n const bodyById = new Map<string, Record<string, unknown>>()\n for (const r of rows) bodyById.set(r.entryId, r.data as Record<string, unknown>)\n\n const result: SessionEntry<T>[] = []\n for (const e of entries) {\n if (e.type !== 'compaction' || !(e as { replacementStripped?: true }).replacementStripped) {\n result.push(e)\n continue\n }\n let body = bodyById.get(e.id)\n // RFC-160 D7:权威行可能是新格式(replacementHashes)——先经 blob 组装收敛回内嵌形态。\n if (body && body['replacementHashes'] != null) {\n body = await decodeReplacementFromBlobs('compaction', body, repo)\n }\n if (!body || body['replacement'] == null) {\n throw new Error(\n `hydrateStrippedReplacements: authoritative row for compaction \"${e.id}\" in session ` +\n `\"${sessionId}\" is missing or has no replacement (corrupted import?); refusing to ` +\n 'continue with stripped state (RFC-160 rule 3)',\n )\n }\n const { replacementStripped: _cleared, ...rest } = e as SessionEntry<T> & {\n type: 'compaction'\n replacementStripped?: true\n }\n result.push({ ...rest, replacement: body['replacement'] as T[] } as SessionEntry<T>)\n }\n return result\n}\n","// session/src/session-write-lease.ts — Per-session single-writer lease.\n// RFC-159 D3: O_EXCL lockfile + token + heartbeat + stale takeover.\n// No lease → read-only mode. RFC-171 D2: token:pid:hostname + liveness probe.\n// Design tradeoff vs OAuth refresh lock (RFC-199 D5): short-hold (mtime) vs long-hold (pid probe).\nimport {\n closeSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n rmSync,\n statSync,\n unlinkSync,\n utimesSync,\n writeSync,\n} from 'node:fs'\nimport { join } from 'node:path'\nimport { randomUUID } from 'node:crypto'\n\nimport { currentHostname, decodeLeaseToken, encodeLeaseToken, isProcessAlive } from '@x-otto/env'\n\n/** 拿不到写租约时 save 抛出的类型化错误(供上层识别并发事件与普通 IO 失败)。 */\nexport class SessionWriteLeaseDeniedError extends Error {\n constructor(readonly sessionId: string) {\n super(\n `Session write lease denied for ${sessionId}: another live process owns the write lease ` +\n '(this process is read-only for the session; refusing to save — RFC-159 D3)',\n )\n this.name = 'SessionWriteLeaseDeniedError'\n }\n}\n\nexport interface SessionWriteLeaseManager {\n /** 获取(或确认持有)某会话的写租约。false = 另一存活进程持有。幂等。 */\n acquire(sessionId: string): boolean\n /** 释放某会话的租约(仅当仍归本进程)。幂等。 */\n release(sessionId: string): void\n /** 释放本进程持有的全部租约(app 停机/进程退出用;同步,exit 钩子安全)。 */\n releaseAll(): void\n /** 本进程当前是否持有某会话租约(token 实核,非本地缓存)。 */\n isOwner(sessionId: string): boolean\n /**\n * 只读探测(无副作用,不 acquire/不 touch 锁文件):某会话当前是否被**另一存活进程**\n * 持有写租约。用于 `/resume` 列表等展示场景——提前告知用户\"切进去会是只读\",\n * 而非等到真正 save 被拒才事后感知(见 handlers/session.ts /resume)。\n *\n * 语义与 acquire() 的接管判定对齐但不产生任何接管/写入:锁文件不存在 → false;\n * 锁文件存在且是本进程持有(isOwner) → false(自己不算\"另一进程\");\n * 锁文件存在且未失活(未超 staleMs) → true;已失活(视为崩溃残留) → false\n * (不阻止未来 acquire 接管,避免误报\"只读\")。\n */\n peekLockedByOther(sessionId: string): boolean\n /**\n * 只读诊断(无副作用):返回某会话锁的当前状态详情,供用户面向的诊断/强制解锁命令\n * 消费(RFC-171 D4,`/session unlock` 只读模式)。内部复用与 `peekLockedByOther` 相同的\n * 读锁文件逻辑,避免重复实现。\n */\n inspect(sessionId: string): LeaseInspection\n /**\n * 强制释放某会话的锁,跳过 token 实核(不要求本进程是持有者)。仅供用户主动触发的\n * 强制解锁命令使用(RFC-171 D4),用于 `acquire()` 存活性探测无法自动恢复的场景\n * (如 hung 进程:心跳停摆但 pid 未退出)。调用方负责在调用前完成风险确认。\n */\n forceRelease(sessionId: string): void\n /**\n * 终态关闭(停机专用,幂等):releaseAll() + 此后 acquire() 恒返回 false。\n *\n * 背景(2026-07-14 修复):`/exit -r` 陪跑重启下父进程 spawn 子进程后**不退出**\n * (持续陪跑到子进程退出,见 cli.ts),`process.on('exit')` 释放钩子因此永不触发——\n * stopApp() 的 saveAll() 又会在 spawn 之前 lazy-acquire 租约,导致新会话进程对同一\n * session 永久只读(心跳持续续约,60s stale takeover 永不满足)。\n *\n * `close()` 必须在 stopApp() 返回、子进程 spawn 之前被调用(当前接线在\n * session-pool-factory.ts 的 `onDispose`,位于 agentRuntime.dispose() 内,正确处于\n * saveAll() 之后、app.stop() 返回之前)。`closed` 标志防止 dispose 链之后迟到的\n * fire-and-forget save 重新 acquire、复活已死的锁文件+心跳。\n */\n close(): void\n}\n\n/** `inspect()` 返回的锁状态详情(RFC-171 D4)。 */\nexport interface LeaseInspection {\n /** 锁文件是否存在。 */\n readonly locked: boolean\n /** 持有者 pid(旧格式锁文件或畸形内容时为 undefined)。 */\n readonly pid?: number\n /** 持有者 hostname。 */\n readonly hostname?: string\n /**\n * 存活性探测结果:`true`=确认存活,`false`=确认死亡,`undefined`=无法判断\n * (跨 hostname、或无 pid 信息)。\n */\n readonly alive?: boolean\n /** 锁文件 mtime 距今毫秒数(用于诊断\"是否疑似 hung 进程\"或\"是否刚释放\")。 */\n readonly mtimeAgoMs?: number\n}\n\nexport interface SessionWriteLeaseOptions {\n /** 锁文件目录(如 `<sessionsDir>/locks`),不存在时自动创建。 */\n lockDir: string\n /**\n * 失活阈值:锁文件 mtime 距今超过此值视为持有者已崩溃,可被接管。\n * 必须显著大于 renewEveryMs(默认 60s vs 10s = 6x)。\n */\n staleMs?: number\n /** 心跳间隔:后台 unref 定时器 touch 锁文件 mtime,保活 idle 会话的租约。 */\n renewEveryMs?: number\n /** 是否注册 process exit 钩子同步释放(默认 true;测试注入 false 避免跨用例泄漏)。 */\n installExitHook?: boolean\n}\n\nconst DEFAULT_STALE_MS = 60_000\nconst DEFAULT_RENEW_EVERY_MS = 10_000\n\ninterface HeldLease {\n token: string\n timer: ReturnType<typeof setInterval>\n}\n\nexport function createSessionWriteLeaseManager(\n options: SessionWriteLeaseOptions,\n): SessionWriteLeaseManager {\n const staleMs = options.staleMs ?? DEFAULT_STALE_MS\n const renewEveryMs = options.renewEveryMs ?? DEFAULT_RENEW_EVERY_MS\n const held = new Map<string, HeldLease>()\n let closed = false\n const myHostname = currentHostname()\n\n const lockPath = (sessionId: string): string => join(options.lockDir, `${sessionId}.lock`)\n\n /** 读锁文件原始内容并解码为 `{ token, pid?, host? }`;文件不存在返回 null。 */\n const readDecoded = (sessionId: string): { token: string; pid?: number; host?: string } | null => {\n try {\n const raw = readFileSync(lockPath(sessionId), 'utf-8').trim()\n return decodeLeaseToken(raw)\n } catch {\n return null\n }\n }\n\n /** 仅取 token 字段(fencing 比对用,兼容旧调用点语义)。 */\n const readToken = (sessionId: string): string | null => readDecoded(sessionId)?.token ?? null\n\n const writeLock = (sessionId: string, token: string): boolean => {\n try {\n mkdirSync(options.lockDir, { recursive: true })\n const fd = openSync(lockPath(sessionId), 'wx') // O_EXCL\n try {\n writeSync(fd, encodeLeaseToken(token, process.pid, myHostname))\n } finally {\n closeSync(fd)\n }\n return true\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err\n return false\n }\n }\n\n /**\n * 判断锁文件当前持有者是否\"可确认已死亡\"(RFC-171 D2):本机同 hostname 且\n * `isProcessAlive` 返回 false。跨 hostname 或无法解码 pid(旧格式/畸形内容)一律\n * 返回 false(不做判断,交由调用方走现有 stale-timeout 逻辑,不引入新风险)。\n */\n const isConfirmedDead = (decoded: { pid?: number; host?: string }): boolean => {\n if (decoded.pid === undefined || decoded.host === undefined) return false\n if (decoded.host !== myHostname) return false\n return !isProcessAlive(decoded.pid)\n }\n\n /**\n * 判断锁文件当前持有者是否\"可确认存活\":本机同 hostname 且 `isProcessAlive` 返回\n * true。用于 D2 的\"存活时即便超过 staleMs 也拒绝接管\"分支——跨 hostname 或无 pid\n * 信息时返回 false(不构成\"确认存活\",交由 stale-timeout 兜底判断)。\n */\n const isConfirmedAlive = (decoded: { pid?: number; host?: string }): boolean => {\n if (decoded.pid === undefined || decoded.host === undefined) return false\n if (decoded.host !== myHostname) return false\n return isProcessAlive(decoded.pid)\n }\n\n const startHeartbeat = (sessionId: string, token: string): void => {\n const timer = setInterval(() => {\n // 只在锁仍归我时续期(防接管后误 touch 他人锁——同 fire-ownership renew 语义)。\n if (readToken(sessionId) !== token) {\n stopTracking(sessionId)\n return\n }\n try {\n const t = new Date()\n utimesSync(lockPath(sessionId), t, t)\n } catch {\n stopTracking(sessionId) // 锁文件消失,失去所有权\n }\n }, renewEveryMs)\n timer.unref() // 不吊住事件循环(RFC-085 退出卫生)\n held.set(sessionId, { token, timer })\n }\n\n const stopTracking = (sessionId: string): void => {\n const lease = held.get(sessionId)\n if (lease) {\n clearInterval(lease.timer)\n held.delete(sessionId)\n }\n }\n\n const manager: SessionWriteLeaseManager = {\n acquire(sessionId: string): boolean {\n if (closed) return false\n const existing = held.get(sessionId)\n if (existing) {\n // token 实核:可能已被 stale 接管(本进程假死后复活的场景,fencing 语义)。\n if (readToken(sessionId) === existing.token) return true\n stopTracking(sessionId)\n }\n\n const token = randomUUID()\n if (writeLock(sessionId, token)) {\n startHeartbeat(sessionId, token)\n return true\n }\n\n // 锁已存在——先尝试存活性探测(RFC-171 D2),探测无法判定时退化为 mtime stale-timeout。\n const decoded = readDecoded(sessionId)\n let mtime: number\n try {\n mtime = statSync(lockPath(sessionId)).mtimeMs\n } catch {\n // 两次调用之间消失了——重试创建。\n if (writeLock(sessionId, token)) {\n startHeartbeat(sessionId, token)\n return true\n }\n return false\n }\n\n if (decoded !== null && isConfirmedAlive(decoded)) {\n // 明确探测到本机同 hostname 的持有者仍存活——即便已超过 staleMs 也拒绝接管\n // (防止心跳短暂延迟被误判为崩溃;代价见模块头注释\"hung 进程死锁\"权衡)。\n return false\n }\n\n const confirmedDead = decoded !== null && isConfirmedDead(decoded)\n if (!confirmedDead && Date.now() - mtime <= staleMs) return false\n\n // stale 或已确认死亡:先原子挪走再重建(同 fire-ownership,防两个接管者互踩)。\n const claimPath = `${lockPath(sessionId)}.stale-${randomUUID()}`\n try {\n renameSync(lockPath(sessionId), claimPath)\n rmSync(claimPath, { force: true })\n } catch {\n return false // 竞争接管输了——让赢家保有\n }\n if (writeLock(sessionId, token)) {\n startHeartbeat(sessionId, token)\n return true\n }\n return false\n },\n\n release(sessionId: string): void {\n const lease = held.get(sessionId)\n if (!lease) return\n if (readToken(sessionId) === lease.token) {\n try {\n unlinkSync(lockPath(sessionId))\n } catch {\n // best-effort:锁文件可能已被清理\n }\n }\n stopTracking(sessionId)\n },\n\n releaseAll(): void {\n for (const sessionId of [...held.keys()]) {\n manager.release(sessionId)\n }\n },\n\n isOwner(sessionId: string): boolean {\n if (closed) return false\n const lease = held.get(sessionId)\n return lease != null && readToken(sessionId) === lease.token\n },\n\n peekLockedByOther(sessionId: string): boolean {\n if (closed) return false\n // 自己持有 → 不算\"另一进程\"(与 acquire 的接管判定同语义:token 实核而非本地缓存)。\n const lease = held.get(sessionId)\n if (lease != null && readToken(sessionId) === lease.token) return false\n\n let mtime: number\n try {\n mtime = statSync(lockPath(sessionId)).mtimeMs\n } catch {\n return false // 无锁文件 → 未被任何进程持有\n }\n // 失活(超 staleMs 无心跳)视为崩溃残留,不阻止后续 acquire 接管——不误报\"只读\"。\n return Date.now() - mtime <= staleMs\n },\n\n inspect(sessionId: string): LeaseInspection {\n const decoded = readDecoded(sessionId)\n if (decoded === null) return { locked: false }\n\n let mtimeAgoMs: number | undefined\n try {\n mtimeAgoMs = Date.now() - statSync(lockPath(sessionId)).mtimeMs\n } catch {\n return { locked: false } // 两次读取之间锁文件消失\n }\n\n const alive = decoded.pid !== undefined && decoded.host !== undefined\n ? (decoded.host === myHostname ? isProcessAlive(decoded.pid) : undefined)\n : undefined\n\n return {\n locked: true,\n pid: decoded.pid,\n hostname: decoded.host,\n alive,\n mtimeAgoMs,\n }\n },\n\n forceRelease(sessionId: string): void {\n // 与 release() 的关键区别:跳过 token 实核(readToken(...) === lease.token)——\n // 用户主动强制解锁不要求本进程是持有者,直接 unlinkSync(RFC-171 D4 逃生舱,\n // 供 hung 进程死锁等 acquire() 无法自动恢复的场景使用)。\n try {\n unlinkSync(lockPath(sessionId))\n } catch {\n // best-effort:锁文件可能已不存在\n }\n stopTracking(sessionId)\n },\n\n close(): void {\n closed = true\n manager.releaseAll()\n },\n }\n\n if (options.installExitHook !== false) {\n // 正常退出(含 /exit -r 陪跑)同步释放全部租约,子进程 spawn 后立即可接管;\n // 崩溃/SIGKILL 不经此钩子,由 stale takeover 兜底。幂等且 unlinkSync 同步,exit 安全。\n process.on('exit', () => manager.releaseAll())\n }\n\n return manager\n}\n\n/**\n * RFC-345 §R5:影子模式无操作写租约 stub。\n *\n * 写租约的目的是**跨进程互斥**保护持久化 auth/session 数据(RFC-159 D3,防 2026-07-13\n * 数据丢失事故)。影子会话零落盘、无外部可见引用、无跨进程并发写同一磁盘会话——该风险\n * 从根不存在,故写租约在影子态可安全退化为 no-op(本函数是该豁免的唯一实现处):\n * - acquire 恒 true(本进程\"独占\"内存会话,无竞争者);\n * - peekLockedByOther / inspect.locked 恒 false(无锁文件);\n * - 全部方法不碰文件系统(不建 lockDir、不写 .lock、无心跳定时器)。\n *\n * 注意:这不削弱 RFC-159 红线——red line 保护的是\"持久化数据被并发写破坏\",无持久化则\n * 无保护对象。next_seq 单调等**数据完整性**不变量仍由 :memory: SqliteSessionRepository\n * 等价保持(不在本 stub 豁免范围)。\n */\nexport function createNoopSessionWriteLeaseManager(): SessionWriteLeaseManager {\n return {\n acquire: () => true,\n release: () => {},\n releaseAll: () => {},\n isOwner: () => true,\n peekLockedByOther: () => false,\n inspect: (): LeaseInspection => ({ locked: false }),\n forceRelease: () => {},\n close: () => {},\n }\n}\n","import { entryToInput, reassembleSnapshot, sessionRowFromSnapshot } from './relational-bridge'\nimport { encodeReplacementToBlobs } from './replacement-blob-codec'\nimport { hydrateStrippedReplacements } from './replacement-hydrator'\nimport { SessionWriteLeaseDeniedError } from './session-write-lease'\n\nimport type {\n PaginatedResult,\n PaginationOptions,\n SessionEntry,\n SessionPersistence,\n SessionSnapshot,\n} from './types'\nimport type { SessionRepository } from './session-repository'\nimport type { SessionWriteLeaseManager } from './session-write-lease'\n\nconst DEFAULT_PAGE_SIZE = 20\n\n/** 极简日志口子(避免 @x-otto/session 依赖 @x-otto/shared);缺省 no-op,App 接线可注真 logger。 */\ninterface MinimalLogger {\n warn(obj: Record<string, unknown>, msg: string): void\n}\nconst NOOP_LOGGER: MinimalLogger = { warn: () => {} }\n\n/**\n * RFC-037 阶段B:会话持久化走 `SessionRepository`(关系化 + append-only),取代 blob 整写。\n * RFC-159:append-forever——save 路径**永不删除** DB entries 行(内存被 capStoredHistory\n * 裁剪后短于 DB 是合法常态),游标 entry_id 制防裁剪位移漏存。\n *\n * - `save`:**只追加新 tail entries**(游标 entry_id 之后的条目)——写放大从 O(n)/save\n * 降到 O(新增条数)。createSession/appendEntry 幂等(规则1),重复 save 安全。\n * - `load`:`reassembleSnapshot` 还原 snapshot → 上层 InMemorySession 照常在内存走 parent 链\n * 重建上下文(规则2:读不优化即天然满足,buildContext 语义不变)。\n *\n * 实例按 `workspaceKey` 绑定(RFC-034 分区);list/listPaginated 仅返回本 workspace 会话。\n */\nexport class RelationalSessionPersistence<T = unknown> implements SessionPersistence<T> {\n private readonly repo: SessionRepository\n /**\n * 惰性解析 workspaceKey:RFC-034/M60-02 切换中,持久化在 App 构造期建好,而 `workspaceRef`\n * 在 `start()` 才解析(异步 walk-up)。传 getter 即可在首次 save/load(start() 恢复阶段、\n * resolveLocal 之后)读到真 `ws_<id>`,无需重构生命周期。传字符串则为静态键(组件测试用)。\n */\n private readonly resolveWorkspaceKey: () => string\n private readonly defaultPageSize: number\n private readonly logger: MinimalLogger\n /**\n * 每会话最后已持久化的 entry_id(增量 append 游标)。\n *\n * RFC-159 D2:游标从\"条数制\"改\"entry_id 制\"。条数制在 capStoredHistory 裁剪内存头部后\n * 会产生数组位移——`entries[已持久化数:]` 切片起点跳过真正的新消息,静默漏存(生产事故\n * dd6d6e73 的 R4 缺陷)。entry_id 制按身份定位追加起点,与数组位移无关;定位失败\n * (游标条目已被内存裁掉、或 clear/fork 重建了 entries)退化为全量幂等 append\n * (appendEntry 按 entry_id 幂等,安全无重复)。\n */\n private readonly persisted = new Map<string, string>()\n\n /** RFC-159 D4:load 尾窗口配置(缺省不开窗,全量加载,行为不变)。 */\n private readonly maxLoadEntries: number | undefined\n private readonly canStartHistory: ((msg: T) => boolean) | undefined\n /** RFC-159 D3:per-session 单写者租约(缺省无防护,由组装根注入)。 */\n private readonly writeLease: SessionWriteLeaseManager | undefined\n /** RFC-160 D2:load 剥离开关(组装根注入)。 */\n private readonly stripInactiveReplacements: boolean\n\n constructor(options: {\n repo: SessionRepository\n workspaceKey?: string | (() => string | undefined)\n defaultPageSize?: number\n logger?: MinimalLogger\n /**\n * RFC-159 D4:超过此条数的会话 load 只取尾部窗口(DB 全量保留,append-forever)。\n * 调用方传 SESSION_MAX_HISTORY_MESSAGES(@x-otto/session-contract)——本包不依赖\n * contract 包,经参数注入保持依赖方向。\n */\n maxLoadEntries?: number\n /** 窗口切点对齐谓词(防孤儿 tool_result,与 capStoredHistory 同语义)。 */\n canStartHistory?: (msg: T) => boolean\n /**\n * RFC-159 D3:per-session 单写者租约。注入后每次 save 先 acquire——拿不到\n * (另一存活进程持有)抛 SessionWriteLeaseDeniedError,本进程对该会话只读。\n * 缺省不注入 = 无租约防护(单进程测试/嵌入场景),行为不变。\n */\n writeLease?: SessionWriteLeaseManager\n /** RFC-160 D2:load 时剥离非活跃 compaction 的 replacement(缺省 false,行为不变)。 */\n stripInactiveReplacements?: boolean\n }) {\n this.repo = options.repo\n const wk = options.workspaceKey\n this.resolveWorkspaceKey =\n typeof wk === 'function' ? () => wk() || 'default' : () => wk || 'default'\n this.defaultPageSize = options.defaultPageSize ?? DEFAULT_PAGE_SIZE\n this.logger = options.logger ?? NOOP_LOGGER\n this.maxLoadEntries = options.maxLoadEntries\n this.canStartHistory = options.canStartHistory\n this.writeLease = options.writeLease\n this.stripInactiveReplacements = options.stripInactiveReplacements ?? false\n }\n\n\n /**\n * RFC-159 D3:archive/restore 同样是**写路径**,必须过写租约闸门。\n *\n * RFC-330 M2 审查发现:archive 写 `sessions.archived=1`、restore 追加 entries,\n * 二者都改 DB,但此前直接透传 repo、绕过了 `save()` 上的租约检查——\n * 另一存活进程持锁时仍会被本进程改写,与 D3\"绝不静默覆盖\"的红线冲突。\n * 此处复用与 save() 完全相同的 acquire 语义(幂等 + stale takeover)。\n */\n private assertWritable(sessionId: string): void {\n if (this.writeLease && !this.writeLease.acquire(sessionId)) {\n this.logger.warn(\n { sessionId },\n 'archive/restore rejected: session write lease held by another live process (read-only; RFC-159 D3)',\n )\n throw new SessionWriteLeaseDeniedError(sessionId)\n }\n }\n\n /** RFC-325 M4:透传 repository 的 archive 能力(只有 SQLite repo 实现)。 */\n async archiveSession(sessionId: string): Promise<string> {\n const repo = this.repo as SessionRepository & {\n archiveSession?: (id: string) => Promise<string>\n }\n if (typeof repo.archiveSession !== 'function') {\n throw new Error('archiveSession is not supported by this repository')\n }\n this.assertWritable(sessionId)\n return repo.archiveSession(sessionId)\n }\n\n /** RFC-325 M4:透传 repository 的 restore 能力。 */\n async restoreArchivedSession(sessionId: string): Promise<number> {\n const repo = this.repo as SessionRepository & {\n restoreArchivedSession?: (id: string) => Promise<number>\n }\n if (typeof repo.restoreArchivedSession !== 'function') return 0\n this.assertWritable(sessionId)\n return repo.restoreArchivedSession(sessionId)\n }\n\n async save(snapshot: SessionSnapshot<T>): Promise<void> {\n // RFC-159 D3:写租约闸门——本 save() 是所有会话写路径的单一汇聚点(persistence-sync\n // doSave/saveSnapshot、removeSession 前 flush 等全部经此)。\n // 拿不到租约 = 另一存活进程正在写此会话 → fail-safe 抛类型化错误,绝不静默覆盖。\n // acquire 幂等且带 stale takeover(崩溃残留锁超阈值自动接管),正常竞态下先到者胜。\n if (this.writeLease && !this.writeLease.acquire(snapshot.id)) {\n this.logger.warn(\n { sessionId: snapshot.id },\n 'save rejected: session write lease held by another live process (read-only; RFC-159 D3)',\n )\n throw new SessionWriteLeaseDeniedError(snapshot.id)\n }\n\n await this.repo.createSession(sessionRowFromSnapshot(snapshot, this.resolveWorkspaceKey()))\n\n // RFC-159 D1(append-forever):内存快照短于 DB 是**合法常态**(capStoredHistory 只\n // 塑造内存视图,DB 全量保留),绝不触发删除。此前这里的 `entries.length < dbCount →\n // deleteAllEntries(全删+next_seq归零+全量重写)` 分支是 2026-07-13 生产数据丢失事故\n // (会话 dd6d6e73 丢 305 条)的根因 R1:与陈旧内存写者(/exit -r 多进程竞态)叠加时,\n // DB 中较新的尾部消息被旧快照整体抹掉。任何\"收缩 DB\"的新需求必须回 RFC-159 重新决策。\n //\n // RFC-159 D2(entry_id 游标):从游标 entry_id 在内存数组中的位置之后追加。定位失败\n // (首次 save / 游标条目已被内存裁掉 / clear·fork 重建)→ 全量幂等 append 兜底。\n // RFC-159 D6(调和纵深):无游标时若 DB 已有条目,说明存在其他写者痕迹——只追加自己\n // 的新 entry_id(appendEntry 幂等去重),绝不删除、绝不覆盖,记警告可观测。\n const cursor = this.persisted.get(snapshot.id)\n let already = 0\n if (cursor != null) {\n const idx = snapshot.entries.findIndex((e) => e.id === cursor)\n already = idx >= 0 ? idx + 1 : 0\n }\n if (already === 0) {\n const dbCount = await this.repo.getEntryCount(snapshot.id)\n if (dbCount > 0 && cursor == null) {\n this.logger.warn(\n { sessionId: snapshot.id, dbCount },\n 'Session has persisted entries but no local cursor (restart or concurrent writer); ' +\n 'reconciling via full idempotent append (append-forever, no loss)',\n )\n }\n }\n\n for (let i = already; i < snapshot.entries.length; i++) {\n const input = entryToInput(snapshot.entries[i]!)\n // RFC-160 D7:新写入 compaction 的 replacement 转内容寻址 blob(hashes 进 data,\n // 消息本体去重落 message_blobs)。非 compaction/已是新格式 → 原样直通。\n const data = await encodeReplacementToBlobs(\n input.type,\n input.data as Record<string, unknown>,\n this.repo,\n )\n await this.repo.appendEntry(snapshot.id, { ...input, data })\n }\n const { leafPersisted } = await this.repo.touchSession(\n snapshot.id,\n snapshot.leafId,\n snapshot.metadata.lastActiveAt,\n snapshot.metadata.title,\n snapshot.metadata.titleSource,\n // 修复:createSession 是 ON CONFLICT DO NOTHING 幂等写,只在会话首次创建时落 config\n // 一次;此前 touchSession 不透传 config,导致 write_todos/editedFiles/subagents 等\n // 回合内更新的会话状态在首次创建之后永远无法持久化(resume 读到冻结在首次快照的旧值)。\n // config 同 title 一样每次 save 透传,仓储层用 COALESCE 保底(缺省不抹既有值)。\n snapshot.metadata.config,\n // archived/pinned/pinGroup 同理:每次 save 透传当前值,/archive /pin 命令改动才能落盘。\n snapshot.metadata.archived,\n snapshot.metadata.pinned,\n snapshot.metadata.pinGroup,\n )\n // P0 事故修复:leaf_id 悬空拒写后可观测——不静默,让并发写竞态可被日志追踪定位。\n if (!leafPersisted && snapshot.leafId != null) {\n this.logger.warn(\n { sessionId: snapshot.id, rejectedLeafId: snapshot.leafId },\n 'touchSession: leafId not found among session entries (rejected to avoid dangling ' +\n 'leaf; likely concurrent writer race); existing leaf_id preserved',\n )\n }\n const lastEntry = snapshot.entries[snapshot.entries.length - 1]\n if (lastEntry) {\n this.persisted.set(snapshot.id, lastEntry.id)\n } else {\n this.persisted.delete(snapshot.id)\n }\n }\n\n /**\n * 修复(RFC-cross-workspace-restore-guard):跨 workspace 越界读取防护。\n * 此前本方法只按 `id` 查询,不校验会话真实归属的 `workspace_key` 是否与本\n * persistence 实例当前解析的 workspaceKey 一致——任何知道其他 workspace 会话\n * id 的调用方(如 `--continue <id>` / `sessionManager.restore(id)` 兜底路径)都能\n * 绕过 `/resume` 列表层面的 workspace 分区隔离,跨项目拉起并非本 workspace 的会话。\n * `/resume` 正常路径本身安全(先经 `listAllSessions` 过滤,找不到不会走到这里),\n * 但 `load()` 作为持久层唯一入口理应自身闭环——不能把隔离职责完全甩给每个调用方。\n * 归属不一致时 fail-closed 返回 null(与\"会话不存在\"同语义,不泄漏其存在性),并记警告日志。\n */\n async load(id: string): Promise<SessionSnapshot<T> | null> {\n const row = await this.repo.getSession(id)\n if (!row) return null\n const expectedKey = this.resolveWorkspaceKey()\n const actualKey = row.workspaceKey || 'default'\n if (actualKey !== expectedKey) {\n this.logger.warn(\n { sessionId: id, sessionWorkspaceKey: actualKey, expectedWorkspaceKey: expectedKey },\n 'RelationalSessionPersistence.load: session belongs to a different workspace, refusing cross-workspace restore',\n )\n return null\n }\n const snap = await reassembleSnapshot<T>(id, this.repo, {\n ...(this.maxLoadEntries != null ? { maxEntries: this.maxLoadEntries } : {}),\n ...(this.canStartHistory ? { canStartHistory: this.canStartHistory } : {}),\n ...(this.stripInactiveReplacements ? { stripInactiveReplacements: true } : {}),\n })\n // RFC-159 D2:load 后游标置为加载集尾 entry_id(尾窗口加载后即窗口尾)。\n const lastEntry = snap?.entries[snap.entries.length - 1]\n if (lastEntry) this.persisted.set(id, lastEntry.id)\n return snap\n }\n\n /**\n * RFC-160 D2a:暴露 entry_id 持久化游标(只读),供 runtime 构造运行时剥离资格谓词\n * ——\"前条 compaction 已持久化\"判定查此游标而非 DB(save 是异步 fire-and-forget,\n * 查 DB 有假阴性;游标只在 save 链成功后推进,是\"已交给持久层\"的可靠信号)。\n */\n getPersistedCursor(sessionId: string): string | undefined {\n return this.persisted.get(sessionId)\n }\n\n /**\n * RFC-160 D3:剥离态 hydrate 能力(ReplacementHydrationCapable)。runtime 三出口\n * (checkpoint 捕获前/fork 前/restore 后)经能力探测调用,点查回填 replacement 真身。\n */\n async hydrateReplacements(\n sessionId: string,\n entries: readonly SessionEntry<T>[],\n ): Promise<SessionEntry<T>[]> {\n return hydrateStrippedReplacements(sessionId, entries, this.repo)\n }\n\n async delete(id: string): Promise<boolean> {\n const removed = await this.repo.deleteSession(id)\n this.persisted.delete(id)\n this.writeLease?.release(id) // 会话已删,锁文件一并清理(D3 生命周期绑定)\n return removed\n }\n\n async list(): Promise<string[]> {\n return (await this.repo.listSessions(this.resolveWorkspaceKey())).map((s) => s.id)\n }\n\n /** RFC-154 D1:委托仓储层 SQL 排序+归档过滤+LIMIT,零额外内存开销(不拉 entries)。 */\n async listRecentMeta(limit: number): Promise<string[]> {\n return this.repo.listRecentSessionIds(this.resolveWorkspaceKey(), limit)\n }\n\n async listPaginated(options: PaginationOptions): Promise<PaginatedResult<string>> {\n const rows = await this.repo.listSessions(this.resolveWorkspaceKey())\n const ordered =\n options.sortBy === 'createdAt' ? [...rows].sort((a, b) => b.createdAt - a.createdAt) : rows\n const items0 = options.order === 'asc' ? [...ordered].reverse() : ordered\n const pageSize = options.pageSize ?? this.defaultPageSize\n const total = items0.length\n const totalPages = Math.max(1, Math.ceil(total / pageSize))\n const page = Math.max(0, Math.min(options.page, totalPages - 1))\n const slice = items0.slice(page * pageSize, page * pageSize + pageSize)\n return {\n items: slice.map((s) => s.id),\n total,\n page,\n pageSize,\n totalPages,\n hasNext: page < totalPages - 1,\n hasPrevious: page > 0,\n }\n }\n}\n","import { createGzip, gunzipSync } from 'node:zlib'\nimport { createWriteStream, existsSync, mkdirSync } from 'node:fs'\nimport { readFile } from 'node:fs/promises'\nimport { resolve, dirname } from 'node:path'\nimport { acquireDb, releaseDb } from '@x-otto/persistence'\nimport type { Migration } from '@x-otto/persistence'\n\nimport type {\n EntryInput,\n EntryRow,\n SessionRepository,\n SessionRow,\n WorkspaceRow,\n} from './session-repository'\nimport type Database from 'better-sqlite3'\n\n/**\n * 关系化会话 schema 段(workspaces + sessions + session_entries)。\n * RFC-074 M5-02b:随 SessionRepository 从 @x-otto/persistence 迁回本包——会话 schema 与领域同归属。\n * 经 `acquireDb(dbPath, wal, SESSION_MIGRATIONS)` 注入给通用 sqlite 连接基建执行。会话表间无外键,\n * 且会话 db(sessions.db)独立于 blob db(storage.db),故只需本段、无需与 blob 段 compose。\n */\nexport const SESSION_MIGRATIONS: Migration[] = [\n {\n version: 1,\n up: (db) => {\n db.exec(`\n CREATE TABLE IF NOT EXISTS workspaces (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n owner_id TEXT,\n org_id TEXT,\n created_at INTEGER NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n workspace_key TEXT NOT NULL DEFAULT 'default',\n title TEXT,\n parent_session_id TEXT,\n fork_point INTEGER,\n leaf_id TEXT,\n owner_id TEXT,\n tags TEXT NOT NULL DEFAULT '[]',\n config TEXT,\n next_seq INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL,\n last_active_at INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS idx_sessions_ws\n ON sessions(workspace_key, last_active_at DESC);\n\n CREATE TABLE IF NOT EXISTS session_entries (\n session_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n entry_id TEXT NOT NULL,\n parent_id TEXT,\n type TEXT NOT NULL,\n data TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n PRIMARY KEY (session_id, seq)\n );\n CREATE UNIQUE INDEX IF NOT EXISTS idx_entries_eid\n ON session_entries(session_id, entry_id);\n `)\n },\n },\n {\n // RFC-078 M132-02:标题来源等级列。迁移版本门控保证只跑一次(老库无列不崩)。\n version: 2,\n up: (db) => {\n db.exec(`ALTER TABLE sessions ADD COLUMN title_source TEXT;`)\n },\n },\n {\n // /archive /pin 持久化镜像列。SQLite 无原生 boolean,用 INTEGER 0/1(COALESCE 友好,\n // 缺省 0 与 SessionMetadata.archived/pinned 的 additive optional 语义一致:老行读出\n // NULL→undefined→按 false 处理,不影响历史数据)。\n version: 3,\n up: (db) => {\n db.exec(`\n ALTER TABLE sessions ADD COLUMN archived INTEGER;\n ALTER TABLE sessions ADD COLUMN pinned INTEGER;\n `)\n },\n },\n {\n // RFC-158:/pin --group 置顶分组名列。additive optional(老行 NULL→undefined→默认组)。\n version: 4,\n up: (db) => {\n db.exec(`ALTER TABLE sessions ADD COLUMN pin_group TEXT;`)\n },\n },\n {\n // RFC-160 M4(D7):内容寻址 blob 表——compaction replacement 消息按 sha256 去重存储。\n // 新写入 compaction 的 data 存 replacementHashes: string[],读取按 hash JOIN 组装。\n // 实测去重潜力:全库 replacement 内嵌消息 151,203 条仅 7% distinct(228.9MB→16.7MB)。\n // 孤儿 blob GC 显式推迟(去重后增量极小,观察项)。\n version: 5,\n up: (db) => {\n db.exec(`\n CREATE TABLE IF NOT EXISTS message_blobs (\n hash TEXT PRIMARY KEY,\n content TEXT NOT NULL,\n created_at INTEGER NOT NULL\n );\n `)\n },\n },\n]\n\nexport interface SqliteSessionRepositoryOptions {\n dbPath: string\n wal?: boolean\n}\n\nconst DEFAULT_WS = 'default'\n\ninterface SessionDbRow {\n id: string\n workspace_key: string\n title: string | null\n title_source: string | null\n parent_session_id: string | null\n fork_point: number | null\n leaf_id: string | null\n owner_id: string | null\n tags: string\n config: string | null\n next_seq: number\n created_at: number\n last_active_at: number\n archived: number | null\n pinned: number | null\n pin_group: string | null\n}\n\ninterface EntryDbRow {\n session_id: string\n seq: number\n entry_id: string\n parent_id: string | null\n type: string\n data: string\n created_at: number\n}\n\n/**\n * SQLite 实现(RFC-037 阶段A)。schema = workspaces / sessions / session_entries(SESSION_MIGRATIONS)。\n * 通过 @x-otto/persistence 的通用连接基建 `acquireDb`/`releaseDb` 打开 sessions.db,DDL 由注入的\n * SESSION_MIGRATIONS 经 applyMigrations 统一管理;本类不自建表。\n */\nexport class SqliteSessionRepository implements SessionRepository {\n private readonly db: Database.Database\n private readonly archiveDir: string\n /** RFC-345:内存会话(dbPath=':memory:')——归档不落盘(磁盘零写;:memory: 无跨重启需求)。 */\n private readonly ephemeral: boolean\n\n constructor(options: SqliteSessionRepositoryOptions) {\n this.db = acquireDb(options.dbPath, options.wal !== false, SESSION_MIGRATIONS)\n // RFC-324 D4 M4:归档目录——与 dbPath 同级的 archives/ 子目录,\n // 每会话一个 .jsonl.gz(JSON Lines + gzip 压缩),条目按 seq 排序逐行写入。\n this.ephemeral = options.dbPath === ':memory:'\n // 注意::memory: 时 dirname 为 '.',archiveDir 会落 cwd——但 ephemeral 分支永不写盘,\n // 该路径仅作占位不被使用(archiveSession 提前返回)。\n this.archiveDir = resolve(dirname(options.dbPath), 'archives')\n }\n\n close(): void {\n releaseDb(this.db)\n }\n\n /** RFC-324 D4 M4:归档——把会话的全部 entries 读出,逐行写入 .jsonl.gz(gzip 压缩)。\n * 不删除 entries 行(RFC-159 R4 红线),只标记 archived=1 + 复制到外部文件。\n * 返回值:归档文件路径。 */\n async archiveSession(sessionId: string): Promise<string> {\n // RFC-345 §D8:内存会话只标记 archived=1(DB 内存态),不写归档文件——磁盘零写。\n // :memory: 会话本就不跨重启,归档到磁盘无意义且会污染 cwd(dirname(':memory:')='.')。\n if (this.ephemeral) {\n this.db.prepare(`UPDATE sessions SET archived = 1 WHERE id = ?`).run(sessionId)\n return `:memory:/${sessionId}.jsonl.gz`\n }\n const archivePath = resolve(this.archiveDir, `${sessionId}.jsonl.gz`)\n if (!existsSync(this.archiveDir)) mkdirSync(this.archiveDir, { recursive: true })\n const entries = await this.loadEntries(sessionId)\n const gzip = createGzip()\n const writer = createWriteStream(archivePath)\n gzip.pipe(writer)\n const encoder = new TextEncoder()\n for (const e of entries) {\n gzip.write(encoder.encode(JSON.stringify(e) + '\\n'))\n }\n gzip.end()\n await new Promise<void>((res, rej) => { writer.on('finish', res); writer.on('error', rej) })\n // 标记 archived=1(entries 保留在 DB——R4 红线不删除行)。\n this.db.prepare(`UPDATE sessions SET archived = 1 WHERE id = ?`).run(sessionId)\n return archivePath\n }\n\n /** RFC-324 D4 M4:恢复——从 .jsonl.gz 读回并重建 entries(RFC-159 append-forever)\n * 已有的 entries 不删(幂等 append 可能产生重复,appendEntry 幂等去重可容忍),\n * 仅当 DB 中的条目数 < 归档条目数时追加缺失部分。 */\n async restoreArchivedSession(sessionId: string): Promise<number> {\n const archivePath = resolve(this.archiveDir, `${sessionId}.jsonl.gz`)\n if (!existsSync(archivePath)) return 0 // 无归档文件\n const content = await readFile(archivePath)\n const decompressed = gunzipSync(content)\n const lines = decompressed.toString('utf-8').trim().split('\\n').filter(Boolean)\n const existing = await this.loadEntries(sessionId)\n let restored = 0\n for (const line of lines) {\n const entry = JSON.parse(line) as EntryRow\n // 幂等检查:已存在同 entry_id 则跳过\n if (existing.some(e => e.entryId === entry.entryId)) continue\n await this.appendEntry(sessionId, entry)\n restored++\n }\n // 清除 archived 标记\n this.db.prepare(`UPDATE sessions SET archived = 0 WHERE id = ?`).run(sessionId)\n return restored\n }\n\n async upsertWorkspace(ws: WorkspaceRow): Promise<void> {\n this.db\n .prepare(\n `INSERT INTO workspaces (id, name, owner_id, org_id, created_at)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n name = excluded.name, owner_id = excluded.owner_id, org_id = excluded.org_id`,\n )\n .run(ws.id, ws.name, ws.ownerId ?? null, ws.orgId ?? null, ws.createdAt)\n }\n\n async getWorkspace(id: string): Promise<WorkspaceRow | null> {\n const row = this.db\n .prepare(`SELECT id, name, owner_id, org_id, created_at FROM workspaces WHERE id = ?`)\n .get(id) as\n | {\n id: string\n name: string\n owner_id: string | null\n org_id: string | null\n created_at: number\n }\n | undefined\n if (!row) return null\n return decodeWorkspace(row)\n }\n\n async listWorkspaces(): Promise<WorkspaceRow[]> {\n const rows = this.db\n .prepare(\n `SELECT id, name, owner_id, org_id, created_at FROM workspaces ORDER BY created_at DESC`,\n )\n .all() as {\n id: string\n name: string\n owner_id: string | null\n org_id: string | null\n created_at: number\n }[]\n return rows.map(decodeWorkspace)\n }\n\n async createSession(row: SessionRow): Promise<void> {\n this.db\n .prepare(\n // createSession 保持幂等(ON CONFLICT DO NOTHING);标题后续变更走 touchSession\n // (title/title_source 与 leaf_id 同属\"少数可变字段\",每次 save 更新,RFC-078 M132-02)。\n `INSERT INTO sessions\n (id, workspace_key, title, title_source, parent_session_id, fork_point, leaf_id, owner_id, tags, config, next_seq, created_at, last_active_at, archived, pinned, pin_group)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO NOTHING`,\n )\n .run(\n row.id,\n row.workspaceKey || DEFAULT_WS,\n row.title ?? null,\n row.titleSource ?? null,\n row.parentSessionId ?? null,\n row.forkPoint ?? null,\n row.leafId ?? null,\n row.ownerId ?? null,\n JSON.stringify(row.tags ?? []),\n row.config != null ? JSON.stringify(row.config) : null,\n row.createdAt,\n row.lastActiveAt,\n row.archived != null ? (row.archived ? 1 : 0) : null,\n row.pinned != null ? (row.pinned ? 1 : 0) : null,\n row.pinGroup ?? null,\n )\n }\n\n async touchSession(\n id: string,\n leafId: string | undefined,\n lastActiveAt: number,\n title?: string,\n titleSource?: 'custom' | 'ai' | 'derived',\n config?: unknown,\n archived?: boolean,\n pinned?: boolean,\n pinGroup?: string,\n ): Promise<{ leafPersisted: boolean }> {\n // title/title_source/config 用 COALESCE:缺省(绑 null)保留既有值,不抹掉\n // (RFC-078 M132-02 起 title 已如此;config 此前遗漏——createSession 是 ON CONFLICT DO\n // NOTHING 幂等写,只在会话首次创建时落 config 一次,此后每次 save 都只经 touchSession\n // 更新 leaf/title,config 从未再被更新,导致 write_todos/editedFiles/subagents 等回合内\n // 状态变更全部无法持久化)。archived/pinned 同理用 COALESCE:调用方每次 save 都透传\n // 当前值(非 undefined),故这里的 COALESCE 主要是防御未来遗漏调用点的兜底,不依赖它。\n //\n // **leaf_id 悬空防护(P0 事故修复,2026-07-12)**:此前 leaf_id 无条件覆写——多进程/多\n // 会话实例并发写同一 session id 时(如 RFC-115 重启陪跑窗口、或误同时 --continue 同一\n // 会话),一个写者的破坏性重写(历史上 RelationalSessionPersistence.save 检测到本地\n // 快照变短时触发 deleteAllEntries——该分支已被 RFC-159 D1 废除,save 路径不再删行)\n // 可能插进另一写者「已 appendEntry 完、还没 touchSession」的窗口之间,\n // 使后者把 leaf_id 钉在一个刚被删除、此后再未重建的 entry_id 上。悬空 leaf_id 会让\n // resume 时 InMemorySession.restoreEntries 静默 fallback 到\"数组最后一条\",若该条恰好是\n // `clear` 边界,则 visibleMessages() 直接返回空——整段转录对用户不可见(本机生产数据\n // 实锤过一例)。修复:在同一原子事务内先校验 leafId 是否真实存在于本会话的\n // session_entries,只有存在才写入;不存在则保留旧值(不覆盖,等价 COALESCE 语义)并向\n // 调用方报告 `leafPersisted:false`,供上层记录告警(可观测,不再静默腐化数据)。\n const tx = this.db.transaction((): boolean => {\n let leafToWrite: string | null = null\n let leafPersisted = true\n if (leafId != null) {\n const exists = this.db\n .prepare(`SELECT 1 FROM session_entries WHERE session_id = ? AND entry_id = ?`)\n .get(id, leafId)\n if (exists) {\n leafToWrite = leafId\n } else {\n leafPersisted = false\n }\n }\n\n this.db\n .prepare(\n leafPersisted\n ? `UPDATE sessions SET\n leaf_id = ?,\n last_active_at = ?,\n title = COALESCE(?, title),\n title_source = COALESCE(?, title_source),\n config = COALESCE(?, config),\n archived = COALESCE(?, archived),\n pinned = COALESCE(?, pinned),\n pin_group = COALESCE(?, pin_group)\n WHERE id = ?`\n : `UPDATE sessions SET\n last_active_at = ?,\n title = COALESCE(?, title),\n title_source = COALESCE(?, title_source),\n config = COALESCE(?, config),\n archived = COALESCE(?, archived),\n pinned = COALESCE(?, pinned),\n pin_group = COALESCE(?, pin_group)\n WHERE id = ?`,\n )\n .run(\n ...(leafPersisted ? [leafToWrite] : []),\n lastActiveAt,\n title ?? null,\n titleSource ?? null,\n config != null ? JSON.stringify(config) : null,\n archived != null ? (archived ? 1 : 0) : null,\n pinned != null ? (pinned ? 1 : 0) : null,\n pinGroup ?? null,\n id,\n )\n return leafPersisted\n })\n return { leafPersisted: tx.immediate() }\n }\n\n async getSession(id: string): Promise<SessionRow | null> {\n const row = this.db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(id) as\n | SessionDbRow\n | undefined\n if (!row) return null\n return decodeSession(row)\n }\n\n async listSessions(workspaceKey: string): Promise<SessionRow[]> {\n const rows = this.db\n .prepare(\n `SELECT * FROM sessions\n WHERE COALESCE(workspace_key, '${DEFAULT_WS}') = ?\n ORDER BY last_active_at DESC`,\n )\n .all(workspaceKey || DEFAULT_WS) as SessionDbRow[]\n return rows.map(decodeSession)\n }\n\n async listAllSessionIds(): Promise<string[]> {\n const rows = this.db.prepare(`SELECT id FROM sessions`).all() as { id: string }[]\n return rows.map((r) => r.id)\n }\n\n /**\n * 批量恢复候选(`restoreAll` 专用)。三重过滤:workspace 分区 + 未归档 + **非真空**。\n *\n * RFC-336 D1-a:`next_seq > 0` 是 D1(会话行预建)的**前置条件**,不是可选优化。\n * D1 让\"会话创建即落 sessions 行\"成为常态,于是从未 prompt 过的真空会话也会留行,\n * 且其 `last_active_at` 恰是创建时间(很新)→ 在 `ORDER BY last_active_at DESC` 下\n * 排在最前,会按 `room` 名额优先占位,把真正有内容的会话挤出恢复窗口。\n * 那不是\"用户看到垃圾\"级的 UX 问题,而是**池容量被空壳耗尽导致真实会话无法恢复**的\n * 功能性回归。故 D1 与本过滤必须同一里程碑落地(RFC-336 重要事项规则 8)。\n *\n * `next_seq` 与 `getEntryCount()`/`SessionRow.entryCount` 同源(RFC-159 D5 单调不回退),\n * 已是 sessions 表列,过滤零额外查询代价、不触 session_entries。\n *\n * **不变量**:本过滤只作用于**批量**恢复候选。`restore(id)` 按需单条恢复不得加此过滤——\n * 用户显式给出 id 时应能恢复空会话(同 `/resume` 对 arg 场景的既有豁免语义)。\n */\n async listRecentSessionIds(workspaceKey: string, limit: number): Promise<string[]> {\n const rows = this.db\n .prepare(\n `SELECT id FROM sessions\n WHERE COALESCE(workspace_key, '${DEFAULT_WS}') = ?\n AND COALESCE(archived, 0) = 0\n AND next_seq > 0\n ORDER BY last_active_at DESC\n LIMIT ?`,\n )\n .all(workspaceKey || DEFAULT_WS, limit) as { id: string }[]\n return rows.map((r) => r.id)\n }\n\n async deleteSession(id: string): Promise<boolean> {\n const tx = this.db.transaction((sessionId: string) => {\n this.db.prepare(`DELETE FROM session_entries WHERE session_id = ?`).run(sessionId)\n return this.db.prepare(`DELETE FROM sessions WHERE id = ?`).run(sessionId).changes > 0\n })\n return tx.immediate(id)\n }\n\n async appendEntry(sessionId: string, entry: EntryInput): Promise<number> {\n const tx = this.db.transaction((sid: string, e: EntryInput): number => {\n const existing = this.db\n .prepare(`SELECT seq FROM session_entries WHERE session_id = ? AND entry_id = ?`)\n .get(sid, e.entryId) as { seq: number } | undefined\n if (existing) return existing.seq\n\n const cur = this.db.prepare(`SELECT next_seq FROM sessions WHERE id = ?`).get(sid) as\n | { next_seq: number }\n | undefined\n if (!cur) throw new Error(`appendEntry: unknown session ${sid}`)\n const seq = cur.next_seq\n\n // RFC-160 D1 防御纵深:entry_id 首次落行时(幂等去重之后走到这里 = 必是首行)\n // data 不得含剥离态标记——首行即权威正文,剥离态成为正文 = replacement 永久丢失。\n // 已知危险出口(fork/checkpoint)另有 D4 断言,此为存储层最后防线。\n if ((e.data as Record<string, unknown>)['replacementStripped']) {\n throw new Error(\n `appendEntry: refusing to persist stripped compaction entry ${e.entryId} as a new ` +\n `authoritative row for session ${sid} (RFC-160 D1; hydrate before persisting)`,\n )\n }\n\n // RFC-159 D5 运行期不变量:next_seq 单调、永不回退。若取号结果 ≤ 既有 MAX(seq)\n // (只能来自绕过应用层的直接 SQL 操作/迁移脚本),fail-safe 拒写——绝不以覆盖或\n // PK 冲突崩溃的方式继续。MAX(seq) 走 (session_id, seq) 主键索引,O(log n)。\n const maxRow = this.db\n .prepare(`SELECT MAX(seq) AS max_seq FROM session_entries WHERE session_id = ?`)\n .get(sid) as { max_seq: number | null }\n if (maxRow.max_seq != null && seq <= maxRow.max_seq) {\n throw new Error(\n `appendEntry: next_seq invariant violated for session ${sid} ` +\n `(next_seq=${seq} <= MAX(seq)=${maxRow.max_seq}); refusing to write (RFC-159 D5)`,\n )\n }\n\n this.db\n .prepare(\n `INSERT INTO session_entries (session_id, seq, entry_id, parent_id, type, data, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(sid, seq, e.entryId, e.parentId ?? null, e.type, JSON.stringify(e.data), e.createdAt)\n this.db\n .prepare(`UPDATE sessions SET next_seq = ?, last_active_at = ? WHERE id = ?`)\n .run(seq + 1, e.createdAt, sid)\n return seq\n })\n return tx.immediate(sessionId, entry)\n }\n\n async loadEntries(sessionId: string, sinceSeq?: number): Promise<EntryRow[]> {\n const rows = (\n sinceSeq == null\n ? this.db\n .prepare(`SELECT * FROM session_entries WHERE session_id = ? ORDER BY seq ASC`)\n .all(sessionId)\n : this.db\n .prepare(\n `SELECT * FROM session_entries WHERE session_id = ? AND seq > ? ORDER BY seq ASC`,\n )\n .all(sessionId, sinceSeq)\n ) as EntryDbRow[]\n return rows.map(decodeEntry)\n }\n\n async getEntryCount(sessionId: string): Promise<number> {\n const row = this.db.prepare(`SELECT next_seq FROM sessions WHERE id = ?`).get(sessionId) as\n | { next_seq: number }\n | undefined\n return row?.next_seq ?? 0\n }\n\n async loadTailEntries(sessionId: string, count: number): Promise<EntryRow[]> {\n if (count <= 0) return []\n // 尾窗口:seq 倒序取 count 条 → 正序返回。走 (session_id, seq) 主键索引,O(count)。\n const rows = this.db\n .prepare(\n `SELECT * FROM (\n SELECT * FROM session_entries WHERE session_id = ? ORDER BY seq DESC LIMIT ?\n ) ORDER BY seq ASC`,\n )\n .all(sessionId, count) as EntryDbRow[]\n return rows.map(decodeEntry)\n }\n\n async loadEntriesBefore(sessionId: string, beforeSeq: number, limit: number): Promise<EntryRow[]> {\n if (beforeSeq <= 0 || limit <= 0) return []\n // RFC-300 M2 反向分页:seq < beforeSeq 倒序取 limit 条 → 正序返回(补页窗口)。\n // 走 (session_id, seq) 主键索引,O(limit)。\n const rows = this.db\n .prepare(\n `SELECT * FROM (\n SELECT * FROM session_entries\n WHERE session_id = ? AND seq < ?\n ORDER BY seq DESC LIMIT ?\n ) ORDER BY seq ASC`,\n )\n .all(sessionId, beforeSeq, limit) as EntryDbRow[]\n return rows.map(decodeEntry)\n }\n\n async countEntriesByType(sessionId: string, type: string): Promise<number> {\n const row = this.db\n .prepare(`SELECT COUNT(*) AS n FROM session_entries WHERE session_id = ? AND type = ?`)\n .get(sessionId, type) as { n: number }\n return row.n\n }\n\n async putBlobs(blobs: ReadonlyArray<{ hash: string; content: string }>): Promise<void> {\n if (blobs.length === 0) return\n const stmt = this.db.prepare(\n `INSERT OR IGNORE INTO message_blobs (hash, content, created_at) VALUES (?, ?, ?)`,\n )\n const tx = this.db.transaction((items: ReadonlyArray<{ hash: string; content: string }>) => {\n const now = Date.now()\n for (const b of items) stmt.run(b.hash, b.content, now)\n })\n tx.immediate(blobs)\n }\n\n async getBlobs(hashes: readonly string[]): Promise<Map<string, string>> {\n const result = new Map<string, string>()\n for (const rows of this.batchInQuery<{ hash: string; content: string }>(\n hashes,\n (placeholders) => `SELECT hash, content FROM message_blobs WHERE hash IN (${placeholders})`,\n )) {\n for (const r of rows) result.set(r.hash, r.content)\n }\n return result\n }\n\n async loadEntriesByIds(sessionId: string, entryIds: readonly string[]): Promise<EntryRow[]> {\n // (session_id, entry_id) 唯一索引点查。\n const results: EntryDbRow[] = []\n for (const rows of this.batchInQuery<EntryDbRow>(\n entryIds,\n (placeholders) =>\n `SELECT * FROM session_entries WHERE session_id = ? AND entry_id IN (${placeholders})`,\n [sessionId],\n )) {\n results.push(...rows)\n }\n results.sort((a, b) => a.seq - b.seq)\n return results.map(decodeEntry)\n }\n\n /**\n * IN 子句分批查询(终局审查 2026-07-18 S14:getBlobs/loadEntriesByIds 的同构分批循环\n * 收束为单一实现)。按 500/批切分防 SQLite 绑定变量上限(999);`prefixParams` 是每批\n * 固定前置绑定参数(如 session_id),批内 IN 列表参数追加其后。\n */\n private *batchInQuery<Row>(\n ids: readonly string[],\n buildSql: (placeholders: string) => string,\n prefixParams: readonly unknown[] = [],\n ): Generator<Row[]> {\n if (ids.length === 0) return\n const BATCH = 500\n for (let i = 0; i < ids.length; i += BATCH) {\n const batch = ids.slice(i, i + BATCH)\n const placeholders = batch.map(() => '?').join(',')\n yield this.db.prepare(buildSql(placeholders)).all(...prefixParams, ...batch) as Row[]\n }\n }\n\n}\n\nfunction decodeWorkspace(r: {\n id: string\n name: string\n owner_id: string | null\n org_id: string | null\n created_at: number\n}): WorkspaceRow {\n return {\n id: r.id,\n name: r.name,\n ...(r.owner_id != null ? { ownerId: r.owner_id } : {}),\n ...(r.org_id != null ? { orgId: r.org_id } : {}),\n createdAt: r.created_at,\n }\n}\n\nfunction decodeSession(r: SessionDbRow): SessionRow {\n return {\n id: r.id,\n workspaceKey: r.workspace_key ?? DEFAULT_WS,\n ...(r.title != null ? { title: r.title } : {}),\n ...(r.title_source != null\n ? { titleSource: r.title_source as 'custom' | 'ai' | 'derived' }\n : {}),\n ...(r.parent_session_id != null ? { parentSessionId: r.parent_session_id } : {}),\n ...(r.fork_point != null ? { forkPoint: r.fork_point } : {}),\n ...(r.leaf_id != null ? { leafId: r.leaf_id } : {}),\n ...(r.owner_id != null ? { ownerId: r.owner_id } : {}),\n tags: JSON.parse(r.tags) as string[],\n ...(r.config != null ? { config: JSON.parse(r.config) as unknown } : {}),\n // entryCount:与 getEntryCount() 同源(sessions.next_seq,非 COUNT(session_entries)\n // ——next_seq 单调递增且已随每次 appendEntry 更新,见 RFC-159 D5 不变量注释),\n // listSessions() 批量枚举场景零额外查询代价即可带出真实条目数(供 /resume 列表\n // 过滤\"从未 prompt 过的空会话\"使用——不能复用 messageCount,那个字段在本地冷\n // 会话路径恒为 0,直接拿它判空会误伤全部历史会话)。\n entryCount: r.next_seq,\n createdAt: r.created_at,\n lastActiveAt: r.last_active_at,\n ...(r.archived != null ? { archived: r.archived === 1 } : {}),\n ...(r.pinned != null ? { pinned: r.pinned === 1 } : {}),\n ...(r.pin_group != null ? { pinGroup: r.pin_group } : {}),\n }\n}\n\nfunction decodeEntry(r: EntryDbRow): EntryRow {\n return {\n sessionId: r.session_id,\n seq: r.seq,\n entryId: r.entry_id,\n ...(r.parent_id != null ? { parentId: r.parent_id } : {}),\n type: r.type as EntryRow['type'],\n data: JSON.parse(r.data) as unknown,\n createdAt: r.created_at,\n }\n}\n","import { RemotePersistence, RemotePersistenceError, RemoteSnapshotConflictError } from '@x-otto/persistence'\nimport { SESSION_PAGE_SIZE } from './constants'\nimport type { SessionPersistence, SessionSnapshot } from './types'\n\nexport { RemoteSnapshotConflictError }\n\nexport interface RemoteSessionPersistenceOptions {\n baseUrl: string\n getAuth: () => Promise<{ token: string }>\n getHeaders?: () => Promise<Record<string, string>>\n fetch: typeof globalThis.fetch\n timeoutMs: number\n /** M71b: user identity id,非空时所有请求带 `X-Otto-User-Id` header。 */\n userId?: string\n /**\n * workspace 分区键(`ws_<id>`,缺省 'default')——修复(N3,resume/workspace 关联审计):\n * 此前 remote 后端从不透传 workspaceRef.key,所有远端会话请求恒落 'default' 分区,\n * 与本地 SQLite 后端(按 workspace_key 分区)行为不一致——多个本地 workspace 共享同一\n * `--session-url` 时,服务端的 `/:ns/:wsKey/:id` 分区能力被架空,会话互相可见/冲突。\n * 客户端侧仅做 URL 路径拼接(见 RemotePersistence.wsPath),真正的隔离仍需服务端\n * `workspaceAuth` 中间件 + token↔workspace scope 校验(M57,已就位)。\n * 支持惰性 getter(消费方在 App 构造期创建实例,workspaceRef 要到 start() 才解析完成,\n * 与 RelationalSessionPersistence.workspaceKey 同因,见 Bug1 修复记录)。\n */\n wsKey?: string | (() => string | undefined)\n}\n\nexport class RemoteSessionPersistence<T = unknown>\n extends RemotePersistence<SessionSnapshot<T>>\n implements SessionPersistence<T>\n{\n /**\n * RFC-305 M3:远端 save 是整份快照 PUT(无游标增量),每 turn 上传随会话线性变贵 →\n * 声明 'snapshot',PersistenceSync 据此对 turn 级落库节流(prompt.end 仍强制落库)。\n * 未来服务端提供 append-tail endpoint 后改此声明即可自动升级 every-turn。\n */\n readonly turnFlush = 'snapshot' as const\n\n constructor(options: RemoteSessionPersistenceOptions) {\n const { userId, wsKey, ...rest } = options\n const userProvidedHeaders = options.getHeaders\n\n super({\n ...rest,\n wsKey,\n getHeaders: userId\n ? async () => {\n const base = (await userProvidedHeaders?.().catch(() => ({}))) ?? {}\n return { ...base, 'X-Otto-User-Id': userId }\n }\n : userProvidedHeaders,\n defaultSortBy: 'lastActiveAt',\n defaultPageSize: SESSION_PAGE_SIZE,\n })\n }\n\n protected override get serviceLabel(): string {\n return '会话服务'\n }\n protected override get endpointFlag(): string {\n return '--session-url'\n }\n\n protected override extractId(snapshot: SessionSnapshot<T>): string {\n return snapshot.id\n }\n\n /**\n * RFC-154 D1:走服务端新增的专用端点 `GET /sessions/:wsKey/recent?limit=N`(不是修复既有\n * `listPaginated`——该方法在服务端返回 shape 与客户端类型契约不匹配,是独立 backlog,见\n * `packages/remote-persistence-server/src/routes/storage.ts` 头注)。服务端已保证排序\n * (lastActiveAt 降序)+ 归档过滤,客户端直接透传结果。\n */\n async listRecentMeta(limit: number): Promise<string[]> {\n const wsPath = `/${encodeURIComponent(this.wsKey)}/recent`\n const response = await this.request(`${wsPath}?limit=${encodeURIComponent(String(limit))}`, {\n method: 'GET',\n })\n const data = (await response.json()) as { ids: string[] }\n return data.ids\n }\n}\n\nexport { RemotePersistenceError }\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@x-otto/session",
3
+ "version": "0.0.1-alpha.0",
4
+ "files": [
5
+ "dist"
6
+ ],
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "registry": "https://registry.npmjs.org",
19
+ "tag": "alpha"
20
+ },
21
+ "dependencies": {
22
+ "@x-otto/env": "0.1.0-alpha.1",
23
+ "@x-otto/persistence": "0.0.1-alpha.0",
24
+ "better-sqlite3": "^12.9.0"
25
+ },
26
+ "private": false,
27
+ "scripts": {
28
+ "build": "tsdown",
29
+ "typecheck:project": "tsc -p tsconfig.json --noEmit",
30
+ "typecheck:file": "tsc src/index.ts --noEmit --target ES2024 --module ESNext --moduleResolution bundler --esModuleInterop --skipLibCheck",
31
+ "typecheck": "tsc --noEmit",
32
+ "clean": "rm -rf dist"
33
+ }
34
+ }