@morlay/session-rdb 0.0.16-alpha.0 → 0.0.16-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifact.d.mts +25 -1
- package/dist/artifact.mjs +3 -3
- package/dist/{import-CKrzjXVB.mjs → import-mZZgDBLd.mjs} +1 -1
- package/dist/import.d.mts +1 -1
- package/dist/import.mjs +1 -1
- package/dist/{index-CgGDHQSK.d.mts → index-GdKkSSb-.d.mts} +14 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +81 -11
- package/dist/{log-DTJsxMud.mjs → log-DBFUBPhv.mjs} +85 -5
- package/dist/{sqlite-DCy4VTD8.mjs → sqlite-CnWwGToZ.mjs} +1 -1
- package/dist/storage.mjs +1 -1
- package/package.json +2 -2
- package/src/index.ts +81 -24
- package/src/legacy.ts +44 -1
- package/src/log.ts +102 -4
package/dist/artifact.d.mts
CHANGED
|
@@ -45,8 +45,32 @@ declare function findSurfaceRepairs(events: readonly SessionEvent[]): {
|
|
|
45
45
|
degradeToAppend: Set<number>;
|
|
46
46
|
addAppendMarker: Set<number>;
|
|
47
47
|
clearSurfaceOp: Set<number>;
|
|
48
|
+
clampEnd: Map<number, number>;
|
|
48
49
|
};
|
|
49
50
|
declare function repairSurfaceOps(events: SessionEvent[]): void;
|
|
51
|
+
/**
|
|
52
|
+
* 读取时补全 assistant 结算字段:旧写入器不落库 `stream`(v2 才把流式记录
|
|
53
|
+
* 嵌入消息),读回时缺失即补空数组——上游 seed 校验要求 turn/step/stream
|
|
54
|
+
* 三者齐备,缺失会让整个会话加载失败。
|
|
55
|
+
*/
|
|
56
|
+
declare function repairAssistantSettlement(events: SessionEvent[]): void;
|
|
57
|
+
/**
|
|
58
|
+
* 把紧邻 replace 的 metering 事件的 `shadowedRange` / `shadowedSeqs` 对齐到
|
|
59
|
+
* replace 最终的稠密 range。
|
|
60
|
+
*
|
|
61
|
+
* 旧写入器重编号事件后,metering 与 replace 一起落在旧坐标空间:二者数值
|
|
62
|
+
* 相等但与当前 surface 无关。夹取(或 range 本身已落在稠密空间而 metering
|
|
63
|
+
* 仍是旧值)后二者不再相等,而上游 token-meter 契约要求紧邻的 metering
|
|
64
|
+
* range 与 replace range 完全一致(否则报 no adjacent shadow price)。range
|
|
65
|
+
* 已一致时不改写——当前写入器保证一致,shadowedSeqs 的额外并发节点因此
|
|
66
|
+
* 原样保留。
|
|
67
|
+
*/
|
|
68
|
+
declare function syncMeteringRanges(events: SessionEvent[]): void;
|
|
69
|
+
/**
|
|
70
|
+
* 读取视图修复总入口:结算字段补全 → surface 语义修复 → metering 对齐 →
|
|
71
|
+
* provenance 重算 → 孤儿 inbox splice 改写。全部只作用于内存视图,不落库。
|
|
72
|
+
*/
|
|
73
|
+
declare function repairReadView(events: SessionEvent[]): void;
|
|
50
74
|
/**
|
|
51
75
|
* 定位无法从空状态增量重放的 agent/inbox/spliced 事件(孤儿操作)。
|
|
52
76
|
*
|
|
@@ -64,4 +88,4 @@ declare function scanRows(rows: readonly EventRow[], base?: number): {
|
|
|
64
88
|
};
|
|
65
89
|
declare function toJsonlArtifact(meta: SessionHeader, inheritedEventCount: number, events: readonly SessionEvent[]): string;
|
|
66
90
|
//#endregion
|
|
67
|
-
export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, repairOrphanInboxSplices, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, toJsonlArtifact };
|
|
91
|
+
export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, repairAssistantSettlement, repairOrphanInboxSplices, repairReadView, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, syncMeteringRanges, toJsonlArtifact };
|
package/dist/artifact.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { a as persistImport, i as parseJsonlArtifact, n as SESSION_LOG_ARTIFACT_FILENAME, o as registerSessionImport, r as parseImportZip, t as SESSION_IMPORT_PATH } from "./import-
|
|
3
|
-
export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, repairOrphanInboxSplices, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, toJsonlArtifact };
|
|
1
|
+
import { a as repairOrphanInboxSplices, c as rowToEvent, d as sessionConflictRow, f as sessionInsertRow, i as repairAssistantSettlement, l as rowToMeta, m as toJsonlArtifact, n as orphanInboxSpliceSeqs, o as repairReadView, p as syncMeteringRanges, r as recomputeReplaceProvenance, s as repairSurfaceOps, t as findSurfaceRepairs, u as scanRows } from "./log-DBFUBPhv.mjs";
|
|
2
|
+
import { a as persistImport, i as parseJsonlArtifact, n as SESSION_LOG_ARTIFACT_FILENAME, o as registerSessionImport, r as parseImportZip, t as SESSION_IMPORT_PATH } from "./import-mZZgDBLd.mjs";
|
|
3
|
+
export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, repairAssistantSettlement, repairOrphanInboxSplices, repairReadView, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, syncMeteringRanges, toJsonlArtifact };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { l as rowToMeta } from "./log-DBFUBPhv.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
|
|
4
4
|
import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
package/dist/import.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as SessionPersistenceRdb } from "./index-
|
|
1
|
+
import { n as SessionPersistenceRdb } from "./index-GdKkSSb-.mjs";
|
|
2
2
|
import { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
|
|
3
3
|
import { Session, SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
|
|
4
4
|
import { Context } from "@deepseek-ai/cordis";
|
package/dist/import.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as persistImport, i as parseJsonlArtifact, n as SESSION_LOG_ARTIFACT_FILENAME, o as registerSessionImport, r as parseImportZip, t as SESSION_IMPORT_PATH } from "./import-
|
|
1
|
+
import { a as persistImport, i as parseJsonlArtifact, n as SESSION_LOG_ARTIFACT_FILENAME, o as registerSessionImport, r as parseImportZip, t as SESSION_IMPORT_PATH } from "./import-mZZgDBLd.mjs";
|
|
2
2
|
export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, parseImportZip, parseJsonlArtifact, persistImport, registerSessionImport };
|
|
@@ -196,7 +196,21 @@ declare class SessionPersistenceRdb extends SessionPersistence {
|
|
|
196
196
|
tornFrom?: number;
|
|
197
197
|
incarnation: string;
|
|
198
198
|
revision: number;
|
|
199
|
+
/** 存储桥接行数(迁移链可能生成/合并事件,与 `events.length` 不同)。 */
|
|
200
|
+
storedCount: number;
|
|
201
|
+
/** 是否经上游迁移链转换(v0/v1 → 当前格式)。 */
|
|
202
|
+
migrated: boolean;
|
|
199
203
|
} | undefined>;
|
|
204
|
+
/**
|
|
205
|
+
* 把迁移链读出的 v2 视图整体落库(旧格式会话写打开时的一次性迁移)。
|
|
206
|
+
*
|
|
207
|
+
* 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件 seq 空间
|
|
208
|
+
* 与存储桥接行数不再相等;写路径以存储 head 为锚点重编号,二者不一致会让
|
|
209
|
+
* append 撞上已有行。这里在同一事务内删光本会话桥接行、按迁移视图重建
|
|
210
|
+
* (新事件行,完整信封),并更新 head 与 revision;旧事件行保留(可能被
|
|
211
|
+
* fork 子会话引用,孤儿由惰性 GC 处理)。
|
|
212
|
+
*/
|
|
213
|
+
private rewriteMigratedLog;
|
|
200
214
|
listSnapshots(signal?: AbortSignal): Promise<Array<SessionPersistenceSnapshot & {
|
|
201
215
|
inheritedEventCount: number;
|
|
202
216
|
}>>;
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { o as SCHEMA_VERSION } from "./schema-COK7-wRV.mjs";
|
|
2
|
-
import { a as SessionBranchRdbProvider, i as SessionBranchRdb, n as SessionPersistenceRdb, o as locateTurnEnd, r as SessionPersistenceRdbInternals, t as Config } from "./index-
|
|
2
|
+
import { a as SessionBranchRdbProvider, i as SessionBranchRdb, n as SessionPersistenceRdb, o as locateTurnEnd, r as SessionPersistenceRdbInternals, t as Config } from "./index-GdKkSSb-.mjs";
|
|
3
3
|
export { Config, SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionPersistenceRdb, SessionPersistenceRdb as default, SessionPersistenceRdbInternals, locateTurnEnd };
|
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { a as SCHEMA_VERSION, g as WriteGuard, h as toPostgresSchema, i as EVENT_ENCODING, m as postgresTableDefs, r as DEFAULT_BUSY_TIMEOUT_MS, s as eventDimensions, t as SqliteBackend } from "./sqlite-
|
|
2
|
-
import {
|
|
3
|
-
import { c as SessionBranchRdbProvider, l as locateTurnEnd, o as registerSessionImport, s as SessionBranchRdb } from "./import-
|
|
1
|
+
import { a as SCHEMA_VERSION, g as WriteGuard, h as toPostgresSchema, i as EVENT_ENCODING, m as postgresTableDefs, r as DEFAULT_BUSY_TIMEOUT_MS, s as eventDimensions, t as SqliteBackend } from "./sqlite-CnWwGToZ.mjs";
|
|
2
|
+
import { d as sessionConflictRow, f as sessionInsertRow, l as rowToMeta, m as toJsonlArtifact, o as repairReadView, u as scanRows } from "./log-DBFUBPhv.mjs";
|
|
3
|
+
import { c as SessionBranchRdbProvider, l as locateTurnEnd, o as registerSessionImport, s as SessionBranchRdb } from "./import-mZZgDBLd.mjs";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { Pool } from "pg";
|
|
7
7
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
8
8
|
import { SessionAlreadyExistsError, SessionAlreadyOwnedError, SessionHandleClosedError, SessionPersistence, SessionPersistenceNotFoundError, SessionPersistenceRevision, SessionReadOnlyError, assertContiguous, assertVersion, materializeAppendBatch, materializeCreateHeader, validateStoredEvents } from "@deepseek-ai/dsh-session-persistence";
|
|
9
|
-
import { SessionLogOffset } from "@deepseek-ai/dsh-session";
|
|
9
|
+
import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
|
|
10
10
|
import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
11
11
|
import { and, eq, gte, sql } from "drizzle-orm";
|
|
12
12
|
import { readdirSync } from "node:fs";
|
|
@@ -209,6 +209,36 @@ function convertLegacyRows(row, eventRows) {
|
|
|
209
209
|
function isLegacyVersion(version) {
|
|
210
210
|
return version < 2;
|
|
211
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* 迁移链拒绝后,把旧格式行直接当**当前格式**事件视图采用。
|
|
214
|
+
*
|
|
215
|
+
* 旧写入器只在建会话时落一次 header version,之后跟随上游演进继续追加
|
|
216
|
+
* 事件——同一个 log 因此可能是混合世代(v0 时代的行 + 新版本字段),不是
|
|
217
|
+
* 任何单一已发布格式,严格迁移链必然拒绝。这些行的数据形状(消息包装、
|
|
218
|
+
* 类型名)已经是当前格式,只需 header 版本归一到当前、按稠密 seq 读取;
|
|
219
|
+
* 事件数据的补全与 surface 修复由读取视图修复(repairReadView)负责。
|
|
220
|
+
*
|
|
221
|
+
* 调用方须在此之后执行 `validateStoredEvents`:真正属于旧格式的类型
|
|
222
|
+
* (`assistant/chunk` / `compact/*` 等)仍会被 fail-loud 拒绝。
|
|
223
|
+
*
|
|
224
|
+
* @param row - 会话行(f_version < 2)。
|
|
225
|
+
* @param eventRows - 该会话全部桥接行(按 f_sequence 升序)。
|
|
226
|
+
* @returns 当前格式 header(version 归一到 SESSION_FORMAT_VERSION)、收缩后的
|
|
227
|
+
* 继承前缀长度、稠密事件与可选 torn tail 起点。
|
|
228
|
+
* @throws scanRows 的已提交区损坏错误(seq gap / 不可解析行)。
|
|
229
|
+
*/
|
|
230
|
+
function adoptLegacyRows(row, eventRows) {
|
|
231
|
+
const { preserved, tornFrom } = scanRows(eventRows, 0);
|
|
232
|
+
return {
|
|
233
|
+
meta: {
|
|
234
|
+
...rowToMeta(row),
|
|
235
|
+
version: SESSION_FORMAT_VERSION
|
|
236
|
+
},
|
|
237
|
+
inheritedEventCount: Math.min(row.fSeedLength ?? 0, preserved.length),
|
|
238
|
+
events: preserved,
|
|
239
|
+
...tornFrom !== void 0 ? { tornFrom } : {}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
212
242
|
//#endregion
|
|
213
243
|
//#region src/index.ts
|
|
214
244
|
/** 会话级写所有权与 live 路由簿记。 */
|
|
@@ -352,9 +382,7 @@ var RdbSessionHandle = class RdbSessionHandle {
|
|
|
352
382
|
};
|
|
353
383
|
throw new SessionPersistenceNotFoundError(this.id);
|
|
354
384
|
}
|
|
355
|
-
|
|
356
|
-
recomputeReplaceProvenance(log.events);
|
|
357
|
-
repairOrphanInboxSplices(log.events);
|
|
385
|
+
repairReadView(log.events);
|
|
358
386
|
return {
|
|
359
387
|
eventState: "detached",
|
|
360
388
|
events: log.events.slice(offset, offset + length)
|
|
@@ -567,6 +595,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
567
595
|
const log = await this.readLog(id, {}, options?.signal);
|
|
568
596
|
if (log === void 0) throw new SessionPersistenceNotFoundError(id);
|
|
569
597
|
validateStoredEvents(log.meta, log.events);
|
|
598
|
+
if (log.migrated && log.events.length !== log.storedCount) await this.rewriteMigratedLog(id, log);
|
|
570
599
|
this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
|
|
571
600
|
return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "write", {
|
|
572
601
|
cursor: log.events.length,
|
|
@@ -630,8 +659,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
630
659
|
signal?.throwIfAborted();
|
|
631
660
|
const log = await this.readLog(id, {}, signal);
|
|
632
661
|
if (log === void 0) return void 0;
|
|
633
|
-
|
|
634
|
-
recomputeReplaceProvenance(log.events);
|
|
662
|
+
repairReadView(log.events);
|
|
635
663
|
const inheritedEventCount = Math.min(log.inheritedEventCount, log.events.length);
|
|
636
664
|
return {
|
|
637
665
|
meta: log.meta,
|
|
@@ -699,14 +727,29 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
699
727
|
const meta = rowToMeta(row);
|
|
700
728
|
const eventRows = options.fromSeq === void 0 ? await this.backend.getEventRows(id) : await this.backend.getEventRows(id, options.fromSeq);
|
|
701
729
|
signal?.throwIfAborted();
|
|
702
|
-
if (isLegacyVersion(row.fVersion)) {
|
|
730
|
+
if (isLegacyVersion(row.fVersion)) try {
|
|
703
731
|
const converted = convertLegacyRows(row, eventRows);
|
|
704
732
|
return {
|
|
705
733
|
meta: converted.meta,
|
|
706
734
|
inheritedEventCount: converted.inheritedEventCount,
|
|
707
735
|
events: converted.events,
|
|
708
736
|
incarnation: row.fIncarnation,
|
|
709
|
-
revision: row.fRevision
|
|
737
|
+
revision: row.fRevision,
|
|
738
|
+
storedCount: eventRows.length,
|
|
739
|
+
migrated: true
|
|
740
|
+
};
|
|
741
|
+
} catch (error) {
|
|
742
|
+
this.ctx.logger.warn(`session-rdb: session "${id}" is not a single released format; adopting its stored rows as current-format data (${error instanceof Error ? error.message : String(error)})`);
|
|
743
|
+
const adopted = adoptLegacyRows(row, eventRows);
|
|
744
|
+
return {
|
|
745
|
+
meta: adopted.meta,
|
|
746
|
+
inheritedEventCount: adopted.inheritedEventCount,
|
|
747
|
+
events: adopted.events,
|
|
748
|
+
incarnation: row.fIncarnation,
|
|
749
|
+
revision: row.fRevision,
|
|
750
|
+
storedCount: eventRows.length,
|
|
751
|
+
migrated: false,
|
|
752
|
+
...adopted.tornFrom !== void 0 ? { tornFrom: adopted.tornFrom } : {}
|
|
710
753
|
};
|
|
711
754
|
}
|
|
712
755
|
const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
|
|
@@ -716,9 +759,35 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
716
759
|
events: preserved,
|
|
717
760
|
incarnation: row.fIncarnation,
|
|
718
761
|
revision: row.fRevision,
|
|
762
|
+
storedCount: eventRows.length,
|
|
763
|
+
migrated: false,
|
|
719
764
|
...tornFrom !== void 0 ? { tornFrom } : {}
|
|
720
765
|
};
|
|
721
766
|
}
|
|
767
|
+
/**
|
|
768
|
+
* 把迁移链读出的 v2 视图整体落库(旧格式会话写打开时的一次性迁移)。
|
|
769
|
+
*
|
|
770
|
+
* 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件 seq 空间
|
|
771
|
+
* 与存储桥接行数不再相等;写路径以存储 head 为锚点重编号,二者不一致会让
|
|
772
|
+
* append 撞上已有行。这里在同一事务内删光本会话桥接行、按迁移视图重建
|
|
773
|
+
* (新事件行,完整信封),并更新 head 与 revision;旧事件行保留(可能被
|
|
774
|
+
* fork 子会话引用,孤儿由惰性 GC 处理)。
|
|
775
|
+
*/
|
|
776
|
+
async rewriteMigratedLog(id, log) {
|
|
777
|
+
await this.backend.transaction(async (tx) => {
|
|
778
|
+
await tx.deleteBridgeTail(id, 0);
|
|
779
|
+
await tx.upsertSession({
|
|
780
|
+
meta: log.meta,
|
|
781
|
+
inheritedEventCount: SessionLogOffset(log.inheritedEventCount)
|
|
782
|
+
}, randomUUID());
|
|
783
|
+
const { headEventId, headSequence } = await appendEventTail(tx, log.meta, log.events, {
|
|
784
|
+
parentId: "",
|
|
785
|
+
nextSeq: 0
|
|
786
|
+
});
|
|
787
|
+
await tx.updateHead(id, headEventId, headSequence);
|
|
788
|
+
await tx.bumpRevision(id);
|
|
789
|
+
});
|
|
790
|
+
}
|
|
722
791
|
async listSnapshots(signal) {
|
|
723
792
|
signal?.throwIfAborted();
|
|
724
793
|
await this.ready;
|
|
@@ -904,6 +973,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
904
973
|
if (stored.meta.cwd !== session.header.cwd) throw new Error(`session "${id}" is already persisted at a different cwd (persisted: ${String(stored.meta.cwd)}, live: ${String(session.header.cwd)}) (id collision)`);
|
|
905
974
|
if (stored.inheritedEventCount !== session.inheritedEventCount) throw new Error(`session "${id}" is already persisted with a different inherited event count (id collision)`);
|
|
906
975
|
assertVersion(stored.meta);
|
|
976
|
+
repairReadView(stored.events);
|
|
907
977
|
const seed = session.snapshotEvents();
|
|
908
978
|
if (!seedCoversPrefix(seed, stored.events)) throw new Error(`session "${id}" already has a persisted log on disk that does not match this live session (id collision)`);
|
|
909
979
|
handle = await this.open(id, "write");
|
|
@@ -142,7 +142,9 @@ function findSurfaceRepairs(events) {
|
|
|
142
142
|
const degradeToAppend = /* @__PURE__ */ new Set();
|
|
143
143
|
const addAppendMarker = /* @__PURE__ */ new Set();
|
|
144
144
|
const clearSurfaceOp = /* @__PURE__ */ new Set();
|
|
145
|
-
|
|
145
|
+
const clampEnd = /* @__PURE__ */ new Map();
|
|
146
|
+
for (let index = 0; index < events.length; index++) {
|
|
147
|
+
const event = events[index];
|
|
146
148
|
const op = event.surfaceOp;
|
|
147
149
|
if (op === void 0) {
|
|
148
150
|
if (SURFACE_EVENT_TYPES.has(event.type)) {
|
|
@@ -165,7 +167,12 @@ function findSurfaceRepairs(events) {
|
|
|
165
167
|
const end = replace?.["end"];
|
|
166
168
|
const shapeOk = replace !== void 0 && replace["op"] === "replace" && isEventSeqLike(start) && isEventSeqLike(end);
|
|
167
169
|
const startIdx = shapeOk ? nodes.indexOf(start) : -1;
|
|
168
|
-
|
|
170
|
+
let endIdx = shapeOk ? nodes.indexOf(end) : -1;
|
|
171
|
+
let clamped;
|
|
172
|
+
if (shapeOk && startIdx !== -1 && endIdx === -1) {
|
|
173
|
+
clamped = clampReplaceEnd(events, index, nodes, startIdx);
|
|
174
|
+
if (clamped !== void 0) endIdx = nodes.indexOf(clamped);
|
|
175
|
+
}
|
|
169
176
|
const rangeOk = shapeOk && startIdx !== -1 && endIdx !== -1 && startIdx <= endIdx;
|
|
170
177
|
let rewriteOk = true;
|
|
171
178
|
if (rangeOk && event.type === "tool/result") {
|
|
@@ -181,25 +188,98 @@ function findSurfaceRepairs(events) {
|
|
|
181
188
|
nodes.push(event.seq);
|
|
182
189
|
continue;
|
|
183
190
|
}
|
|
191
|
+
if (clamped !== void 0) clampEnd.set(event.seq, clamped);
|
|
184
192
|
nodes.splice(startIdx, endIdx - startIdx + 1, event.seq);
|
|
185
193
|
}
|
|
186
194
|
return {
|
|
187
195
|
degradeToAppend,
|
|
188
196
|
addAppendMarker,
|
|
189
|
-
clearSurfaceOp
|
|
197
|
+
clearSurfaceOp,
|
|
198
|
+
clampEnd
|
|
190
199
|
};
|
|
191
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* 夹取一个 end 落在旧坐标空间的 replace 的结尾。
|
|
203
|
+
*
|
|
204
|
+
* 仅当 replace 紧邻一个 metering 事件且其 `shadowedSeqs` 是权威数量时成立;
|
|
205
|
+
* 夹取点是从 `start` 起的第 `shadowedSeqs.length` 个当前 surface 节点(不足
|
|
206
|
+
* 则取当前 surface 末尾)。无法夹取(start 也不在当前 surface)时返回
|
|
207
|
+
* `undefined`,由调用方降级为 append。
|
|
208
|
+
*/
|
|
209
|
+
function clampReplaceEnd(events, index, nodes, startIdx) {
|
|
210
|
+
const metering = index > 0 ? events[index - 1] : void 0;
|
|
211
|
+
if (metering === void 0 || !METERING_EVENT_TYPES.has(metering.type)) return void 0;
|
|
212
|
+
const shadowedSeqs = metering.data.shadowedSeqs;
|
|
213
|
+
if (!Array.isArray(shadowedSeqs) || shadowedSeqs.length === 0) return void 0;
|
|
214
|
+
return nodes[Math.min(startIdx + shadowedSeqs.length - 1, nodes.length - 1)];
|
|
215
|
+
}
|
|
192
216
|
function repairSurfaceOps(events) {
|
|
193
217
|
const repairs = findSurfaceRepairs(events);
|
|
194
|
-
if (repairs.degradeToAppend.size === 0 && repairs.addAppendMarker.size === 0 && repairs.clearSurfaceOp.size === 0) return;
|
|
218
|
+
if (repairs.degradeToAppend.size === 0 && repairs.addAppendMarker.size === 0 && repairs.clearSurfaceOp.size === 0 && repairs.clampEnd.size === 0) return;
|
|
195
219
|
for (const event of events) {
|
|
196
220
|
const raw = event;
|
|
221
|
+
const clamped = repairs.clampEnd.get(event.seq);
|
|
197
222
|
if (repairs.degradeToAppend.has(event.seq)) raw.surfaceOp = "append";
|
|
223
|
+
else if (clamped !== void 0) raw.surfaceOp = {
|
|
224
|
+
op: "replace",
|
|
225
|
+
start: raw.surfaceOp.start,
|
|
226
|
+
end: clamped
|
|
227
|
+
};
|
|
198
228
|
else if (repairs.addAppendMarker.has(event.seq)) raw.surfaceOp = "append";
|
|
199
229
|
else if (repairs.clearSurfaceOp.has(event.seq)) delete raw.surfaceOp;
|
|
200
230
|
}
|
|
201
231
|
}
|
|
202
232
|
/**
|
|
233
|
+
* 读取时补全 assistant 结算字段:旧写入器不落库 `stream`(v2 才把流式记录
|
|
234
|
+
* 嵌入消息),读回时缺失即补空数组——上游 seed 校验要求 turn/step/stream
|
|
235
|
+
* 三者齐备,缺失会让整个会话加载失败。
|
|
236
|
+
*/
|
|
237
|
+
function repairAssistantSettlement(events) {
|
|
238
|
+
for (const event of events) {
|
|
239
|
+
if (event.type !== "assistant/message" && event.type !== "assistant/attempt") continue;
|
|
240
|
+
const data = event.data;
|
|
241
|
+
if (!Array.isArray(data["stream"])) data["stream"] = [];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* 把紧邻 replace 的 metering 事件的 `shadowedRange` / `shadowedSeqs` 对齐到
|
|
246
|
+
* replace 最终的稠密 range。
|
|
247
|
+
*
|
|
248
|
+
* 旧写入器重编号事件后,metering 与 replace 一起落在旧坐标空间:二者数值
|
|
249
|
+
* 相等但与当前 surface 无关。夹取(或 range 本身已落在稠密空间而 metering
|
|
250
|
+
* 仍是旧值)后二者不再相等,而上游 token-meter 契约要求紧邻的 metering
|
|
251
|
+
* range 与 replace range 完全一致(否则报 no adjacent shadow price)。range
|
|
252
|
+
* 已一致时不改写——当前写入器保证一致,shadowedSeqs 的额外并发节点因此
|
|
253
|
+
* 原样保留。
|
|
254
|
+
*/
|
|
255
|
+
function syncMeteringRanges(events) {
|
|
256
|
+
for (let i = 1; i < events.length; i++) {
|
|
257
|
+
const metering = events[i - 1];
|
|
258
|
+
if (!METERING_EVENT_TYPES.has(metering.type)) continue;
|
|
259
|
+
const op = events[i].surfaceOp;
|
|
260
|
+
if (typeof op !== "object" || op === null || op.op !== "replace") continue;
|
|
261
|
+
const { start, end } = op;
|
|
262
|
+
const data = metering.data;
|
|
263
|
+
if (data.shadowedRange?.start === start && data.shadowedRange.end === end) continue;
|
|
264
|
+
data.shadowedRange = {
|
|
265
|
+
start,
|
|
266
|
+
end
|
|
267
|
+
};
|
|
268
|
+
data.shadowedSeqs = events.filter((candidate) => candidate.seq >= start && candidate.seq <= end && SURFACE_EVENT_TYPES.has(candidate.type)).map((candidate) => candidate.seq);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* 读取视图修复总入口:结算字段补全 → surface 语义修复 → metering 对齐 →
|
|
273
|
+
* provenance 重算 → 孤儿 inbox splice 改写。全部只作用于内存视图,不落库。
|
|
274
|
+
*/
|
|
275
|
+
function repairReadView(events) {
|
|
276
|
+
repairAssistantSettlement(events);
|
|
277
|
+
repairSurfaceOps(events);
|
|
278
|
+
syncMeteringRanges(events);
|
|
279
|
+
recomputeReplaceProvenance(events);
|
|
280
|
+
repairOrphanInboxSplices(events);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
203
283
|
* 定位无法从空状态增量重放的 agent/inbox/spliced 事件(孤儿操作)。
|
|
204
284
|
*
|
|
205
285
|
* 上游 Inbox 每次构造都从会话起点重放全部 inbox splice,失败即拒绝整个
|
|
@@ -311,4 +391,4 @@ function toJsonlArtifact(meta, inheritedEventCount, events) {
|
|
|
311
391
|
return lines.join("\n");
|
|
312
392
|
}
|
|
313
393
|
//#endregion
|
|
314
|
-
export {
|
|
394
|
+
export { repairOrphanInboxSplices as a, rowToEvent as c, sessionConflictRow as d, sessionInsertRow as f, repairAssistantSettlement as i, rowToMeta as l, toJsonlArtifact as m, orphanInboxSpliceSeqs as n, repairReadView as o, syncMeteringRanges as p, recomputeReplaceProvenance as r, repairSurfaceOps as s, findSurfaceRepairs as t, scanRows as u };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { d as sessionConflictRow, f as sessionInsertRow } from "./log-DBFUBPhv.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { and, eq, gte, sql } from "drizzle-orm";
|
|
4
4
|
import { check, index, integer, sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
|
package/dist/storage.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as SCHEMA_VERSION, c as eventKind, d as tSchemaMeta, f as tSessionEvents, g as WriteGuard, i as EVENT_ENCODING, l as tEvents, n as openDatabase, o as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, p as tSessions, r as DEFAULT_BUSY_TIMEOUT_MS, s as eventDimensions, t as SqliteBackend, u as tPersistenceState } from "./sqlite-
|
|
1
|
+
import { a as SCHEMA_VERSION, c as eventKind, d as tSchemaMeta, f as tSessionEvents, g as WriteGuard, i as EVENT_ENCODING, l as tEvents, n as openDatabase, o as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, p as tSessions, r as DEFAULT_BUSY_TIMEOUT_MS, s as eventDimensions, t as SqliteBackend, u as tPersistenceState } from "./sqlite-CnWwGToZ.mjs";
|
|
2
2
|
export { DEFAULT_BUSY_TIMEOUT_MS, EVENT_ENCODING, SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, SqliteBackend, WriteGuard, eventDimensions, eventKind, openDatabase, tEvents, tPersistenceState, tSchemaMeta, tSessionEvents, tSessions };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/session-rdb",
|
|
3
|
-
"version": "0.0.16-alpha.
|
|
3
|
+
"version": "0.0.16-alpha.1",
|
|
4
4
|
"description": "RDB durable session backend for DeepSeek Harness: implements both session-persistence and session-branch (rewind / retry / fork) providers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"branch",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@morlay/session-branch": "^0.0.4-alpha.
|
|
38
|
+
"@morlay/session-branch": "^0.0.4-alpha.1",
|
|
39
39
|
"drizzle-orm": "^1.0.0-rc.5-ab785fc",
|
|
40
40
|
"fflate": "^0.8.2",
|
|
41
41
|
"pg": "^8.23.0"
|
package/src/index.ts
CHANGED
|
@@ -39,14 +39,7 @@ import {
|
|
|
39
39
|
} from "@deepseek-ai/dsh-session";
|
|
40
40
|
import { type Backend, type BackendTx, type EventInsert } from "./backend.ts";
|
|
41
41
|
import { WriteGuard } from "./write-guard.ts";
|
|
42
|
-
import {
|
|
43
|
-
recomputeReplaceProvenance,
|
|
44
|
-
repairOrphanInboxSplices,
|
|
45
|
-
repairSurfaceOps,
|
|
46
|
-
rowToMeta,
|
|
47
|
-
scanRows,
|
|
48
|
-
toJsonlArtifact,
|
|
49
|
-
} from "./log.ts";
|
|
42
|
+
import { repairReadView, rowToMeta, scanRows, toJsonlArtifact } from "./log.ts";
|
|
50
43
|
import {
|
|
51
44
|
DEFAULT_BUSY_TIMEOUT_MS,
|
|
52
45
|
eventDimensions,
|
|
@@ -57,7 +50,7 @@ import { SqliteBackend } from "./sqlite.ts";
|
|
|
57
50
|
import { PostgresBackend } from "./postgres.ts";
|
|
58
51
|
import { SessionBranchRdb } from "./branch.ts";
|
|
59
52
|
import { registerSessionImport } from "./import.ts";
|
|
60
|
-
import { convertLegacyRows, isLegacyVersion } from "./legacy.ts";
|
|
53
|
+
import { adoptLegacyRows, convertLegacyRows, isLegacyVersion } from "./legacy.ts";
|
|
61
54
|
|
|
62
55
|
export { SCHEMA_VERSION } from "./schema.ts";
|
|
63
56
|
export { SessionBranchRdb, SessionBranchRdbProvider, locateTurnEnd } from "./branch.ts";
|
|
@@ -278,11 +271,10 @@ class RdbSessionHandle implements SessionHandle {
|
|
|
278
271
|
}
|
|
279
272
|
throw new SessionPersistenceNotFoundError(this.id);
|
|
280
273
|
}
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
repairOrphanInboxSplices(log.events);
|
|
274
|
+
// 读取时修复(视图只读,不落库):结算字段补全、非法 surface 替换降级
|
|
275
|
+
// 或按 metering 数量夹取、metering range 对齐、provenance 重算、孤儿
|
|
276
|
+
// inbox splice 改写。
|
|
277
|
+
repairReadView(log.events);
|
|
286
278
|
return { eventState: "detached", events: log.events.slice(offset, offset + length) };
|
|
287
279
|
}
|
|
288
280
|
|
|
@@ -593,6 +585,12 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
593
585
|
const log = await this.readLog(id, {}, options?.signal);
|
|
594
586
|
if (log === undefined) throw new SessionPersistenceNotFoundError(id);
|
|
595
587
|
validateStoredEvents(log.meta, log.events);
|
|
588
|
+
// 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件坐标与
|
|
589
|
+
// 存储桥接行数不再相等——写打开时把迁移视图整体落库,使读写同坐标;
|
|
590
|
+
// 否则 append 按存储 head 重编号会撞上已有行或写坏 log。
|
|
591
|
+
if (log.migrated && log.events.length !== log.storedCount) {
|
|
592
|
+
await this.rewriteMigratedLog(id, log);
|
|
593
|
+
}
|
|
596
594
|
// 确认 head:本实例已读该会话,后续 append 的并发校验以此为基准。
|
|
597
595
|
this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
|
|
598
596
|
return this.tracker.adopt(
|
|
@@ -669,8 +667,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
669
667
|
signal?.throwIfAborted();
|
|
670
668
|
const log = await this.readLog(id, {}, signal);
|
|
671
669
|
if (log === undefined) return undefined;
|
|
672
|
-
|
|
673
|
-
recomputeReplaceProvenance(log.events);
|
|
670
|
+
repairReadView(log.events);
|
|
674
671
|
const inheritedEventCount = Math.min(log.inheritedEventCount, log.events.length);
|
|
675
672
|
return {
|
|
676
673
|
meta: log.meta,
|
|
@@ -762,6 +759,12 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
762
759
|
incarnation: string;
|
|
763
760
|
|
|
764
761
|
revision: number;
|
|
762
|
+
|
|
763
|
+
/** 存储桥接行数(迁移链可能生成/合并事件,与 `events.length` 不同)。 */
|
|
764
|
+
storedCount: number;
|
|
765
|
+
|
|
766
|
+
/** 是否经上游迁移链转换(v0/v1 → 当前格式)。 */
|
|
767
|
+
migrated: boolean;
|
|
765
768
|
}
|
|
766
769
|
| undefined
|
|
767
770
|
> {
|
|
@@ -778,15 +781,36 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
778
781
|
signal?.throwIfAborted();
|
|
779
782
|
// 旧格式(v0/v1)历史数据:行重建为物理记录,经上游迁移链转 v2 逻辑事件。
|
|
780
783
|
// 迁移链自带 seq gap / torn tail 校验(strict recovery),无需 scanRows。
|
|
784
|
+
// 混合世代 log(旧写入器跨上游版本追加)不是任何单一已发布格式,迁移链
|
|
785
|
+
// 必然拒绝——回退为当前格式视图(header 版本归一 + 读取视图修复)。
|
|
781
786
|
if (isLegacyVersion(row.fVersion)) {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
787
|
+
try {
|
|
788
|
+
const converted = convertLegacyRows(row, eventRows);
|
|
789
|
+
return {
|
|
790
|
+
meta: converted.meta,
|
|
791
|
+
inheritedEventCount: converted.inheritedEventCount,
|
|
792
|
+
events: converted.events,
|
|
793
|
+
incarnation: row.fIncarnation,
|
|
794
|
+
revision: row.fRevision,
|
|
795
|
+
storedCount: eventRows.length,
|
|
796
|
+
migrated: true,
|
|
797
|
+
};
|
|
798
|
+
} catch (error: unknown) {
|
|
799
|
+
this.ctx.logger.warn(
|
|
800
|
+
`session-rdb: session "${id}" is not a single released format; adopting its stored rows as current-format data (${error instanceof Error ? error.message : String(error)})`,
|
|
801
|
+
);
|
|
802
|
+
const adopted = adoptLegacyRows(row, eventRows);
|
|
803
|
+
return {
|
|
804
|
+
meta: adopted.meta,
|
|
805
|
+
inheritedEventCount: adopted.inheritedEventCount,
|
|
806
|
+
events: adopted.events,
|
|
807
|
+
incarnation: row.fIncarnation,
|
|
808
|
+
revision: row.fRevision,
|
|
809
|
+
storedCount: eventRows.length,
|
|
810
|
+
migrated: false,
|
|
811
|
+
...(adopted.tornFrom !== undefined ? { tornFrom: adopted.tornFrom } : {}),
|
|
812
|
+
};
|
|
813
|
+
}
|
|
790
814
|
}
|
|
791
815
|
const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
|
|
792
816
|
return {
|
|
@@ -795,10 +819,40 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
795
819
|
events: preserved,
|
|
796
820
|
incarnation: row.fIncarnation,
|
|
797
821
|
revision: row.fRevision,
|
|
822
|
+
storedCount: eventRows.length,
|
|
823
|
+
migrated: false,
|
|
798
824
|
...(tornFrom !== undefined ? { tornFrom } : {}),
|
|
799
825
|
};
|
|
800
826
|
}
|
|
801
827
|
|
|
828
|
+
/**
|
|
829
|
+
* 把迁移链读出的 v2 视图整体落库(旧格式会话写打开时的一次性迁移)。
|
|
830
|
+
*
|
|
831
|
+
* 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件 seq 空间
|
|
832
|
+
* 与存储桥接行数不再相等;写路径以存储 head 为锚点重编号,二者不一致会让
|
|
833
|
+
* append 撞上已有行。这里在同一事务内删光本会话桥接行、按迁移视图重建
|
|
834
|
+
* (新事件行,完整信封),并更新 head 与 revision;旧事件行保留(可能被
|
|
835
|
+
* fork 子会话引用,孤儿由惰性 GC 处理)。
|
|
836
|
+
*/
|
|
837
|
+
private async rewriteMigratedLog(
|
|
838
|
+
id: SessionId,
|
|
839
|
+
log: { meta: SessionHeader; inheritedEventCount: number; events: SessionEvent[] },
|
|
840
|
+
): Promise<void> {
|
|
841
|
+
await this.backend.transaction(async (tx) => {
|
|
842
|
+
await tx.deleteBridgeTail(id, 0);
|
|
843
|
+
await tx.upsertSession(
|
|
844
|
+
{ meta: log.meta, inheritedEventCount: SessionLogOffset(log.inheritedEventCount) },
|
|
845
|
+
randomUUID(),
|
|
846
|
+
);
|
|
847
|
+
const { headEventId, headSequence } = await appendEventTail(tx, log.meta, log.events, {
|
|
848
|
+
parentId: "",
|
|
849
|
+
nextSeq: 0,
|
|
850
|
+
});
|
|
851
|
+
await tx.updateHead(id, headEventId, headSequence);
|
|
852
|
+
await tx.bumpRevision(id);
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
|
|
802
856
|
async listSnapshots(
|
|
803
857
|
signal?: AbortSignal,
|
|
804
858
|
): Promise<Array<SessionPersistenceSnapshot & { inheritedEventCount: number }>> {
|
|
@@ -1068,6 +1122,9 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1068
1122
|
);
|
|
1069
1123
|
}
|
|
1070
1124
|
assertVersion(stored.meta);
|
|
1125
|
+
// adopt 比较必须与读取视图同源:live seed 来自修复后的读取视图(补
|
|
1126
|
+
// stream、surface 修复),未修复的存储视图会把修复差异误判为 id 冲突。
|
|
1127
|
+
repairReadView(stored.events);
|
|
1071
1128
|
const seed = session.snapshotEvents();
|
|
1072
1129
|
if (!seedCoversPrefix(seed, stored.events)) {
|
|
1073
1130
|
throw new Error(
|
package/src/legacy.ts
CHANGED
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
// delta(assistant/chunk)落盘被过滤——v1→v2 迁移对无 chunk 引用的
|
|
4
4
|
// assistant/message 走「无 pending 直接 emit」路径,生成 stream: [] 的 v2 事件。
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import {
|
|
7
|
+
SESSION_FORMAT_VERSION,
|
|
8
|
+
type SessionEvent,
|
|
9
|
+
type SessionHeader,
|
|
10
|
+
type SessionId,
|
|
11
|
+
} from "@deepseek-ai/dsh-session";
|
|
7
12
|
import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
8
13
|
import type { EventRow, SessionRow } from "./backend.ts";
|
|
14
|
+
import { rowToMeta, scanRows } from "./log.ts";
|
|
9
15
|
|
|
10
16
|
/** 重建 v0/v1 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
|
|
11
17
|
function physicalHeader(row: SessionRow): Record<string, unknown> {
|
|
@@ -64,4 +70,41 @@ export function isLegacyVersion(version: number): boolean {
|
|
|
64
70
|
return version < 2;
|
|
65
71
|
}
|
|
66
72
|
|
|
73
|
+
/**
|
|
74
|
+
* 迁移链拒绝后,把旧格式行直接当**当前格式**事件视图采用。
|
|
75
|
+
*
|
|
76
|
+
* 旧写入器只在建会话时落一次 header version,之后跟随上游演进继续追加
|
|
77
|
+
* 事件——同一个 log 因此可能是混合世代(v0 时代的行 + 新版本字段),不是
|
|
78
|
+
* 任何单一已发布格式,严格迁移链必然拒绝。这些行的数据形状(消息包装、
|
|
79
|
+
* 类型名)已经是当前格式,只需 header 版本归一到当前、按稠密 seq 读取;
|
|
80
|
+
* 事件数据的补全与 surface 修复由读取视图修复(repairReadView)负责。
|
|
81
|
+
*
|
|
82
|
+
* 调用方须在此之后执行 `validateStoredEvents`:真正属于旧格式的类型
|
|
83
|
+
* (`assistant/chunk` / `compact/*` 等)仍会被 fail-loud 拒绝。
|
|
84
|
+
*
|
|
85
|
+
* @param row - 会话行(f_version < 2)。
|
|
86
|
+
* @param eventRows - 该会话全部桥接行(按 f_sequence 升序)。
|
|
87
|
+
* @returns 当前格式 header(version 归一到 SESSION_FORMAT_VERSION)、收缩后的
|
|
88
|
+
* 继承前缀长度、稠密事件与可选 torn tail 起点。
|
|
89
|
+
* @throws scanRows 的已提交区损坏错误(seq gap / 不可解析行)。
|
|
90
|
+
*/
|
|
91
|
+
export function adoptLegacyRows(
|
|
92
|
+
row: SessionRow,
|
|
93
|
+
eventRows: readonly EventRow[],
|
|
94
|
+
): {
|
|
95
|
+
meta: SessionHeader;
|
|
96
|
+
inheritedEventCount: number;
|
|
97
|
+
events: SessionEvent[];
|
|
98
|
+
tornFrom?: number;
|
|
99
|
+
} {
|
|
100
|
+
const { preserved, tornFrom } = scanRows(eventRows, 0);
|
|
101
|
+
return {
|
|
102
|
+
meta: { ...rowToMeta(row), version: SESSION_FORMAT_VERSION },
|
|
103
|
+
// 继承前缀不得超过事件数(历史 rewind 未收缩的损坏样式),上游 load 判损坏。
|
|
104
|
+
inheritedEventCount: Math.min(row.fSeedLength ?? 0, preserved.length),
|
|
105
|
+
events: preserved,
|
|
106
|
+
...(tornFrom !== undefined ? { tornFrom } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
67
110
|
export type { SessionId };
|
package/src/log.ts
CHANGED
|
@@ -211,12 +211,15 @@ export function findSurfaceRepairs(events: readonly SessionEvent[]): {
|
|
|
211
211
|
degradeToAppend: Set<number>;
|
|
212
212
|
addAppendMarker: Set<number>;
|
|
213
213
|
clearSurfaceOp: Set<number>;
|
|
214
|
+
clampEnd: Map<number, number>;
|
|
214
215
|
} {
|
|
215
216
|
const nodes: number[] = [];
|
|
216
217
|
const degradeToAppend = new Set<number>();
|
|
217
218
|
const addAppendMarker = new Set<number>();
|
|
218
219
|
const clearSurfaceOp = new Set<number>();
|
|
219
|
-
|
|
220
|
+
const clampEnd = new Map<number, number>();
|
|
221
|
+
for (let index = 0; index < events.length; index++) {
|
|
222
|
+
const event = events[index]!;
|
|
220
223
|
const raw = event as SessionEvent & { surfaceOp?: unknown };
|
|
221
224
|
const op = raw.surfaceOp;
|
|
222
225
|
if (op === undefined) {
|
|
@@ -247,7 +250,16 @@ export function findSurfaceRepairs(events: readonly SessionEvent[]): {
|
|
|
247
250
|
isEventSeqLike(start) &&
|
|
248
251
|
isEventSeqLike(end);
|
|
249
252
|
const startIdx = shapeOk ? nodes.indexOf(start as number) : -1;
|
|
250
|
-
|
|
253
|
+
let endIdx = shapeOk ? nodes.indexOf(end as number) : -1;
|
|
254
|
+
// 旧写入器重编号事件后,存储的 range 端点可能落在旧坐标空间:end 不在
|
|
255
|
+
// 当前 surface 中,但紧邻 metering 事件的 shadowedSeqs 仍给出被遮蔽
|
|
256
|
+
// 节点数量。按数量从 start 起夹取到当前 surface 的同长区间,保住压缩
|
|
257
|
+
// 语义(否则只能降级 append,被压缩历史全部回到派生历史)。
|
|
258
|
+
let clamped: number | undefined;
|
|
259
|
+
if (shapeOk && startIdx !== -1 && endIdx === -1) {
|
|
260
|
+
clamped = clampReplaceEnd(events, index, nodes, startIdx);
|
|
261
|
+
if (clamped !== undefined) endIdx = nodes.indexOf(clamped);
|
|
262
|
+
}
|
|
251
263
|
const rangeOk = shapeOk && startIdx !== -1 && endIdx !== -1 && startIdx <= endIdx;
|
|
252
264
|
let rewriteOk = true;
|
|
253
265
|
if (rangeOk && event.type === "tool/result") {
|
|
@@ -265,9 +277,31 @@ export function findSurfaceRepairs(events: readonly SessionEvent[]): {
|
|
|
265
277
|
nodes.push(event.seq);
|
|
266
278
|
continue;
|
|
267
279
|
}
|
|
280
|
+
if (clamped !== undefined) clampEnd.set(event.seq, clamped);
|
|
268
281
|
nodes.splice(startIdx, endIdx - startIdx + 1, event.seq);
|
|
269
282
|
}
|
|
270
|
-
return { degradeToAppend, addAppendMarker, clearSurfaceOp };
|
|
283
|
+
return { degradeToAppend, addAppendMarker, clearSurfaceOp, clampEnd };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 夹取一个 end 落在旧坐标空间的 replace 的结尾。
|
|
288
|
+
*
|
|
289
|
+
* 仅当 replace 紧邻一个 metering 事件且其 `shadowedSeqs` 是权威数量时成立;
|
|
290
|
+
* 夹取点是从 `start` 起的第 `shadowedSeqs.length` 个当前 surface 节点(不足
|
|
291
|
+
* 则取当前 surface 末尾)。无法夹取(start 也不在当前 surface)时返回
|
|
292
|
+
* `undefined`,由调用方降级为 append。
|
|
293
|
+
*/
|
|
294
|
+
function clampReplaceEnd(
|
|
295
|
+
events: readonly SessionEvent[],
|
|
296
|
+
index: number,
|
|
297
|
+
nodes: readonly number[],
|
|
298
|
+
startIdx: number,
|
|
299
|
+
): number | undefined {
|
|
300
|
+
const metering = index > 0 ? events[index - 1] : undefined;
|
|
301
|
+
if (metering === undefined || !METERING_EVENT_TYPES.has(metering.type)) return undefined;
|
|
302
|
+
const shadowedSeqs = (metering.data as unknown as { shadowedSeqs?: unknown }).shadowedSeqs;
|
|
303
|
+
if (!Array.isArray(shadowedSeqs) || shadowedSeqs.length === 0) return undefined;
|
|
304
|
+
return nodes[Math.min(startIdx + shadowedSeqs.length - 1, nodes.length - 1)];
|
|
271
305
|
}
|
|
272
306
|
|
|
273
307
|
export function repairSurfaceOps(events: SessionEvent[]): void {
|
|
@@ -275,14 +309,19 @@ export function repairSurfaceOps(events: SessionEvent[]): void {
|
|
|
275
309
|
if (
|
|
276
310
|
repairs.degradeToAppend.size === 0 &&
|
|
277
311
|
repairs.addAppendMarker.size === 0 &&
|
|
278
|
-
repairs.clearSurfaceOp.size === 0
|
|
312
|
+
repairs.clearSurfaceOp.size === 0 &&
|
|
313
|
+
repairs.clampEnd.size === 0
|
|
279
314
|
) {
|
|
280
315
|
return;
|
|
281
316
|
}
|
|
282
317
|
for (const event of events) {
|
|
283
318
|
const raw = event as SessionEvent & { surfaceOp?: unknown };
|
|
319
|
+
const clamped = repairs.clampEnd.get(event.seq);
|
|
284
320
|
if (repairs.degradeToAppend.has(event.seq)) {
|
|
285
321
|
raw.surfaceOp = "append";
|
|
322
|
+
} else if (clamped !== undefined) {
|
|
323
|
+
const op = raw.surfaceOp as { start: number };
|
|
324
|
+
raw.surfaceOp = { op: "replace", start: op.start, end: clamped };
|
|
286
325
|
} else if (repairs.addAppendMarker.has(event.seq)) {
|
|
287
326
|
raw.surfaceOp = "append";
|
|
288
327
|
} else if (repairs.clearSurfaceOp.has(event.seq)) {
|
|
@@ -291,6 +330,65 @@ export function repairSurfaceOps(events: SessionEvent[]): void {
|
|
|
291
330
|
}
|
|
292
331
|
}
|
|
293
332
|
|
|
333
|
+
/**
|
|
334
|
+
* 读取时补全 assistant 结算字段:旧写入器不落库 `stream`(v2 才把流式记录
|
|
335
|
+
* 嵌入消息),读回时缺失即补空数组——上游 seed 校验要求 turn/step/stream
|
|
336
|
+
* 三者齐备,缺失会让整个会话加载失败。
|
|
337
|
+
*/
|
|
338
|
+
export function repairAssistantSettlement(events: SessionEvent[]): void {
|
|
339
|
+
for (const event of events) {
|
|
340
|
+
if (event.type !== "assistant/message" && event.type !== "assistant/attempt") continue;
|
|
341
|
+
const data = event.data as unknown as Record<string, unknown>;
|
|
342
|
+
if (!Array.isArray(data["stream"])) data["stream"] = [];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* 把紧邻 replace 的 metering 事件的 `shadowedRange` / `shadowedSeqs` 对齐到
|
|
348
|
+
* replace 最终的稠密 range。
|
|
349
|
+
*
|
|
350
|
+
* 旧写入器重编号事件后,metering 与 replace 一起落在旧坐标空间:二者数值
|
|
351
|
+
* 相等但与当前 surface 无关。夹取(或 range 本身已落在稠密空间而 metering
|
|
352
|
+
* 仍是旧值)后二者不再相等,而上游 token-meter 契约要求紧邻的 metering
|
|
353
|
+
* range 与 replace range 完全一致(否则报 no adjacent shadow price)。range
|
|
354
|
+
* 已一致时不改写——当前写入器保证一致,shadowedSeqs 的额外并发节点因此
|
|
355
|
+
* 原样保留。
|
|
356
|
+
*/
|
|
357
|
+
export function syncMeteringRanges(events: SessionEvent[]): void {
|
|
358
|
+
for (let i = 1; i < events.length; i++) {
|
|
359
|
+
const metering = events[i - 1]!;
|
|
360
|
+
if (!METERING_EVENT_TYPES.has(metering.type)) continue;
|
|
361
|
+
const event = events[i]!;
|
|
362
|
+
const op = (event as SessionEvent & { surfaceOp?: unknown }).surfaceOp;
|
|
363
|
+
if (typeof op !== "object" || op === null || (op as { op?: string }).op !== "replace") continue;
|
|
364
|
+
const { start, end } = op as { start: number; end: number };
|
|
365
|
+
const data = metering.data as unknown as {
|
|
366
|
+
shadowedRange?: { start: number; end: number };
|
|
367
|
+
shadowedSeqs?: number[];
|
|
368
|
+
};
|
|
369
|
+
if (data.shadowedRange?.start === start && data.shadowedRange.end === end) continue;
|
|
370
|
+
data.shadowedRange = { start, end };
|
|
371
|
+
data.shadowedSeqs = events
|
|
372
|
+
.filter(
|
|
373
|
+
(candidate) =>
|
|
374
|
+
candidate.seq >= start && candidate.seq <= end && SURFACE_EVENT_TYPES.has(candidate.type),
|
|
375
|
+
)
|
|
376
|
+
.map((candidate) => candidate.seq);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* 读取视图修复总入口:结算字段补全 → surface 语义修复 → metering 对齐 →
|
|
382
|
+
* provenance 重算 → 孤儿 inbox splice 改写。全部只作用于内存视图,不落库。
|
|
383
|
+
*/
|
|
384
|
+
export function repairReadView(events: SessionEvent[]): void {
|
|
385
|
+
repairAssistantSettlement(events);
|
|
386
|
+
repairSurfaceOps(events);
|
|
387
|
+
syncMeteringRanges(events);
|
|
388
|
+
recomputeReplaceProvenance(events);
|
|
389
|
+
repairOrphanInboxSplices(events);
|
|
390
|
+
}
|
|
391
|
+
|
|
294
392
|
interface InboxSpliceLike {
|
|
295
393
|
target?: unknown;
|
|
296
394
|
start?: unknown;
|