@morlay/session-rdb 0.0.16-alpha.0 → 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 +44 -1
- package/dist/artifact.mjs +3 -3
- package/dist/{import-CKrzjXVB.mjs → import-eBSoCrxl.mjs} +1 -1
- package/dist/import.d.mts +1 -1
- package/dist/import.mjs +1 -1
- package/dist/{index-CgGDHQSK.d.mts → index-DfS0PzGM.d.mts} +17 -3
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +105 -24
- package/dist/{log-DTJsxMud.mjs → log-BIuXv09P.mjs} +215 -12
- package/dist/{sqlite-DCy4VTD8.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 +96 -33
- package/src/legacy.ts +74 -13
- package/src/log.ts +254 -13
- package/src/testing/contract.ts +1 -1
package/src/legacy.ts
CHANGED
|
@@ -1,43 +1,62 @@
|
|
|
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
|
|
|
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 { repairRequestHeaders, rowToMeta, scanRows } from "./log.ts";
|
|
9
15
|
|
|
10
|
-
/** 重建 v0/v1 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
|
|
16
|
+
/** 重建 v0/v1/v2 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
|
|
11
17
|
function physicalHeader(row: SessionRow): Record<string, unknown> {
|
|
12
|
-
|
|
18
|
+
const common = {
|
|
13
19
|
type: "session",
|
|
14
20
|
version: row.fVersion,
|
|
15
21
|
id: row.fSessionId,
|
|
16
22
|
createdAt: row.fCreatedAt,
|
|
17
23
|
...(row.fCwd !== null ? { cwd: row.fCwd } : {}),
|
|
18
24
|
...(row.fParentSession !== null ? { parentSession: row.fParentSession } : {}),
|
|
19
|
-
...(row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {}),
|
|
20
25
|
...(row.fOrigin !== null ? { origin: row.fOrigin } : {}),
|
|
21
26
|
delegationDepth: row.fDelegationDepth ?? 0,
|
|
22
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 };
|
|
23
33
|
}
|
|
24
34
|
|
|
25
|
-
/** 重建一条 v0/v1 物理事件记录(桥接行 + 事件行 → 物理 JSON 对象)。 */
|
|
35
|
+
/** 重建一条 v0/v1/v2 物理事件记录(桥接行 + 事件行 → 物理 JSON 对象)。 */
|
|
26
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>);
|
|
27
46
|
return {
|
|
28
47
|
type: row.fType,
|
|
29
48
|
seq: row.fSequence,
|
|
30
49
|
time: row.fCreatedAt,
|
|
31
|
-
data:
|
|
50
|
+
data: full ? (stored as Record<string, unknown>)["data"] : stored,
|
|
32
51
|
...(row.fSurfaceOp !== null ? { surfaceOp: JSON.parse(row.fSurfaceOp) as unknown } : {}),
|
|
33
52
|
};
|
|
34
53
|
}
|
|
35
54
|
|
|
36
55
|
/**
|
|
37
|
-
*
|
|
38
|
-
* @param row - 会话行(f_version <
|
|
56
|
+
* 把非当前格式(v0/v1/v2)行转换为当前逻辑事件与 header。
|
|
57
|
+
* @param row - 会话行(f_version < SESSION_FORMAT_VERSION)。
|
|
39
58
|
* @param eventRows - 已扫描保留的桥接行(稠密 seq,torn tail 已剔除)。
|
|
40
|
-
* @returns
|
|
59
|
+
* @returns 当前逻辑 header、继承前缀长度与事件。
|
|
41
60
|
* @throws 迁移链拒绝(格式损坏 / 无法迁移)时原样抛出。
|
|
42
61
|
*/
|
|
43
62
|
export function convertLegacyRows(
|
|
@@ -59,9 +78,51 @@ export function convertLegacyRows(
|
|
|
59
78
|
};
|
|
60
79
|
}
|
|
61
80
|
|
|
62
|
-
/**
|
|
81
|
+
/** 会话行是否携带非当前格式(f_version < SESSION_FORMAT_VERSION)数据。 */
|
|
63
82
|
export function isLegacyVersion(version: number): boolean {
|
|
64
|
-
return version <
|
|
83
|
+
return version < SESSION_FORMAT_VERSION;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 迁移链拒绝后,把旧格式行直接当**当前格式**事件视图采用。
|
|
88
|
+
*
|
|
89
|
+
* 旧写入器只在建会话时落一次 header version,之后跟随上游演进继续追加
|
|
90
|
+
* 事件——同一个 log 因此可能是混合世代(v0 时代的行 + 新版本字段),不是
|
|
91
|
+
* 任何单一已发布格式,严格迁移链必然拒绝。这些行的数据形状(消息包装、
|
|
92
|
+
* 类型名)已经是当前格式,只需 header 版本归一到当前、按稠密 seq 读取;
|
|
93
|
+
* 事件数据的补全与 surface 修复由读取视图修复(repairReadView)负责。
|
|
94
|
+
*
|
|
95
|
+
* 调用方须在此之后执行 `validateStoredEvents`:真正属于旧格式的类型
|
|
96
|
+
* (`assistant/chunk` / `compact/*` 等)仍会被 fail-loud 拒绝。
|
|
97
|
+
*
|
|
98
|
+
* @param row - 会话行(f_version < SESSION_FORMAT_VERSION)。
|
|
99
|
+
* @param eventRows - 该会话全部桥接行(按 f_sequence 升序)。
|
|
100
|
+
* @returns 当前格式 header(version 归一到 SESSION_FORMAT_VERSION)、收缩后的
|
|
101
|
+
* 继承前缀长度、稠密事件与可选 torn tail 起点。
|
|
102
|
+
* @throws scanRows 的已提交区损坏错误(seq gap / 不可解析行)。
|
|
103
|
+
*/
|
|
104
|
+
export function adoptLegacyRows(
|
|
105
|
+
row: SessionRow,
|
|
106
|
+
eventRows: readonly EventRow[],
|
|
107
|
+
): {
|
|
108
|
+
meta: SessionHeader;
|
|
109
|
+
inheritedEventCount: number;
|
|
110
|
+
events: SessionEvent[];
|
|
111
|
+
tornFrom?: number;
|
|
112
|
+
} {
|
|
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);
|
|
119
|
+
return {
|
|
120
|
+
meta: { ...rowToMeta(row), version: SESSION_FORMAT_VERSION },
|
|
121
|
+
// 继承前缀不得超过事件数(历史 rewind 未收缩的损坏样式),上游 load 判损坏。
|
|
122
|
+
inheritedEventCount: Math.min(row.fSeedLength ?? 0, preserved.length),
|
|
123
|
+
events: preserved,
|
|
124
|
+
...(tornFrom !== undefined ? { tornFrom } : {}),
|
|
125
|
+
};
|
|
65
126
|
}
|
|
66
127
|
|
|
67
128
|
export type { SessionId };
|
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);
|
|
@@ -211,12 +238,15 @@ export function findSurfaceRepairs(events: readonly SessionEvent[]): {
|
|
|
211
238
|
degradeToAppend: Set<number>;
|
|
212
239
|
addAppendMarker: Set<number>;
|
|
213
240
|
clearSurfaceOp: Set<number>;
|
|
241
|
+
clampEnd: Map<number, number>;
|
|
214
242
|
} {
|
|
215
243
|
const nodes: number[] = [];
|
|
216
244
|
const degradeToAppend = new Set<number>();
|
|
217
245
|
const addAppendMarker = new Set<number>();
|
|
218
246
|
const clearSurfaceOp = new Set<number>();
|
|
219
|
-
|
|
247
|
+
const clampEnd = new Map<number, number>();
|
|
248
|
+
for (let index = 0; index < events.length; index++) {
|
|
249
|
+
const event = events[index]!;
|
|
220
250
|
const raw = event as SessionEvent & { surfaceOp?: unknown };
|
|
221
251
|
const op = raw.surfaceOp;
|
|
222
252
|
if (op === undefined) {
|
|
@@ -239,16 +269,29 @@ export function findSurfaceRepairs(events: readonly SessionEvent[]): {
|
|
|
239
269
|
typeof op === "object" && op !== null && !Array.isArray(op)
|
|
240
270
|
? (op as Record<string, unknown>)
|
|
241
271
|
: undefined;
|
|
242
|
-
const start = replace?.["
|
|
243
|
-
const end = replace?.["
|
|
272
|
+
const start = replace?.["startSeq"];
|
|
273
|
+
const end = replace?.["endSeq"];
|
|
244
274
|
const shapeOk =
|
|
245
275
|
replace !== undefined &&
|
|
246
276
|
replace["op"] === "replace" &&
|
|
247
277
|
isEventSeqLike(start) &&
|
|
248
278
|
isEventSeqLike(end);
|
|
249
279
|
const startIdx = shapeOk ? nodes.indexOf(start as number) : -1;
|
|
250
|
-
|
|
280
|
+
let endIdx = shapeOk ? nodes.indexOf(end as number) : -1;
|
|
281
|
+
// 旧写入器重编号事件后,存储的 range 端点可能落在旧坐标空间:end 不在
|
|
282
|
+
// 当前 surface 中,但紧邻 metering 事件的 shadowedSeqs 仍给出被遮蔽
|
|
283
|
+
// 节点数量。按数量从 start 起夹取到当前 surface 的同长区间,保住压缩
|
|
284
|
+
// 语义(否则只能降级 append,被压缩历史全部回到派生历史)。
|
|
285
|
+
let clamped: number | undefined;
|
|
286
|
+
if (shapeOk && startIdx !== -1 && endIdx === -1) {
|
|
287
|
+
clamped = clampReplaceEnd(events, index, nodes, startIdx);
|
|
288
|
+
if (clamped !== undefined) endIdx = nodes.indexOf(clamped);
|
|
289
|
+
}
|
|
251
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";
|
|
252
295
|
let rewriteOk = true;
|
|
253
296
|
if (rangeOk && event.type === "tool/result") {
|
|
254
297
|
const shadowed = nodes.slice(startIdx, endIdx + 1);
|
|
@@ -260,14 +303,36 @@ export function findSurfaceRepairs(events: readonly SessionEvent[]): {
|
|
|
260
303
|
original?.type === "tool/result" && toolResultRewriteContentOnly(original, event);
|
|
261
304
|
}
|
|
262
305
|
}
|
|
263
|
-
if (!rangeOk || !rewriteOk) {
|
|
306
|
+
if (!rangeOk || !rewriteOk || !provenanceOk) {
|
|
264
307
|
degradeToAppend.add(event.seq);
|
|
265
308
|
nodes.push(event.seq);
|
|
266
309
|
continue;
|
|
267
310
|
}
|
|
311
|
+
if (clamped !== undefined) clampEnd.set(event.seq, clamped);
|
|
268
312
|
nodes.splice(startIdx, endIdx - startIdx + 1, event.seq);
|
|
269
313
|
}
|
|
270
|
-
return { degradeToAppend, addAppendMarker, clearSurfaceOp };
|
|
314
|
+
return { degradeToAppend, addAppendMarker, clearSurfaceOp, clampEnd };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 夹取一个 end 落在旧坐标空间的 replace 的结尾。
|
|
319
|
+
*
|
|
320
|
+
* 仅当 replace 紧邻一个 metering 事件且其 `shadowedSeqs` 是权威数量时成立;
|
|
321
|
+
* 夹取点是从 `start` 起的第 `shadowedSeqs.length` 个当前 surface 节点(不足
|
|
322
|
+
* 则取当前 surface 末尾)。无法夹取(start 也不在当前 surface)时返回
|
|
323
|
+
* `undefined`,由调用方降级为 append。
|
|
324
|
+
*/
|
|
325
|
+
function clampReplaceEnd(
|
|
326
|
+
events: readonly SessionEvent[],
|
|
327
|
+
index: number,
|
|
328
|
+
nodes: readonly number[],
|
|
329
|
+
startIdx: number,
|
|
330
|
+
): number | undefined {
|
|
331
|
+
const metering = index > 0 ? events[index - 1] : undefined;
|
|
332
|
+
if (metering === undefined || !METERING_EVENT_TYPES.has(metering.type)) return undefined;
|
|
333
|
+
const shadowedSeqs = (metering.data as unknown as { shadowedSeqs?: unknown }).shadowedSeqs;
|
|
334
|
+
if (!Array.isArray(shadowedSeqs) || shadowedSeqs.length === 0) return undefined;
|
|
335
|
+
return nodes[Math.min(startIdx + shadowedSeqs.length - 1, nodes.length - 1)];
|
|
271
336
|
}
|
|
272
337
|
|
|
273
338
|
export function repairSurfaceOps(events: SessionEvent[]): void {
|
|
@@ -275,14 +340,23 @@ export function repairSurfaceOps(events: SessionEvent[]): void {
|
|
|
275
340
|
if (
|
|
276
341
|
repairs.degradeToAppend.size === 0 &&
|
|
277
342
|
repairs.addAppendMarker.size === 0 &&
|
|
278
|
-
repairs.clearSurfaceOp.size === 0
|
|
343
|
+
repairs.clearSurfaceOp.size === 0 &&
|
|
344
|
+
repairs.clampEnd.size === 0
|
|
279
345
|
) {
|
|
280
346
|
return;
|
|
281
347
|
}
|
|
282
348
|
for (const event of events) {
|
|
283
349
|
const raw = event as SessionEvent & { surfaceOp?: unknown };
|
|
350
|
+
const clamped = repairs.clampEnd.get(event.seq);
|
|
284
351
|
if (repairs.degradeToAppend.has(event.seq)) {
|
|
285
352
|
raw.surfaceOp = "append";
|
|
353
|
+
} else if (clamped !== undefined) {
|
|
354
|
+
const op = raw.surfaceOp as { startSeq: number };
|
|
355
|
+
raw.surfaceOp = {
|
|
356
|
+
op: "replace",
|
|
357
|
+
startSeq: SessionSeq(op.startSeq),
|
|
358
|
+
endSeq: SessionSeq(clamped),
|
|
359
|
+
};
|
|
286
360
|
} else if (repairs.addAppendMarker.has(event.seq)) {
|
|
287
361
|
raw.surfaceOp = "append";
|
|
288
362
|
} else if (repairs.clearSurfaceOp.has(event.seq)) {
|
|
@@ -291,6 +365,173 @@ export function repairSurfaceOps(events: SessionEvent[]): void {
|
|
|
291
365
|
}
|
|
292
366
|
}
|
|
293
367
|
|
|
368
|
+
/**
|
|
369
|
+
* 读取时补全 assistant 结算字段:旧写入器不落库 `stream`(v2 才把流式记录
|
|
370
|
+
* 嵌入消息),读回时缺失即补空数组——上游 seed 校验要求 turn/step/stream
|
|
371
|
+
* 三者齐备,缺失会让整个会话加载失败。
|
|
372
|
+
*/
|
|
373
|
+
export function repairAssistantSettlement(events: SessionEvent[]): void {
|
|
374
|
+
for (const event of events) {
|
|
375
|
+
if (event.type !== "assistant/message" && event.type !== "assistant/attempt") continue;
|
|
376
|
+
const data = event.data as unknown as Record<string, unknown>;
|
|
377
|
+
if (!Array.isArray(data["stream"])) data["stream"] = [];
|
|
378
|
+
}
|
|
379
|
+
}
|
|
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
|
+
|
|
409
|
+
/**
|
|
410
|
+
* 把紧邻 replace 的 metering 事件的 `shadowedRange` / `shadowedSeqs` 对齐到
|
|
411
|
+
* replace 最终的稠密 range。
|
|
412
|
+
*
|
|
413
|
+
* 旧写入器重编号事件后,metering 与 replace 一起落在旧坐标空间:二者数值
|
|
414
|
+
* 相等但与当前 surface 无关。夹取(或 range 本身已落在稠密空间而 metering
|
|
415
|
+
* 仍是旧值)后二者不再相等,而上游 token-meter 契约要求紧邻的 metering
|
|
416
|
+
* range 与 replace range 完全一致(否则报 no adjacent shadow price)。range
|
|
417
|
+
* 已一致时不改写——当前写入器保证一致,shadowedSeqs 的额外并发节点因此
|
|
418
|
+
* 原样保留。
|
|
419
|
+
*/
|
|
420
|
+
export function syncMeteringRanges(events: SessionEvent[]): void {
|
|
421
|
+
for (let i = 1; i < events.length; i++) {
|
|
422
|
+
const metering = events[i - 1]!;
|
|
423
|
+
if (!METERING_EVENT_TYPES.has(metering.type)) continue;
|
|
424
|
+
const event = events[i]!;
|
|
425
|
+
const op = (event as SessionEvent & { surfaceOp?: unknown }).surfaceOp;
|
|
426
|
+
if (typeof op !== "object" || op === null || (op as { op?: string }).op !== "replace") continue;
|
|
427
|
+
const { startSeq, endSeq } = op as { startSeq: number; endSeq: number };
|
|
428
|
+
const data = metering.data as unknown as {
|
|
429
|
+
shadowedRange?: { start: number; end: number };
|
|
430
|
+
shadowedSeqs?: number[];
|
|
431
|
+
};
|
|
432
|
+
if (data.shadowedRange?.start === startSeq && data.shadowedRange.end === endSeq) continue;
|
|
433
|
+
data.shadowedRange = { start: startSeq, end: endSeq };
|
|
434
|
+
data.shadowedSeqs = events
|
|
435
|
+
.filter(
|
|
436
|
+
(candidate) =>
|
|
437
|
+
candidate.seq >= startSeq &&
|
|
438
|
+
candidate.seq <= endSeq &&
|
|
439
|
+
SURFACE_EVENT_TYPES.has(candidate.type),
|
|
440
|
+
)
|
|
441
|
+
.map((candidate) => candidate.seq);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
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
|
+
|
|
520
|
+
/**
|
|
521
|
+
* 读取视图修复总入口:形状补全(结算字段 / request header)→ PTC 词汇归一
|
|
522
|
+
* → surface 语义修复 → metering 对齐 → provenance 重算 → 孤儿 inbox splice
|
|
523
|
+
* 改写。全部只作用于内存视图,不落库。
|
|
524
|
+
*/
|
|
525
|
+
export function repairReadView(events: SessionEvent[]): void {
|
|
526
|
+
repairAssistantSettlement(events);
|
|
527
|
+
repairRequestHeaders(events);
|
|
528
|
+
renameLegacyPtcEvents(events);
|
|
529
|
+
repairSurfaceOps(events);
|
|
530
|
+
syncMeteringRanges(events);
|
|
531
|
+
recomputeReplaceProvenance(events);
|
|
532
|
+
repairOrphanInboxSplices(events);
|
|
533
|
+
}
|
|
534
|
+
|
|
294
535
|
interface InboxSpliceLike {
|
|
295
536
|
target?: unknown;
|
|
296
537
|
start?: unknown;
|
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 } },
|