@morlay/session-rdb 0.0.16-alpha.1 → 0.0.16-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifact.d.mts +22 -3
- package/dist/artifact.mjs +3 -3
- package/dist/{import-mZZgDBLd.mjs → import-eBSoCrxl.mjs} +1 -1
- package/dist/import.d.mts +1 -1
- package/dist/import.mjs +1 -1
- package/dist/{index-GdKkSSb-.d.mts → index-DfS0PzGM.d.mts} +5 -5
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +29 -18
- package/dist/{log-DBFUBPhv.mjs → log-BIuXv09P.mjs} +143 -20
- package/dist/{sqlite-CnWwGToZ.mjs → sqlite-Dpd17YhI.mjs} +1 -1
- package/dist/storage.mjs +1 -1
- package/dist/testing.mjs +4 -4
- package/package.json +12 -12
- package/src/index.ts +19 -13
- package/src/legacy.ts +32 -14
- package/src/log.ts +160 -17
- package/src/testing/contract.ts +1 -1
package/dist/artifact.d.mts
CHANGED
|
@@ -54,6 +54,14 @@ declare function repairSurfaceOps(events: SessionEvent[]): void;
|
|
|
54
54
|
* 三者齐备,缺失会让整个会话加载失败。
|
|
55
55
|
*/
|
|
56
56
|
declare function repairAssistantSettlement(events: SessionEvent[]): void;
|
|
57
|
+
/**
|
|
58
|
+
* 读取时把旧格式 `request/header` 归一为当前格式:v3 起 system prompt 由
|
|
59
|
+
* surface 上的 `system/message` 承载,header 必须省略 `system`,空 `tools` /
|
|
60
|
+
* `adapterDefaults` 也必须省略(上游 v3 事件校验 fail loud)。混合世代回退
|
|
61
|
+
* 视图直接采用存储行,不归一会让整个会话加载失败。system prompt 因此不再
|
|
62
|
+
* 进入模型请求;会话下次运行由 v3 的 system/message 机制重建。
|
|
63
|
+
*/
|
|
64
|
+
declare function repairRequestHeaders(events: SessionEvent[]): void;
|
|
57
65
|
/**
|
|
58
66
|
* 把紧邻 replace 的 metering 事件的 `shadowedRange` / `shadowedSeqs` 对齐到
|
|
59
67
|
* replace 最终的稠密 range。
|
|
@@ -67,8 +75,19 @@ declare function repairAssistantSettlement(events: SessionEvent[]): void;
|
|
|
67
75
|
*/
|
|
68
76
|
declare function syncMeteringRanges(events: SessionEvent[]): void;
|
|
69
77
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
78
|
+
* 读取时把 v2 时代的 PTC 词汇归一为当前词汇。
|
|
79
|
+
*
|
|
80
|
+
* 上游 v2→v3 迁移把 `tool/code-dispatch(-start)` 改名为
|
|
81
|
+
* `tool/ptc-dispatch(-start)`、把 `tools-code-mode` 插件来源改名为 `tools-ptc`、
|
|
82
|
+
* 把 agent preset 值 `code` 改名为 `ptc`。混合世代回退视图直接采用存储行,
|
|
83
|
+
* 不重命名会被上游 v3 校验以「unknown event type」拒绝整个会话。词汇改写
|
|
84
|
+
* 与上游迁移链的 renamePtcEvent 对齐。
|
|
85
|
+
*/
|
|
86
|
+
declare function renameLegacyPtcEvents(events: SessionEvent[]): void;
|
|
87
|
+
/**
|
|
88
|
+
* 读取视图修复总入口:形状补全(结算字段 / request header)→ PTC 词汇归一
|
|
89
|
+
* → surface 语义修复 → metering 对齐 → provenance 重算 → 孤儿 inbox splice
|
|
90
|
+
* 改写。全部只作用于内存视图,不落库。
|
|
72
91
|
*/
|
|
73
92
|
declare function repairReadView(events: SessionEvent[]): void;
|
|
74
93
|
/**
|
|
@@ -88,4 +107,4 @@ declare function scanRows(rows: readonly EventRow[], base?: number): {
|
|
|
88
107
|
};
|
|
89
108
|
declare function toJsonlArtifact(meta: SessionHeader, inheritedEventCount: number, events: readonly SessionEvent[]): string;
|
|
90
109
|
//#endregion
|
|
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 };
|
|
110
|
+
export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, renameLegacyPtcEvents, repairAssistantSettlement, repairOrphanInboxSplices, repairReadView, repairRequestHeaders, 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, repairAssistantSettlement, repairOrphanInboxSplices, repairReadView, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, syncMeteringRanges, toJsonlArtifact };
|
|
1
|
+
import { a as repairAssistantSettlement, c as repairRequestHeaders, d as rowToMeta, f as scanRows, g as toJsonlArtifact, h as syncMeteringRanges, i as renameLegacyPtcEvents, l as repairSurfaceOps, m as sessionInsertRow, n as orphanInboxSpliceSeqs, o as repairOrphanInboxSplices, p as sessionConflictRow, r as recomputeReplaceProvenance, s as repairReadView, t as findSurfaceRepairs, u as rowToEvent } from "./log-BIuXv09P.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-eBSoCrxl.mjs";
|
|
3
|
+
export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, renameLegacyPtcEvents, repairAssistantSettlement, repairOrphanInboxSplices, repairReadView, repairRequestHeaders, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, syncMeteringRanges, toJsonlArtifact };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { d as rowToMeta } from "./log-BIuXv09P.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-DfS0PzGM.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-eBSoCrxl.mjs";
|
|
2
2
|
export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, parseImportZip, parseJsonlArtifact, persistImport, registerSessionImport };
|
|
@@ -182,9 +182,9 @@ declare class SessionPersistenceRdb extends SessionPersistence {
|
|
|
182
182
|
/** 空会话 materialize(flush 的持久化屏障)。 */
|
|
183
183
|
materializeEmpty(meta: SessionHeader, inheritedEventCount: SessionLogOffset): Promise<void>;
|
|
184
184
|
/** 原样 append 落库(handle 已校验 contiguity;torn tail 先截断)。
|
|
185
|
-
* 与上游 JSONL 一致:ignorable
|
|
186
|
-
*
|
|
187
|
-
*
|
|
185
|
+
* 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验当前
|
|
186
|
+
* 格式形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
|
|
187
|
+
* 非当前格式(v0/v1/v2)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
|
|
188
188
|
appendBatch(meta: SessionHeader, inheritedEventCount: SessionLogOffset, events: readonly SessionEvent[], tornTruncateTo?: number): Promise<boolean>;
|
|
189
189
|
/** 读取一个会话的稠密 log(含 torn tail 检测,不含修复改写)。 */
|
|
190
190
|
readLog(id: SessionId, options?: {
|
|
@@ -198,11 +198,11 @@ declare class SessionPersistenceRdb extends SessionPersistence {
|
|
|
198
198
|
revision: number;
|
|
199
199
|
/** 存储桥接行数(迁移链可能生成/合并事件,与 `events.length` 不同)。 */
|
|
200
200
|
storedCount: number;
|
|
201
|
-
/**
|
|
201
|
+
/** 是否经上游迁移链转换(非当前格式 → 当前格式)。 */
|
|
202
202
|
migrated: boolean;
|
|
203
203
|
} | undefined>;
|
|
204
204
|
/**
|
|
205
|
-
*
|
|
205
|
+
* 把迁移链读出的当前格式视图整体落库(非当前格式会话写打开时的一次性迁移)。
|
|
206
206
|
*
|
|
207
207
|
* 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件 seq 空间
|
|
208
208
|
* 与存储桥接行数不再相等;写路径以存储 head 为锚点重编号,二者不一致会让
|
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-DfS0PzGM.mjs";
|
|
3
3
|
export { Config, SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionPersistenceRdb, SessionPersistenceRdb as default, SessionPersistenceRdbInternals, locateTurnEnd };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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 { d as
|
|
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-Dpd17YhI.mjs";
|
|
2
|
+
import { c as repairRequestHeaders, d as rowToMeta, f as scanRows, g as toJsonlArtifact, m as sessionInsertRow, p as sessionConflictRow, s as repairReadView } from "./log-BIuXv09P.mjs";
|
|
3
|
+
import { c as SessionBranchRdbProvider, l as locateTurnEnd, o as registerSessionImport, s as SessionBranchRdb } from "./import-eBSoCrxl.mjs";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { Pool } from "pg";
|
|
@@ -161,35 +161,43 @@ var PostgresBackend = class PostgresBackend {
|
|
|
161
161
|
};
|
|
162
162
|
//#endregion
|
|
163
163
|
//#region src/legacy.ts
|
|
164
|
-
/** 重建 v0/v1 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
|
|
164
|
+
/** 重建 v0/v1/v2 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
|
|
165
165
|
function physicalHeader(row) {
|
|
166
|
-
|
|
166
|
+
const common = {
|
|
167
167
|
type: "session",
|
|
168
168
|
version: row.fVersion,
|
|
169
169
|
id: row.fSessionId,
|
|
170
170
|
createdAt: row.fCreatedAt,
|
|
171
171
|
...row.fCwd !== null ? { cwd: row.fCwd } : {},
|
|
172
172
|
...row.fParentSession !== null ? { parentSession: row.fParentSession } : {},
|
|
173
|
-
...row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {},
|
|
174
173
|
...row.fOrigin !== null ? { origin: row.fOrigin } : {},
|
|
175
174
|
delegationDepth: row.fDelegationDepth ?? 0
|
|
176
175
|
};
|
|
176
|
+
return row.fVersion < 2 ? {
|
|
177
|
+
...common,
|
|
178
|
+
...row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {}
|
|
179
|
+
} : {
|
|
180
|
+
...common,
|
|
181
|
+
isSeeded: row.fSeedLength !== null
|
|
182
|
+
};
|
|
177
183
|
}
|
|
178
|
-
/** 重建一条 v0/v1 物理事件记录(桥接行 + 事件行 → 物理 JSON 对象)。 */
|
|
184
|
+
/** 重建一条 v0/v1/v2 物理事件记录(桥接行 + 事件行 → 物理 JSON 对象)。 */
|
|
179
185
|
function physicalEvent(row) {
|
|
186
|
+
const stored = JSON.parse(row.fData);
|
|
187
|
+
const full = typeof stored === "object" && stored !== null && !Array.isArray(stored) && typeof stored["type"] === "string" && "data" in stored;
|
|
180
188
|
return {
|
|
181
189
|
type: row.fType,
|
|
182
190
|
seq: row.fSequence,
|
|
183
191
|
time: row.fCreatedAt,
|
|
184
|
-
data:
|
|
192
|
+
data: full ? stored["data"] : stored,
|
|
185
193
|
...row.fSurfaceOp !== null ? { surfaceOp: JSON.parse(row.fSurfaceOp) } : {}
|
|
186
194
|
};
|
|
187
195
|
}
|
|
188
196
|
/**
|
|
189
|
-
*
|
|
190
|
-
* @param row - 会话行(f_version <
|
|
197
|
+
* 把非当前格式(v0/v1/v2)行转换为当前逻辑事件与 header。
|
|
198
|
+
* @param row - 会话行(f_version < SESSION_FORMAT_VERSION)。
|
|
191
199
|
* @param eventRows - 已扫描保留的桥接行(稠密 seq,torn tail 已剔除)。
|
|
192
|
-
* @returns
|
|
200
|
+
* @returns 当前逻辑 header、继承前缀长度与事件。
|
|
193
201
|
* @throws 迁移链拒绝(格式损坏 / 无法迁移)时原样抛出。
|
|
194
202
|
*/
|
|
195
203
|
function convertLegacyRows(row, eventRows) {
|
|
@@ -205,9 +213,9 @@ function convertLegacyRows(row, eventRows) {
|
|
|
205
213
|
events: artifact.events
|
|
206
214
|
};
|
|
207
215
|
}
|
|
208
|
-
/**
|
|
216
|
+
/** 会话行是否携带非当前格式(f_version < SESSION_FORMAT_VERSION)数据。 */
|
|
209
217
|
function isLegacyVersion(version) {
|
|
210
|
-
return version <
|
|
218
|
+
return version < SESSION_FORMAT_VERSION;
|
|
211
219
|
}
|
|
212
220
|
/**
|
|
213
221
|
* 迁移链拒绝后,把旧格式行直接当**当前格式**事件视图采用。
|
|
@@ -221,7 +229,7 @@ function isLegacyVersion(version) {
|
|
|
221
229
|
* 调用方须在此之后执行 `validateStoredEvents`:真正属于旧格式的类型
|
|
222
230
|
* (`assistant/chunk` / `compact/*` 等)仍会被 fail-loud 拒绝。
|
|
223
231
|
*
|
|
224
|
-
* @param row - 会话行(f_version <
|
|
232
|
+
* @param row - 会话行(f_version < SESSION_FORMAT_VERSION)。
|
|
225
233
|
* @param eventRows - 该会话全部桥接行(按 f_sequence 升序)。
|
|
226
234
|
* @returns 当前格式 header(version 归一到 SESSION_FORMAT_VERSION)、收缩后的
|
|
227
235
|
* 继承前缀长度、稠密事件与可选 torn tail 起点。
|
|
@@ -229,6 +237,7 @@ function isLegacyVersion(version) {
|
|
|
229
237
|
*/
|
|
230
238
|
function adoptLegacyRows(row, eventRows) {
|
|
231
239
|
const { preserved, tornFrom } = scanRows(eventRows, 0);
|
|
240
|
+
repairRequestHeaders(preserved);
|
|
232
241
|
return {
|
|
233
242
|
meta: {
|
|
234
243
|
...rowToMeta(row),
|
|
@@ -578,6 +587,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
578
587
|
}));
|
|
579
588
|
const log = await this.readLog(id, {}, options?.signal);
|
|
580
589
|
if (log === void 0) throw new SessionPersistenceNotFoundError(id);
|
|
590
|
+
repairReadView(log.events);
|
|
581
591
|
validateStoredEvents(log.meta, log.events);
|
|
582
592
|
return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "read", {
|
|
583
593
|
cursor: log.events.length,
|
|
@@ -594,6 +604,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
594
604
|
}));
|
|
595
605
|
const log = await this.readLog(id, {}, options?.signal);
|
|
596
606
|
if (log === void 0) throw new SessionPersistenceNotFoundError(id);
|
|
607
|
+
repairReadView(log.events);
|
|
597
608
|
validateStoredEvents(log.meta, log.events);
|
|
598
609
|
if (log.migrated && log.events.length !== log.storedCount) await this.rewriteMigratedLog(id, log);
|
|
599
610
|
this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
|
|
@@ -682,9 +693,9 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
682
693
|
this.writeGuard.confirmHead(meta.id, -1);
|
|
683
694
|
}
|
|
684
695
|
/** 原样 append 落库(handle 已校验 contiguity;torn tail 先截断)。
|
|
685
|
-
* 与上游 JSONL 一致:ignorable
|
|
686
|
-
*
|
|
687
|
-
*
|
|
696
|
+
* 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验当前
|
|
697
|
+
* 格式形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
|
|
698
|
+
* 非当前格式(v0/v1/v2)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
|
|
688
699
|
async appendBatch(meta, inheritedEventCount, events, tornTruncateTo) {
|
|
689
700
|
await this.ready;
|
|
690
701
|
if (events.length === 0) return false;
|
|
@@ -765,7 +776,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
765
776
|
};
|
|
766
777
|
}
|
|
767
778
|
/**
|
|
768
|
-
*
|
|
779
|
+
* 把迁移链读出的当前格式视图整体落库(非当前格式会话写打开时的一次性迁移)。
|
|
769
780
|
*
|
|
770
781
|
* 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件 seq 空间
|
|
771
782
|
* 与存储桥接行数不再相等;写路径以存储 head 为锚点重编号,二者不一致会让
|
|
@@ -1,5 +1,27 @@
|
|
|
1
|
+
import { SessionSeq } from "@deepseek-ai/dsh-session";
|
|
1
2
|
import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
2
3
|
//#region src/log.ts
|
|
4
|
+
/**
|
|
5
|
+
* 把桥接行列里的 replace surfaceOp 归一到当前字段名。
|
|
6
|
+
*
|
|
7
|
+
* v2 时代落库的 JSON 用 `start`/`end`,当前格式用 `startSeq`/`endSeq`;
|
|
8
|
+
* 混合世代回退路径直接采用存储行,必须在此归一,否则上游 v3 surface 校验
|
|
9
|
+
* 以「invalid replace surfaceOp」拒绝整个会话。非 replace 形状原样返回,
|
|
10
|
+
* 由读取视图修复决定降级。
|
|
11
|
+
*/
|
|
12
|
+
function normalizeSurfaceOp(value) {
|
|
13
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
|
|
14
|
+
const record = value;
|
|
15
|
+
if (record["op"] !== "replace") return value;
|
|
16
|
+
const startSeq = record["startSeq"] ?? record["start"];
|
|
17
|
+
const endSeq = record["endSeq"] ?? record["end"];
|
|
18
|
+
if (typeof startSeq !== "number" || typeof endSeq !== "number") return value;
|
|
19
|
+
return {
|
|
20
|
+
op: "replace",
|
|
21
|
+
startSeq: SessionSeq(startSeq),
|
|
22
|
+
endSeq: SessionSeq(endSeq)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
3
25
|
function rowToMeta(row) {
|
|
4
26
|
if (!Number.isSafeInteger(row.fCreatedAt) || row.fCreatedAt < 0) throw new Error("stored session createdAt must be a non-negative safe integer");
|
|
5
27
|
return {
|
|
@@ -43,7 +65,7 @@ function sessionConflictRow(storage) {
|
|
|
43
65
|
};
|
|
44
66
|
}
|
|
45
67
|
function rowToEvent(row) {
|
|
46
|
-
const surfaceOp = row.fSurfaceOp !== null ? JSON.parse(row.fSurfaceOp) : void 0;
|
|
68
|
+
const surfaceOp = row.fSurfaceOp !== null ? normalizeSurfaceOp(JSON.parse(row.fSurfaceOp)) : void 0;
|
|
47
69
|
const record = JSON.parse(row.fData);
|
|
48
70
|
if (typeof record === "object" && record !== null && !Array.isArray(record) && typeof record["type"] === "string" && typeof record["seq"] === "number" && typeof record["time"] === "number" && "data" in record) return {
|
|
49
71
|
...record,
|
|
@@ -60,6 +82,7 @@ function rowToEvent(row) {
|
|
|
60
82
|
};
|
|
61
83
|
}
|
|
62
84
|
const SURFACE_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
85
|
+
"system/message",
|
|
63
86
|
"user/message",
|
|
64
87
|
"assistant/message",
|
|
65
88
|
"tool/result"
|
|
@@ -82,7 +105,7 @@ function recomputeReplaceProvenance(events) {
|
|
|
82
105
|
const raw = events[i];
|
|
83
106
|
const op = raw.surfaceOp;
|
|
84
107
|
if (typeof op !== "object" || op === null || op.op !== "replace") continue;
|
|
85
|
-
const {
|
|
108
|
+
const { startSeq, endSeq } = op;
|
|
86
109
|
const metering = i > 0 ? events[i - 1] : void 0;
|
|
87
110
|
const meteringData = metering !== void 0 && METERING_EVENT_TYPES.has(metering.type) ? metering.data : void 0;
|
|
88
111
|
if (meteringData?.shadowedSeqs !== void 0) {
|
|
@@ -90,7 +113,7 @@ function recomputeReplaceProvenance(events) {
|
|
|
90
113
|
continue;
|
|
91
114
|
}
|
|
92
115
|
const refs = [];
|
|
93
|
-
for (const candidate of events) if (candidate.seq >=
|
|
116
|
+
for (const candidate of events) if (candidate.seq >= startSeq && candidate.seq <= endSeq && SURFACE_EVENT_TYPES.has(candidate.type)) refs.push(candidate.seq);
|
|
94
117
|
raw.sourceEventSeqs = refs;
|
|
95
118
|
}
|
|
96
119
|
}
|
|
@@ -163,8 +186,8 @@ function findSurfaceRepairs(events) {
|
|
|
163
186
|
continue;
|
|
164
187
|
}
|
|
165
188
|
const replace = typeof op === "object" && op !== null && !Array.isArray(op) ? op : void 0;
|
|
166
|
-
const start = replace?.["
|
|
167
|
-
const end = replace?.["
|
|
189
|
+
const start = replace?.["startSeq"];
|
|
190
|
+
const end = replace?.["endSeq"];
|
|
168
191
|
const shapeOk = replace !== void 0 && replace["op"] === "replace" && isEventSeqLike(start) && isEventSeqLike(end);
|
|
169
192
|
const startIdx = shapeOk ? nodes.indexOf(start) : -1;
|
|
170
193
|
let endIdx = shapeOk ? nodes.indexOf(end) : -1;
|
|
@@ -174,6 +197,7 @@ function findSurfaceRepairs(events) {
|
|
|
174
197
|
if (clamped !== void 0) endIdx = nodes.indexOf(clamped);
|
|
175
198
|
}
|
|
176
199
|
const rangeOk = shapeOk && startIdx !== -1 && endIdx !== -1 && startIdx <= endIdx;
|
|
200
|
+
const provenanceOk = event.type !== "assistant/message";
|
|
177
201
|
let rewriteOk = true;
|
|
178
202
|
if (rangeOk && event.type === "tool/result") {
|
|
179
203
|
const shadowed = nodes.slice(startIdx, endIdx + 1);
|
|
@@ -183,7 +207,7 @@ function findSurfaceRepairs(events) {
|
|
|
183
207
|
rewriteOk = original?.type === "tool/result" && toolResultRewriteContentOnly(original, event);
|
|
184
208
|
}
|
|
185
209
|
}
|
|
186
|
-
if (!rangeOk || !rewriteOk) {
|
|
210
|
+
if (!rangeOk || !rewriteOk || !provenanceOk) {
|
|
187
211
|
degradeToAppend.add(event.seq);
|
|
188
212
|
nodes.push(event.seq);
|
|
189
213
|
continue;
|
|
@@ -220,12 +244,14 @@ function repairSurfaceOps(events) {
|
|
|
220
244
|
const raw = event;
|
|
221
245
|
const clamped = repairs.clampEnd.get(event.seq);
|
|
222
246
|
if (repairs.degradeToAppend.has(event.seq)) raw.surfaceOp = "append";
|
|
223
|
-
else if (clamped !== void 0)
|
|
224
|
-
op
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
247
|
+
else if (clamped !== void 0) {
|
|
248
|
+
const op = raw.surfaceOp;
|
|
249
|
+
raw.surfaceOp = {
|
|
250
|
+
op: "replace",
|
|
251
|
+
startSeq: SessionSeq(op.startSeq),
|
|
252
|
+
endSeq: SessionSeq(clamped)
|
|
253
|
+
};
|
|
254
|
+
} else if (repairs.addAppendMarker.has(event.seq)) raw.surfaceOp = "append";
|
|
229
255
|
else if (repairs.clearSurfaceOp.has(event.seq)) delete raw.surfaceOp;
|
|
230
256
|
}
|
|
231
257
|
}
|
|
@@ -242,6 +268,25 @@ function repairAssistantSettlement(events) {
|
|
|
242
268
|
}
|
|
243
269
|
}
|
|
244
270
|
/**
|
|
271
|
+
* 读取时把旧格式 `request/header` 归一为当前格式:v3 起 system prompt 由
|
|
272
|
+
* surface 上的 `system/message` 承载,header 必须省略 `system`,空 `tools` /
|
|
273
|
+
* `adapterDefaults` 也必须省略(上游 v3 事件校验 fail loud)。混合世代回退
|
|
274
|
+
* 视图直接采用存储行,不归一会让整个会话加载失败。system prompt 因此不再
|
|
275
|
+
* 进入模型请求;会话下次运行由 v3 的 system/message 机制重建。
|
|
276
|
+
*/
|
|
277
|
+
function repairRequestHeaders(events) {
|
|
278
|
+
for (const event of events) {
|
|
279
|
+
if (event.type !== "request/header") continue;
|
|
280
|
+
const header = event.data["header"];
|
|
281
|
+
if (typeof header !== "object" || header === null || Array.isArray(header)) continue;
|
|
282
|
+
const record = header;
|
|
283
|
+
delete record["system"];
|
|
284
|
+
if (Array.isArray(record["tools"]) && record["tools"].length === 0) delete record["tools"];
|
|
285
|
+
const defaults = record["adapterDefaults"];
|
|
286
|
+
if (typeof defaults === "object" && defaults !== null && !Array.isArray(defaults) && Object.keys(defaults).length === 0) delete record["adapterDefaults"];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
245
290
|
* 把紧邻 replace 的 metering 事件的 `shadowedRange` / `shadowedSeqs` 对齐到
|
|
246
291
|
* replace 最终的稠密 range。
|
|
247
292
|
*
|
|
@@ -258,22 +303,100 @@ function syncMeteringRanges(events) {
|
|
|
258
303
|
if (!METERING_EVENT_TYPES.has(metering.type)) continue;
|
|
259
304
|
const op = events[i].surfaceOp;
|
|
260
305
|
if (typeof op !== "object" || op === null || op.op !== "replace") continue;
|
|
261
|
-
const {
|
|
306
|
+
const { startSeq, endSeq } = op;
|
|
262
307
|
const data = metering.data;
|
|
263
|
-
if (data.shadowedRange?.start ===
|
|
308
|
+
if (data.shadowedRange?.start === startSeq && data.shadowedRange.end === endSeq) continue;
|
|
264
309
|
data.shadowedRange = {
|
|
265
|
-
start,
|
|
266
|
-
end
|
|
310
|
+
start: startSeq,
|
|
311
|
+
end: endSeq
|
|
267
312
|
};
|
|
268
|
-
data.shadowedSeqs = events.filter((candidate) => candidate.seq >=
|
|
313
|
+
data.shadowedSeqs = events.filter((candidate) => candidate.seq >= startSeq && candidate.seq <= endSeq && SURFACE_EVENT_TYPES.has(candidate.type)).map((candidate) => candidate.seq);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const PTC_EVENT_RENAMES = {
|
|
317
|
+
"tool/code-dispatch-start": "tool/ptc-dispatch-start",
|
|
318
|
+
"tool/code-dispatch": "tool/ptc-dispatch"
|
|
319
|
+
};
|
|
320
|
+
/** 把消息的 `tools-code-mode` 插件来源改写为 `tools-ptc`(非该来源原样返回)。 */
|
|
321
|
+
function renamePtcMessageSource(message) {
|
|
322
|
+
if (typeof message !== "object" || message === null || Array.isArray(message)) return message;
|
|
323
|
+
const record = message;
|
|
324
|
+
const source = record["source"];
|
|
325
|
+
if (typeof source !== "object" || source === null || Array.isArray(source)) return message;
|
|
326
|
+
const sourceRecord = source;
|
|
327
|
+
if (sourceRecord["kind"] !== "plugin" || sourceRecord["plugin"] !== "tools-code-mode") return message;
|
|
328
|
+
return {
|
|
329
|
+
...record,
|
|
330
|
+
source: {
|
|
331
|
+
...sourceRecord,
|
|
332
|
+
plugin: "tools-ptc"
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* 读取时把 v2 时代的 PTC 词汇归一为当前词汇。
|
|
338
|
+
*
|
|
339
|
+
* 上游 v2→v3 迁移把 `tool/code-dispatch(-start)` 改名为
|
|
340
|
+
* `tool/ptc-dispatch(-start)`、把 `tools-code-mode` 插件来源改名为 `tools-ptc`、
|
|
341
|
+
* 把 agent preset 值 `code` 改名为 `ptc`。混合世代回退视图直接采用存储行,
|
|
342
|
+
* 不重命名会被上游 v3 校验以「unknown event type」拒绝整个会话。词汇改写
|
|
343
|
+
* 与上游迁移链的 renamePtcEvent 对齐。
|
|
344
|
+
*/
|
|
345
|
+
function renameLegacyPtcEvents(events) {
|
|
346
|
+
for (let index = 0; index < events.length; index++) {
|
|
347
|
+
const event = events[index];
|
|
348
|
+
const renamedType = PTC_EVENT_RENAMES[event.type];
|
|
349
|
+
if (renamedType !== void 0) {
|
|
350
|
+
events[index] = {
|
|
351
|
+
...event,
|
|
352
|
+
type: renamedType
|
|
353
|
+
};
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (event.type === "agent-preset/selected") {
|
|
357
|
+
const data = event.data;
|
|
358
|
+
if (data["agentPreset"] === "code") events[index] = {
|
|
359
|
+
...event,
|
|
360
|
+
data: {
|
|
361
|
+
...data,
|
|
362
|
+
agentPreset: "ptc"
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (event.type === "user/message") {
|
|
368
|
+
const renamed = renamePtcMessageSource(event.data);
|
|
369
|
+
if (renamed !== event.data) events[index] = {
|
|
370
|
+
...event,
|
|
371
|
+
data: renamed
|
|
372
|
+
};
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (event.type === "agent/inbox/spliced" || event.type === "session/title-llm-request") {
|
|
376
|
+
const data = event.data;
|
|
377
|
+
const key = event.type === "agent/inbox/spliced" ? "inserted" : "messages";
|
|
378
|
+
const list = data[key];
|
|
379
|
+
if (!Array.isArray(list)) continue;
|
|
380
|
+
const renamed = list.map(renamePtcMessageSource);
|
|
381
|
+
if (renamed.some((message, position) => message !== list[position])) events[index] = {
|
|
382
|
+
...event,
|
|
383
|
+
data: {
|
|
384
|
+
...data,
|
|
385
|
+
[key]: renamed
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
269
389
|
}
|
|
270
390
|
}
|
|
271
391
|
/**
|
|
272
|
-
*
|
|
273
|
-
* provenance 重算 → 孤儿 inbox splice
|
|
392
|
+
* 读取视图修复总入口:形状补全(结算字段 / request header)→ PTC 词汇归一
|
|
393
|
+
* → surface 语义修复 → metering 对齐 → provenance 重算 → 孤儿 inbox splice
|
|
394
|
+
* 改写。全部只作用于内存视图,不落库。
|
|
274
395
|
*/
|
|
275
396
|
function repairReadView(events) {
|
|
276
397
|
repairAssistantSettlement(events);
|
|
398
|
+
repairRequestHeaders(events);
|
|
399
|
+
renameLegacyPtcEvents(events);
|
|
277
400
|
repairSurfaceOps(events);
|
|
278
401
|
syncMeteringRanges(events);
|
|
279
402
|
recomputeReplaceProvenance(events);
|
|
@@ -391,4 +514,4 @@ function toJsonlArtifact(meta, inheritedEventCount, events) {
|
|
|
391
514
|
return lines.join("\n");
|
|
392
515
|
}
|
|
393
516
|
//#endregion
|
|
394
|
-
export {
|
|
517
|
+
export { repairAssistantSettlement as a, repairRequestHeaders as c, rowToMeta as d, scanRows as f, toJsonlArtifact as g, syncMeteringRanges as h, renameLegacyPtcEvents as i, repairSurfaceOps as l, sessionInsertRow as m, orphanInboxSpliceSeqs as n, repairOrphanInboxSplices as o, sessionConflictRow as p, recomputeReplaceProvenance as r, repairReadView as s, findSurfaceRepairs as t, rowToEvent as u };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { m as sessionInsertRow, p as sessionConflictRow } from "./log-BIuXv09P.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-Dpd17YhI.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/dist/testing.mjs
CHANGED
|
@@ -5116,7 +5116,7 @@ function manageArtifactAttachment(attachment) {
|
|
|
5116
5116
|
if (attachment.body != null) attachment.bodyEncoding ??= "base64";
|
|
5117
5117
|
}
|
|
5118
5118
|
//#endregion
|
|
5119
|
-
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.
|
|
5119
|
+
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.14.0_vite@8.2.2__b2fde82745a4512744fb5cfb49e8ecf4/node_modules/vitest/dist/chunks/utils.BX5Fg8C4.js
|
|
5120
5120
|
const NAME_WORKER_STATE = "__vitest_worker__";
|
|
5121
5121
|
function getWorkerState() {
|
|
5122
5122
|
const workerState = globalThis[NAME_WORKER_STATE];
|
|
@@ -10019,10 +10019,10 @@ function offsetToLineNumber(source, offset) {
|
|
|
10019
10019
|
return line + 1;
|
|
10020
10020
|
}
|
|
10021
10021
|
//#endregion
|
|
10022
|
-
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.
|
|
10022
|
+
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.14.0_vite@8.2.2__b2fde82745a4512744fb5cfb49e8ecf4/node_modules/vitest/dist/chunks/_commonjsHelpers.D26ty3Ew.js
|
|
10023
10023
|
var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
|
|
10024
10024
|
//#endregion
|
|
10025
|
-
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.
|
|
10025
|
+
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.14.0_vite@8.2.2__b2fde82745a4512744fb5cfb49e8ecf4/node_modules/vitest/dist/chunks/rpc.MzXet3jl.js
|
|
10026
10026
|
const RealDate = Date;
|
|
10027
10027
|
let now = null;
|
|
10028
10028
|
var MockDate = class MockDate extends RealDate {
|
|
@@ -10987,7 +10987,7 @@ function raceWith(promise, other) {
|
|
|
10987
10987
|
}))]);
|
|
10988
10988
|
}
|
|
10989
10989
|
//#endregion
|
|
10990
|
-
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.
|
|
10990
|
+
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.14.0_vite@8.2.2__b2fde82745a4512744fb5cfb49e8ecf4/node_modules/vitest/dist/chunks/test.DNmyFkvJ.js
|
|
10991
10991
|
var fakeTimersSrc = {};
|
|
10992
10992
|
var global$1;
|
|
10993
10993
|
var hasRequiredGlobal;
|
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.2",
|
|
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,27 +35,27 @@
|
|
|
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.2",
|
|
39
39
|
"drizzle-orm": "^1.0.0-rc.5-ab785fc",
|
|
40
40
|
"fflate": "^0.8.2",
|
|
41
41
|
"pg": "^8.23.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@deepseek-ai/dsh-session-projection": "^0.1.
|
|
45
|
-
"@deepseek-ai/dsh-token-meter": "^0.1.
|
|
44
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.5-alpha.1",
|
|
45
|
+
"@deepseek-ai/dsh-token-meter": "^0.1.5-alpha.1",
|
|
46
46
|
"@types/pg": "^8.23.1",
|
|
47
47
|
"drizzle-kit": "1.0.0-rc.5-ab785fc"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
51
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
52
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
53
|
-
"@deepseek-ai/dsh-scope": "^0.1.
|
|
54
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
55
|
-
"@deepseek-ai/dsh-session-format": "^0.1.
|
|
56
|
-
"@deepseek-ai/dsh-session-format-catalog": "^0.1.
|
|
57
|
-
"@deepseek-ai/dsh-session-persistence": "^0.1.
|
|
58
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
51
|
+
"@deepseek-ai/dsh-invariants": "^0.1.5-alpha.1",
|
|
52
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-alpha.1",
|
|
53
|
+
"@deepseek-ai/dsh-scope": "^0.1.5-alpha.1",
|
|
54
|
+
"@deepseek-ai/dsh-session": "^0.1.5-alpha.1",
|
|
55
|
+
"@deepseek-ai/dsh-session-format": "^0.1.5-alpha.1",
|
|
56
|
+
"@deepseek-ai/dsh-session-format-catalog": "^0.1.5-alpha.1",
|
|
57
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.5-alpha.1",
|
|
58
|
+
"@deepseek-ai/dsh-settings": "^0.1.5-alpha.1",
|
|
59
59
|
"@deepseek-ai/schemastery": "^3.18.2"
|
|
60
60
|
},
|
|
61
61
|
"dsh": {
|
package/src/index.ts
CHANGED
|
@@ -560,6 +560,10 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
560
560
|
}
|
|
561
561
|
const log = await this.readLog(id, {}, options?.signal);
|
|
562
562
|
if (log === undefined) throw new SessionPersistenceNotFoundError(id);
|
|
563
|
+
// 读取视图修复必须先于校验:越界 replace(旧写入器重编号遗留的旧坐标)
|
|
564
|
+
// 降级/夹取、request/header 归一,否则上游事件校验 fail loud 拒绝整个
|
|
565
|
+
// 会话(load / open 是历史会话的加载入口,不能只依赖 handle.read)。
|
|
566
|
+
repairReadView(log.events);
|
|
563
567
|
// fail-closed:未知事件类型(非 ignorable)拒绝解释。
|
|
564
568
|
validateStoredEvents(log.meta, log.events);
|
|
565
569
|
return this.tracker.adopt(
|
|
@@ -584,6 +588,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
584
588
|
}
|
|
585
589
|
const log = await this.readLog(id, {}, options?.signal);
|
|
586
590
|
if (log === undefined) throw new SessionPersistenceNotFoundError(id);
|
|
591
|
+
repairReadView(log.events);
|
|
587
592
|
validateStoredEvents(log.meta, log.events);
|
|
588
593
|
// 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件坐标与
|
|
589
594
|
// 存储桥接行数不再相等——写打开时把迁移视图整体落库,使读写同坐标;
|
|
@@ -692,9 +697,9 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
692
697
|
}
|
|
693
698
|
|
|
694
699
|
/** 原样 append 落库(handle 已校验 contiguity;torn tail 先截断)。
|
|
695
|
-
* 与上游 JSONL 一致:ignorable
|
|
696
|
-
*
|
|
697
|
-
*
|
|
700
|
+
* 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验当前
|
|
701
|
+
* 格式形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
|
|
702
|
+
* 非当前格式(v0/v1/v2)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
|
|
698
703
|
async appendBatch(
|
|
699
704
|
meta: SessionHeader,
|
|
700
705
|
inheritedEventCount: SessionLogOffset,
|
|
@@ -703,8 +708,8 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
703
708
|
): Promise<boolean> {
|
|
704
709
|
await this.ready;
|
|
705
710
|
if (events.length === 0) return false;
|
|
706
|
-
//
|
|
707
|
-
//
|
|
711
|
+
// 写路径当前格式校验:与读路径同契约(validateStoredEvents),保证新入库
|
|
712
|
+
// 数据只能是当前格式形状。拷贝避免 adopt 替换污染调用方数组。
|
|
708
713
|
validateStoredEvents(meta, [...events]);
|
|
709
714
|
// fork 派生会话的 seed 复用源会话事件行(不复制);消费后清除。
|
|
710
715
|
const reuse = this.reuseEventIds.get(meta.id);
|
|
@@ -763,7 +768,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
763
768
|
/** 存储桥接行数(迁移链可能生成/合并事件,与 `events.length` 不同)。 */
|
|
764
769
|
storedCount: number;
|
|
765
770
|
|
|
766
|
-
/**
|
|
771
|
+
/** 是否经上游迁移链转换(非当前格式 → 当前格式)。 */
|
|
767
772
|
migrated: boolean;
|
|
768
773
|
}
|
|
769
774
|
| undefined
|
|
@@ -779,10 +784,11 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
779
784
|
? await this.backend.getEventRows(id)
|
|
780
785
|
: await this.backend.getEventRows(id, options.fromSeq);
|
|
781
786
|
signal?.throwIfAborted();
|
|
782
|
-
//
|
|
783
|
-
//
|
|
784
|
-
//
|
|
785
|
-
//
|
|
787
|
+
// 非当前格式(v0/v1/v2)历史数据:行重建为物理记录,经上游迁移链转
|
|
788
|
+
// 当前逻辑事件。迁移链自带 seq gap / torn tail 校验(strict recovery),
|
|
789
|
+
// 无需 scanRows。混合世代 log(旧写入器跨上游版本追加)不是任何单一已
|
|
790
|
+
// 发布格式,迁移链必然拒绝——回退为当前格式视图(header 版本归一 +
|
|
791
|
+
// 读取视图修复)。
|
|
786
792
|
if (isLegacyVersion(row.fVersion)) {
|
|
787
793
|
try {
|
|
788
794
|
const converted = convertLegacyRows(row, eventRows);
|
|
@@ -826,7 +832,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
826
832
|
}
|
|
827
833
|
|
|
828
834
|
/**
|
|
829
|
-
*
|
|
835
|
+
* 把迁移链读出的当前格式视图整体落库(非当前格式会话写打开时的一次性迁移)。
|
|
830
836
|
*
|
|
831
837
|
* 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件 seq 空间
|
|
832
838
|
* 与存储桥接行数不再相等;写路径以存储 head 为锚点重编号,二者不一致会让
|
|
@@ -927,8 +933,8 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
927
933
|
);
|
|
928
934
|
const row = await this.backend.getSession(id);
|
|
929
935
|
if (row === undefined) throw new SessionPersistenceNotFoundError(id);
|
|
930
|
-
//
|
|
931
|
-
//
|
|
936
|
+
// 非当前格式会话:meta 与继承前缀来自转换链(handle.header 已是转换后的
|
|
937
|
+
// 当前格式 header);当前格式会话用存储行。
|
|
932
938
|
if (isLegacyVersion(row.fVersion)) {
|
|
933
939
|
return {
|
|
934
940
|
meta: handle.header,
|
package/src/legacy.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// 非当前格式(v0/v1/v2)历史数据读取转换:RDB 行 → 物理记录 → 上游迁移链 →
|
|
2
|
+
// 当前逻辑事件。RDB 写路径从不持久化 sourceEventSeqs(读取时重计算),且
|
|
3
3
|
// delta(assistant/chunk)落盘被过滤——v1→v2 迁移对无 chunk 引用的
|
|
4
4
|
// assistant/message 走「无 pending 直接 emit」路径,生成 stream: [] 的 v2 事件。
|
|
5
5
|
|
|
@@ -11,39 +11,52 @@ import {
|
|
|
11
11
|
} from "@deepseek-ai/dsh-session";
|
|
12
12
|
import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
13
13
|
import type { EventRow, SessionRow } from "./backend.ts";
|
|
14
|
-
import { rowToMeta, scanRows } from "./log.ts";
|
|
14
|
+
import { repairRequestHeaders, rowToMeta, scanRows } from "./log.ts";
|
|
15
15
|
|
|
16
|
-
/** 重建 v0/v1 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
|
|
16
|
+
/** 重建 v0/v1/v2 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
|
|
17
17
|
function physicalHeader(row: SessionRow): Record<string, unknown> {
|
|
18
|
-
|
|
18
|
+
const common = {
|
|
19
19
|
type: "session",
|
|
20
20
|
version: row.fVersion,
|
|
21
21
|
id: row.fSessionId,
|
|
22
22
|
createdAt: row.fCreatedAt,
|
|
23
23
|
...(row.fCwd !== null ? { cwd: row.fCwd } : {}),
|
|
24
24
|
...(row.fParentSession !== null ? { parentSession: row.fParentSession } : {}),
|
|
25
|
-
...(row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {}),
|
|
26
25
|
...(row.fOrigin !== null ? { origin: row.fOrigin } : {}),
|
|
27
26
|
delegationDepth: row.fDelegationDepth ?? 0,
|
|
28
27
|
};
|
|
28
|
+
// v0/v1 物理 header 用 seedLength 表达继承前缀;v2 起改用 isSeeded,
|
|
29
|
+
// 前缀长度由 log 里的 session/end-seed 标记推导。
|
|
30
|
+
return row.fVersion < 2
|
|
31
|
+
? { ...common, ...(row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {}) }
|
|
32
|
+
: { ...common, isSeeded: row.fSeedLength !== null };
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
/** 重建一条 v0/v1 物理事件记录(桥接行 + 事件行 → 物理 JSON 对象)。 */
|
|
35
|
+
/** 重建一条 v0/v1/v2 物理事件记录(桥接行 + 事件行 → 物理 JSON 对象)。 */
|
|
32
36
|
function physicalEvent(row: EventRow): Record<string, unknown> {
|
|
37
|
+
const stored = JSON.parse(row.fData) as unknown;
|
|
38
|
+
// 两种存储形状:当前写入器落完整事件信封(与 JSONL 每行同构),schema v2
|
|
39
|
+
// 时代只落 data 部分。迁移链要的是 payload,故信封形状取 `data`。
|
|
40
|
+
const full =
|
|
41
|
+
typeof stored === "object" &&
|
|
42
|
+
stored !== null &&
|
|
43
|
+
!Array.isArray(stored) &&
|
|
44
|
+
typeof (stored as Record<string, unknown>)["type"] === "string" &&
|
|
45
|
+
"data" in (stored as Record<string, unknown>);
|
|
33
46
|
return {
|
|
34
47
|
type: row.fType,
|
|
35
48
|
seq: row.fSequence,
|
|
36
49
|
time: row.fCreatedAt,
|
|
37
|
-
data:
|
|
50
|
+
data: full ? (stored as Record<string, unknown>)["data"] : stored,
|
|
38
51
|
...(row.fSurfaceOp !== null ? { surfaceOp: JSON.parse(row.fSurfaceOp) as unknown } : {}),
|
|
39
52
|
};
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
/**
|
|
43
|
-
*
|
|
44
|
-
* @param row - 会话行(f_version <
|
|
56
|
+
* 把非当前格式(v0/v1/v2)行转换为当前逻辑事件与 header。
|
|
57
|
+
* @param row - 会话行(f_version < SESSION_FORMAT_VERSION)。
|
|
45
58
|
* @param eventRows - 已扫描保留的桥接行(稠密 seq,torn tail 已剔除)。
|
|
46
|
-
* @returns
|
|
59
|
+
* @returns 当前逻辑 header、继承前缀长度与事件。
|
|
47
60
|
* @throws 迁移链拒绝(格式损坏 / 无法迁移)时原样抛出。
|
|
48
61
|
*/
|
|
49
62
|
export function convertLegacyRows(
|
|
@@ -65,9 +78,9 @@ export function convertLegacyRows(
|
|
|
65
78
|
};
|
|
66
79
|
}
|
|
67
80
|
|
|
68
|
-
/**
|
|
81
|
+
/** 会话行是否携带非当前格式(f_version < SESSION_FORMAT_VERSION)数据。 */
|
|
69
82
|
export function isLegacyVersion(version: number): boolean {
|
|
70
|
-
return version <
|
|
83
|
+
return version < SESSION_FORMAT_VERSION;
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
/**
|
|
@@ -82,7 +95,7 @@ export function isLegacyVersion(version: number): boolean {
|
|
|
82
95
|
* 调用方须在此之后执行 `validateStoredEvents`:真正属于旧格式的类型
|
|
83
96
|
* (`assistant/chunk` / `compact/*` 等)仍会被 fail-loud 拒绝。
|
|
84
97
|
*
|
|
85
|
-
* @param row - 会话行(f_version <
|
|
98
|
+
* @param row - 会话行(f_version < SESSION_FORMAT_VERSION)。
|
|
86
99
|
* @param eventRows - 该会话全部桥接行(按 f_sequence 升序)。
|
|
87
100
|
* @returns 当前格式 header(version 归一到 SESSION_FORMAT_VERSION)、收缩后的
|
|
88
101
|
* 继承前缀长度、稠密事件与可选 torn tail 起点。
|
|
@@ -98,6 +111,11 @@ export function adoptLegacyRows(
|
|
|
98
111
|
tornFrom?: number;
|
|
99
112
|
} {
|
|
100
113
|
const { preserved, tornFrom } = scanRows(eventRows, 0);
|
|
114
|
+
// 旧格式 request/header 归一为当前格式:v3 起 system prompt 由 surface 上的
|
|
115
|
+
// system/message 承载,header 必须省略 system / 空 tools / 空 adapterDefaults
|
|
116
|
+
// (上游事件校验 fail loud)。归一必须发生在调用方 `validateStoredEvents`
|
|
117
|
+
// 之前,否则整个会话加载失败。
|
|
118
|
+
repairRequestHeaders(preserved);
|
|
101
119
|
return {
|
|
102
120
|
meta: { ...rowToMeta(row), version: SESSION_FORMAT_VERSION },
|
|
103
121
|
// 继承前缀不得超过事件数(历史 rewind 未收缩的损坏样式),上游 load 判损坏。
|
package/src/log.ts
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
|
+
import { SessionSeq } from "@deepseek-ai/dsh-session";
|
|
1
2
|
import type { SessionEvent, SessionHeader, SessionId, SurfaceOp } from "@deepseek-ai/dsh-session";
|
|
2
3
|
import type { SessionFormatEvent, SessionFormatHeader } from "@deepseek-ai/dsh-session-format";
|
|
3
4
|
import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
4
5
|
import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
|
|
5
6
|
import type { EventRow, SessionRow } from "./backend.ts";
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* 把桥接行列里的 replace surfaceOp 归一到当前字段名。
|
|
10
|
+
*
|
|
11
|
+
* v2 时代落库的 JSON 用 `start`/`end`,当前格式用 `startSeq`/`endSeq`;
|
|
12
|
+
* 混合世代回退路径直接采用存储行,必须在此归一,否则上游 v3 surface 校验
|
|
13
|
+
* 以「invalid replace surfaceOp」拒绝整个会话。非 replace 形状原样返回,
|
|
14
|
+
* 由读取视图修复决定降级。
|
|
15
|
+
*/
|
|
16
|
+
function normalizeSurfaceOp(value: unknown): SurfaceOp {
|
|
17
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
18
|
+
return value as SurfaceOp;
|
|
19
|
+
}
|
|
20
|
+
const record = value as Record<string, unknown>;
|
|
21
|
+
if (record["op"] !== "replace") return value as SurfaceOp;
|
|
22
|
+
const startSeq = record["startSeq"] ?? record["start"];
|
|
23
|
+
const endSeq = record["endSeq"] ?? record["end"];
|
|
24
|
+
if (typeof startSeq !== "number" || typeof endSeq !== "number") return value as SurfaceOp;
|
|
25
|
+
return { op: "replace", startSeq: SessionSeq(startSeq), endSeq: SessionSeq(endSeq) };
|
|
26
|
+
}
|
|
27
|
+
|
|
7
28
|
export function rowToMeta(row: SessionRow): SessionHeader {
|
|
8
29
|
if (!Number.isSafeInteger(row.fCreatedAt) || row.fCreatedAt < 0) {
|
|
9
30
|
throw new Error("stored session createdAt must be a non-negative safe integer");
|
|
@@ -76,7 +97,8 @@ export function sessionConflictRow(storage: SessionStorageMetadata): {
|
|
|
76
97
|
}
|
|
77
98
|
|
|
78
99
|
export function rowToEvent(row: EventRow): SessionEvent {
|
|
79
|
-
const surfaceOp =
|
|
100
|
+
const surfaceOp =
|
|
101
|
+
row.fSurfaceOp !== null ? normalizeSurfaceOp(JSON.parse(row.fSurfaceOp) as unknown) : undefined;
|
|
80
102
|
const record = JSON.parse(row.fData) as unknown;
|
|
81
103
|
// fData 形状判别:新写入的完整事件(含 ignorable 信封,与 JSONL 每行
|
|
82
104
|
// 同构)vs 旧 v2 库的纯 data 部分。完整事件必有 type/seq/time/data 四键。
|
|
@@ -107,7 +129,12 @@ export function rowToEvent(row: EventRow): SessionEvent {
|
|
|
107
129
|
} as SessionEvent;
|
|
108
130
|
}
|
|
109
131
|
|
|
110
|
-
const SURFACE_EVENT_TYPES = new Set([
|
|
132
|
+
const SURFACE_EVENT_TYPES = new Set([
|
|
133
|
+
"system/message",
|
|
134
|
+
"user/message",
|
|
135
|
+
"assistant/message",
|
|
136
|
+
"tool/result",
|
|
137
|
+
]);
|
|
111
138
|
|
|
112
139
|
const METERING_EVENT_TYPES = new Set(["compaction/summary", "compaction/prune"]);
|
|
113
140
|
|
|
@@ -126,12 +153,12 @@ const METERING_EVENT_TYPES = new Set(["compaction/summary", "compaction/prune"])
|
|
|
126
153
|
export function recomputeReplaceProvenance(events: SessionEvent[]): void {
|
|
127
154
|
for (let i = 0; i < events.length; i++) {
|
|
128
155
|
const event = events[i]!;
|
|
129
|
-
const raw = event as
|
|
156
|
+
const raw = event as unknown as { surfaceOp?: unknown; sourceEventSeqs?: number[] };
|
|
130
157
|
const op = raw.surfaceOp;
|
|
131
158
|
if (typeof op !== "object" || op === null || (op as { op?: string }).op !== "replace") {
|
|
132
159
|
continue;
|
|
133
160
|
}
|
|
134
|
-
const {
|
|
161
|
+
const { startSeq, endSeq } = op as { startSeq: number; endSeq: number };
|
|
135
162
|
const metering = i > 0 ? events[i - 1] : undefined;
|
|
136
163
|
const meteringData =
|
|
137
164
|
metering !== undefined && METERING_EVENT_TYPES.has(metering.type)
|
|
@@ -144,8 +171,8 @@ export function recomputeReplaceProvenance(events: SessionEvent[]): void {
|
|
|
144
171
|
const refs: number[] = [];
|
|
145
172
|
for (const candidate of events) {
|
|
146
173
|
if (
|
|
147
|
-
candidate.seq >=
|
|
148
|
-
candidate.seq <=
|
|
174
|
+
candidate.seq >= startSeq &&
|
|
175
|
+
candidate.seq <= endSeq &&
|
|
149
176
|
SURFACE_EVENT_TYPES.has(candidate.type)
|
|
150
177
|
) {
|
|
151
178
|
refs.push(candidate.seq);
|
|
@@ -242,8 +269,8 @@ export function findSurfaceRepairs(events: readonly SessionEvent[]): {
|
|
|
242
269
|
typeof op === "object" && op !== null && !Array.isArray(op)
|
|
243
270
|
? (op as Record<string, unknown>)
|
|
244
271
|
: undefined;
|
|
245
|
-
const start = replace?.["
|
|
246
|
-
const end = replace?.["
|
|
272
|
+
const start = replace?.["startSeq"];
|
|
273
|
+
const end = replace?.["endSeq"];
|
|
247
274
|
const shapeOk =
|
|
248
275
|
replace !== undefined &&
|
|
249
276
|
replace["op"] === "replace" &&
|
|
@@ -261,6 +288,10 @@ export function findSurfaceRepairs(events: readonly SessionEvent[]): {
|
|
|
261
288
|
if (clamped !== undefined) endIdx = nodes.indexOf(clamped);
|
|
262
289
|
}
|
|
263
290
|
const rangeOk = shapeOk && startIdx !== -1 && endIdx !== -1 && startIdx <= endIdx;
|
|
291
|
+
// assistant/message 的来源内嵌在 stream,上游禁止其携带 sourceEventSeqs,
|
|
292
|
+
// replace 的 provenance 因此永远无法满足 fold 校验——降级 append 是唯一
|
|
293
|
+
// 可加载形态。
|
|
294
|
+
const provenanceOk = event.type !== "assistant/message";
|
|
264
295
|
let rewriteOk = true;
|
|
265
296
|
if (rangeOk && event.type === "tool/result") {
|
|
266
297
|
const shadowed = nodes.slice(startIdx, endIdx + 1);
|
|
@@ -272,7 +303,7 @@ export function findSurfaceRepairs(events: readonly SessionEvent[]): {
|
|
|
272
303
|
original?.type === "tool/result" && toolResultRewriteContentOnly(original, event);
|
|
273
304
|
}
|
|
274
305
|
}
|
|
275
|
-
if (!rangeOk || !rewriteOk) {
|
|
306
|
+
if (!rangeOk || !rewriteOk || !provenanceOk) {
|
|
276
307
|
degradeToAppend.add(event.seq);
|
|
277
308
|
nodes.push(event.seq);
|
|
278
309
|
continue;
|
|
@@ -320,8 +351,12 @@ export function repairSurfaceOps(events: SessionEvent[]): void {
|
|
|
320
351
|
if (repairs.degradeToAppend.has(event.seq)) {
|
|
321
352
|
raw.surfaceOp = "append";
|
|
322
353
|
} else if (clamped !== undefined) {
|
|
323
|
-
const op = raw.surfaceOp as {
|
|
324
|
-
raw.surfaceOp = {
|
|
354
|
+
const op = raw.surfaceOp as { startSeq: number };
|
|
355
|
+
raw.surfaceOp = {
|
|
356
|
+
op: "replace",
|
|
357
|
+
startSeq: SessionSeq(op.startSeq),
|
|
358
|
+
endSeq: SessionSeq(clamped),
|
|
359
|
+
};
|
|
325
360
|
} else if (repairs.addAppendMarker.has(event.seq)) {
|
|
326
361
|
raw.surfaceOp = "append";
|
|
327
362
|
} else if (repairs.clearSurfaceOp.has(event.seq)) {
|
|
@@ -343,6 +378,34 @@ export function repairAssistantSettlement(events: SessionEvent[]): void {
|
|
|
343
378
|
}
|
|
344
379
|
}
|
|
345
380
|
|
|
381
|
+
/**
|
|
382
|
+
* 读取时把旧格式 `request/header` 归一为当前格式:v3 起 system prompt 由
|
|
383
|
+
* surface 上的 `system/message` 承载,header 必须省略 `system`,空 `tools` /
|
|
384
|
+
* `adapterDefaults` 也必须省略(上游 v3 事件校验 fail loud)。混合世代回退
|
|
385
|
+
* 视图直接采用存储行,不归一会让整个会话加载失败。system prompt 因此不再
|
|
386
|
+
* 进入模型请求;会话下次运行由 v3 的 system/message 机制重建。
|
|
387
|
+
*/
|
|
388
|
+
export function repairRequestHeaders(events: SessionEvent[]): void {
|
|
389
|
+
for (const event of events) {
|
|
390
|
+
if (event.type !== "request/header") continue;
|
|
391
|
+
const data = event.data as unknown as Record<string, unknown>;
|
|
392
|
+
const header = data["header"];
|
|
393
|
+
if (typeof header !== "object" || header === null || Array.isArray(header)) continue;
|
|
394
|
+
const record = header as Record<string, unknown>;
|
|
395
|
+
delete record["system"];
|
|
396
|
+
if (Array.isArray(record["tools"]) && record["tools"].length === 0) delete record["tools"];
|
|
397
|
+
const defaults = record["adapterDefaults"];
|
|
398
|
+
if (
|
|
399
|
+
typeof defaults === "object" &&
|
|
400
|
+
defaults !== null &&
|
|
401
|
+
!Array.isArray(defaults) &&
|
|
402
|
+
Object.keys(defaults).length === 0
|
|
403
|
+
) {
|
|
404
|
+
delete record["adapterDefaults"];
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
346
409
|
/**
|
|
347
410
|
* 把紧邻 replace 的 metering 事件的 `shadowedRange` / `shadowedSeqs` 对齐到
|
|
348
411
|
* replace 最终的稠密 range。
|
|
@@ -361,28 +424,108 @@ export function syncMeteringRanges(events: SessionEvent[]): void {
|
|
|
361
424
|
const event = events[i]!;
|
|
362
425
|
const op = (event as SessionEvent & { surfaceOp?: unknown }).surfaceOp;
|
|
363
426
|
if (typeof op !== "object" || op === null || (op as { op?: string }).op !== "replace") continue;
|
|
364
|
-
const {
|
|
427
|
+
const { startSeq, endSeq } = op as { startSeq: number; endSeq: number };
|
|
365
428
|
const data = metering.data as unknown as {
|
|
366
429
|
shadowedRange?: { start: number; end: number };
|
|
367
430
|
shadowedSeqs?: number[];
|
|
368
431
|
};
|
|
369
|
-
if (data.shadowedRange?.start ===
|
|
370
|
-
data.shadowedRange = { start, end };
|
|
432
|
+
if (data.shadowedRange?.start === startSeq && data.shadowedRange.end === endSeq) continue;
|
|
433
|
+
data.shadowedRange = { start: startSeq, end: endSeq };
|
|
371
434
|
data.shadowedSeqs = events
|
|
372
435
|
.filter(
|
|
373
436
|
(candidate) =>
|
|
374
|
-
candidate.seq >=
|
|
437
|
+
candidate.seq >= startSeq &&
|
|
438
|
+
candidate.seq <= endSeq &&
|
|
439
|
+
SURFACE_EVENT_TYPES.has(candidate.type),
|
|
375
440
|
)
|
|
376
441
|
.map((candidate) => candidate.seq);
|
|
377
442
|
}
|
|
378
443
|
}
|
|
379
444
|
|
|
445
|
+
const PTC_EVENT_RENAMES: Record<string, string> = {
|
|
446
|
+
"tool/code-dispatch-start": "tool/ptc-dispatch-start",
|
|
447
|
+
"tool/code-dispatch": "tool/ptc-dispatch",
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
/** 把消息的 `tools-code-mode` 插件来源改写为 `tools-ptc`(非该来源原样返回)。 */
|
|
451
|
+
function renamePtcMessageSource(message: unknown): unknown {
|
|
452
|
+
if (typeof message !== "object" || message === null || Array.isArray(message)) return message;
|
|
453
|
+
const record = message as Record<string, unknown>;
|
|
454
|
+
const source = record["source"];
|
|
455
|
+
if (typeof source !== "object" || source === null || Array.isArray(source)) return message;
|
|
456
|
+
const sourceRecord = source as Record<string, unknown>;
|
|
457
|
+
if (sourceRecord["kind"] !== "plugin" || sourceRecord["plugin"] !== "tools-code-mode") {
|
|
458
|
+
return message;
|
|
459
|
+
}
|
|
460
|
+
return { ...record, source: { ...sourceRecord, plugin: "tools-ptc" } };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** 宽类型事件视图:PTC 词汇归一涉及本包 SessionEventMap 之外的插件类型。 */
|
|
464
|
+
interface LegacyPtcEvent {
|
|
465
|
+
type: string;
|
|
466
|
+
data: unknown;
|
|
467
|
+
[key: string]: unknown;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* 读取时把 v2 时代的 PTC 词汇归一为当前词汇。
|
|
472
|
+
*
|
|
473
|
+
* 上游 v2→v3 迁移把 `tool/code-dispatch(-start)` 改名为
|
|
474
|
+
* `tool/ptc-dispatch(-start)`、把 `tools-code-mode` 插件来源改名为 `tools-ptc`、
|
|
475
|
+
* 把 agent preset 值 `code` 改名为 `ptc`。混合世代回退视图直接采用存储行,
|
|
476
|
+
* 不重命名会被上游 v3 校验以「unknown event type」拒绝整个会话。词汇改写
|
|
477
|
+
* 与上游迁移链的 renamePtcEvent 对齐。
|
|
478
|
+
*/
|
|
479
|
+
export function renameLegacyPtcEvents(events: SessionEvent[]): void {
|
|
480
|
+
for (let index = 0; index < events.length; index++) {
|
|
481
|
+
const event = events[index] as unknown as LegacyPtcEvent;
|
|
482
|
+
const renamedType = PTC_EVENT_RENAMES[event.type];
|
|
483
|
+
if (renamedType !== undefined) {
|
|
484
|
+
events[index] = { ...event, type: renamedType } as unknown as SessionEvent;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (event.type === "agent-preset/selected") {
|
|
488
|
+
const data = event.data as Record<string, unknown>;
|
|
489
|
+
if (data["agentPreset"] === "code") {
|
|
490
|
+
events[index] = {
|
|
491
|
+
...event,
|
|
492
|
+
data: { ...data, agentPreset: "ptc" },
|
|
493
|
+
} as unknown as SessionEvent;
|
|
494
|
+
}
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (event.type === "user/message") {
|
|
498
|
+
const renamed = renamePtcMessageSource(event.data);
|
|
499
|
+
if (renamed !== event.data) {
|
|
500
|
+
events[index] = { ...event, data: renamed } as unknown as SessionEvent;
|
|
501
|
+
}
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (event.type === "agent/inbox/spliced" || event.type === "session/title-llm-request") {
|
|
505
|
+
const data = event.data as Record<string, unknown>;
|
|
506
|
+
const key = event.type === "agent/inbox/spliced" ? "inserted" : "messages";
|
|
507
|
+
const list = data[key];
|
|
508
|
+
if (!Array.isArray(list)) continue;
|
|
509
|
+
const renamed = list.map(renamePtcMessageSource);
|
|
510
|
+
if (renamed.some((message, position) => message !== list[position])) {
|
|
511
|
+
events[index] = {
|
|
512
|
+
...event,
|
|
513
|
+
data: { ...data, [key]: renamed },
|
|
514
|
+
} as unknown as SessionEvent;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
380
520
|
/**
|
|
381
|
-
*
|
|
382
|
-
* provenance 重算 → 孤儿 inbox splice
|
|
521
|
+
* 读取视图修复总入口:形状补全(结算字段 / request header)→ PTC 词汇归一
|
|
522
|
+
* → surface 语义修复 → metering 对齐 → provenance 重算 → 孤儿 inbox splice
|
|
523
|
+
* 改写。全部只作用于内存视图,不落库。
|
|
383
524
|
*/
|
|
384
525
|
export function repairReadView(events: SessionEvent[]): void {
|
|
385
526
|
repairAssistantSettlement(events);
|
|
527
|
+
repairRequestHeaders(events);
|
|
528
|
+
renameLegacyPtcEvents(events);
|
|
386
529
|
repairSurfaceOps(events);
|
|
387
530
|
syncMeteringRanges(events);
|
|
388
531
|
recomputeReplaceProvenance(events);
|
package/src/testing/contract.ts
CHANGED
|
@@ -535,7 +535,7 @@ export function runPersistenceContract(name: string, make: () => Promise<Contrac
|
|
|
535
535
|
const m = meta("foreign-vocabulary");
|
|
536
536
|
const handle = await persistence.create(m);
|
|
537
537
|
await handle.append(oneTurnLog());
|
|
538
|
-
//
|
|
538
|
+
// 写路径当前格式校验:未知类型(非 ignorable)拒绝入库,批次不落库。
|
|
539
539
|
await expect(
|
|
540
540
|
handle.append([
|
|
541
541
|
{ type: "mystery/event", seq: SessionSeq(6), time: 7, data: { payload: true } },
|