@morlay/session-rdb 0.0.20 → 0.0.21-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -31
- package/dist/artifact.d.mts +25 -65
- package/dist/artifact.mjs +2 -2
- package/dist/{schema-BkQN5GyT.d.mts → backend-DpdtxYpz.d.mts} +346 -93
- package/dist/{branch-Co6xlbi0.mjs → branch-5JzX9rUq.mjs} +163 -52
- package/dist/deletion.d.mts +7 -0
- package/dist/deletion.mjs +2 -0
- package/dist/dist-vIVO6bA-.mjs +1524 -0
- package/dist/import.d.mts +4 -4
- package/dist/import.mjs +28 -11
- package/dist/index.d.mts +2 -3
- package/dist/index.mjs +4 -1631
- package/dist/{log-DO69NQnn.mjs → log-CnYct2Dv.mjs} +37 -82
- package/dist/{sqlite-DYExtbLo.mjs → sqlite-fpvm5Dzs.mjs} +205 -75
- package/dist/src-CWTWV7vx.mjs +1904 -0
- package/dist/storage.d.mts +14 -5
- package/dist/storage.mjs +2 -2
- package/dist/testing.d.mts +1 -26
- package/dist/testing.mjs +10503 -9614
- package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +14 -0
- package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +1309 -0
- package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +14 -0
- package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +1031 -0
- package/package.json +28 -29
- package/src/adapters/to-postgres.ts +1 -3
- package/src/adapters/to-sqlite.ts +0 -2
- package/src/adapters/types.ts +0 -3
- package/src/artifact.ts +0 -2
- package/src/backend.ts +31 -2
- package/src/branch.ts +223 -135
- package/src/deletion.ts +87 -0
- package/src/drizzle/postgres-v2.ts +0 -1
- package/src/drizzle/postgres-v3.ts +0 -1
- package/src/drizzle/sqlite-v2.ts +0 -1
- package/src/drizzle/sqlite-v3.ts +0 -1
- package/src/entities/v2/session-events.ts +0 -1
- package/src/entities/v3/event-usage.ts +25 -0
- package/src/entities/v3/events.ts +1 -3
- package/src/entities/v3/index.ts +4 -0
- package/src/entities/v3/session-events.ts +1 -2
- package/src/entities/v3/session-projcache-rows.ts +0 -8
- package/src/entities/v3/sessions.ts +3 -3
- package/src/entities/v3/storage-units.ts +0 -1
- package/src/entities/v3/workspace-sessions.ts +0 -5
- package/src/entities/v3/workspace-state.ts +0 -6
- package/src/entities/v3/workspaces.ts +0 -5
- package/src/export.ts +103 -0
- package/src/gc.ts +76 -0
- package/src/import-storages.ts +4 -37
- package/src/import.ts +54 -31
- package/src/index.ts +150 -114
- package/src/legacy.ts +4 -42
- package/src/log.ts +63 -106
- package/src/postgres.ts +249 -22
- package/src/schema.ts +6 -6
- package/src/session-query.ts +0 -4
- package/src/sqlite.ts +231 -55
- package/src/storage-takeover/index.ts +2 -28
- package/src/storage-takeover/projection-cache.ts +30 -134
- package/src/storage-takeover/repository.ts +20 -59
- package/src/storage-takeover/storage-backend.ts +3 -33
- package/src/storage-takeover/types.ts +14 -56
- package/src/storage.ts +0 -2
- package/src/testing/contract.ts +19 -50
- package/src/testing/coordinator-contract.ts +10 -25
- package/src/testing.ts +1 -4
- package/src/usage.ts +191 -0
- package/dist/index-D55G9n4k.d.mts +0 -271
- package/dist/magic-string.es-BgJoa-3K.mjs +0 -1017
package/src/legacy.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
// 非当前格式(v0/v1/v2)历史数据读取转换:RDB 行 → 物理记录 → 上游迁移链 →
|
|
2
|
-
// 当前逻辑事件。RDB 写路径从不持久化 sourceEventSeqs(读取时重计算),且
|
|
3
|
-
// delta(assistant/chunk)落盘被过滤——v1→v2 迁移对无 chunk 引用的
|
|
4
|
-
// assistant/message 走「无 pending 直接 emit」路径,生成 stream: [] 的 v2 事件。
|
|
5
|
-
|
|
6
1
|
import {
|
|
7
2
|
SESSION_FORMAT_VERSION,
|
|
8
3
|
type SessionEvent,
|
|
@@ -13,7 +8,6 @@ import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
|
13
8
|
import type { EventRow, SessionRow } from "./backend.ts";
|
|
14
9
|
import { repairRequestHeaders, rowToMeta, scanRows } from "./log.ts";
|
|
15
10
|
|
|
16
|
-
/** 重建 v0/v1/v2 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
|
|
17
11
|
function physicalHeader(row: SessionRow): Record<string, unknown> {
|
|
18
12
|
const common = {
|
|
19
13
|
type: "session",
|
|
@@ -25,18 +19,15 @@ function physicalHeader(row: SessionRow): Record<string, unknown> {
|
|
|
25
19
|
...(row.fOrigin !== null ? { origin: row.fOrigin } : {}),
|
|
26
20
|
delegationDepth: row.fDelegationDepth ?? 0,
|
|
27
21
|
};
|
|
28
|
-
|
|
29
|
-
// 前缀长度由 log 里的 session/end-seed 标记推导。
|
|
22
|
+
|
|
30
23
|
return row.fVersion < 2
|
|
31
24
|
? { ...common, ...(row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {}) }
|
|
32
25
|
: { ...common, isSeeded: row.fSeedLength !== null };
|
|
33
26
|
}
|
|
34
27
|
|
|
35
|
-
/** 重建一条 v0/v1/v2 物理事件记录(桥接行 + 事件行 → 物理 JSON 对象)。 */
|
|
36
28
|
function physicalEvent(row: EventRow): Record<string, unknown> {
|
|
37
29
|
const stored = JSON.parse(row.fData) as unknown;
|
|
38
|
-
|
|
39
|
-
// 时代只落 data 部分。迁移链要的是 payload,故信封形状取 `data`。
|
|
30
|
+
|
|
40
31
|
const full =
|
|
41
32
|
typeof stored === "object" &&
|
|
42
33
|
stored !== null &&
|
|
@@ -52,13 +43,6 @@ function physicalEvent(row: EventRow): Record<string, unknown> {
|
|
|
52
43
|
};
|
|
53
44
|
}
|
|
54
45
|
|
|
55
|
-
/**
|
|
56
|
-
* 把非当前格式(v0/v1/v2)行转换为当前逻辑事件与 header。
|
|
57
|
-
* @param row - 会话行(f_version < SESSION_FORMAT_VERSION)。
|
|
58
|
-
* @param eventRows - 已扫描保留的桥接行(稠密 seq,torn tail 已剔除)。
|
|
59
|
-
* @returns 当前逻辑 header、继承前缀长度与事件。
|
|
60
|
-
* @throws 迁移链拒绝(格式损坏 / 无法迁移)时原样抛出。
|
|
61
|
-
*/
|
|
62
46
|
export function convertLegacyRows(
|
|
63
47
|
row: SessionRow,
|
|
64
48
|
eventRows: readonly EventRow[],
|
|
@@ -78,29 +62,10 @@ export function convertLegacyRows(
|
|
|
78
62
|
};
|
|
79
63
|
}
|
|
80
64
|
|
|
81
|
-
/** 会话行是否携带非当前格式(f_version < SESSION_FORMAT_VERSION)数据。 */
|
|
82
65
|
export function isLegacyVersion(version: number): boolean {
|
|
83
66
|
return version < SESSION_FORMAT_VERSION;
|
|
84
67
|
}
|
|
85
68
|
|
|
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
69
|
export function adoptLegacyRows(
|
|
105
70
|
row: SessionRow,
|
|
106
71
|
eventRows: readonly EventRow[],
|
|
@@ -111,14 +76,11 @@ export function adoptLegacyRows(
|
|
|
111
76
|
tornFrom?: number;
|
|
112
77
|
} {
|
|
113
78
|
const { preserved, tornFrom } = scanRows(eventRows, 0);
|
|
114
|
-
|
|
115
|
-
// system/message 承载,header 必须省略 system / 空 tools / 空 adapterDefaults
|
|
116
|
-
// (上游事件校验 fail loud)。归一必须发生在调用方 `validateStoredEvents`
|
|
117
|
-
// 之前,否则整个会话加载失败。
|
|
79
|
+
|
|
118
80
|
repairRequestHeaders(preserved);
|
|
119
81
|
return {
|
|
120
82
|
meta: { ...rowToMeta(row), version: SESSION_FORMAT_VERSION },
|
|
121
|
-
|
|
83
|
+
|
|
122
84
|
inheritedEventCount: Math.min(row.fSeedLength ?? 0, preserved.length),
|
|
123
85
|
events: preserved,
|
|
124
86
|
...(tornFrom !== undefined ? { tornFrom } : {}),
|
package/src/log.ts
CHANGED
|
@@ -5,14 +5,6 @@ import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
|
5
5
|
import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
|
|
6
6
|
import type { EventRow, SessionRow } from "./backend.ts";
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* 把桥接行列里的 replace surfaceOp 归一到当前字段名。
|
|
10
|
-
*
|
|
11
|
-
* v2 时代落库的 JSON 用 `start`/`end`,当前格式用 `startSeq`/`endSeq`;
|
|
12
|
-
* 混合世代回退路径直接采用存储行,必须在此归一,否则上游 v3 surface 校验
|
|
13
|
-
* 以「invalid replace surfaceOp」拒绝整个会话。非 replace 形状原样返回,
|
|
14
|
-
* 由读取视图修复决定降级。
|
|
15
|
-
*/
|
|
16
8
|
function normalizeSurfaceOp(value: unknown): SurfaceOp {
|
|
17
9
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
18
10
|
return value as SurfaceOp;
|
|
@@ -100,8 +92,7 @@ export function rowToEvent(row: EventRow): SessionEvent {
|
|
|
100
92
|
const surfaceOp =
|
|
101
93
|
row.fSurfaceOp !== null ? normalizeSurfaceOp(JSON.parse(row.fSurfaceOp) as unknown) : undefined;
|
|
102
94
|
const record = JSON.parse(row.fData) as unknown;
|
|
103
|
-
|
|
104
|
-
// 同构)vs 旧 v2 库的纯 data 部分。完整事件必有 type/seq/time/data 四键。
|
|
95
|
+
|
|
105
96
|
const full =
|
|
106
97
|
typeof record === "object" &&
|
|
107
98
|
record !== null &&
|
|
@@ -111,8 +102,6 @@ export function rowToEvent(row: EventRow): SessionEvent {
|
|
|
111
102
|
typeof (record as Record<string, unknown>)["time"] === "number" &&
|
|
112
103
|
"data" in (record as Record<string, unknown>);
|
|
113
104
|
if (full) {
|
|
114
|
-
// seq/time 以桥接行/事件列为权威(rewind 截断后仍稠密连续);
|
|
115
|
-
// surfaceOp 走桥接行列,sourceEventSeqs 不落库(读取时重计算)。
|
|
116
105
|
return {
|
|
117
106
|
...(record as SessionEvent),
|
|
118
107
|
seq: row.fSequence,
|
|
@@ -138,18 +127,6 @@ const SURFACE_EVENT_TYPES = new Set([
|
|
|
138
127
|
|
|
139
128
|
const METERING_EVENT_TYPES = new Set(["compaction/summary", "compaction/prune"]);
|
|
140
129
|
|
|
141
|
-
/**
|
|
142
|
-
* 读取时重计算 replace 的 sourceEventSeqs(sourceEventSeqs 不落库)。
|
|
143
|
-
*
|
|
144
|
-
* 优先采用紧邻 metering 事件(compaction/summary | compaction/prune)的
|
|
145
|
-
* shadowedSeqs:它是压缩事务落库的**权威被遮蔽节点列表**(range 只是首尾
|
|
146
|
-
* 边界对,压缩竞态下可能漏掉并发落地的节点——range 数值扫描会漏掉这些
|
|
147
|
-
* 节点,使上游 assertProvenance 报 missing)。shadowedSeqs 已由 rowToEvent
|
|
148
|
-
* 重映射到稠密坐标,与 replace 的 surfaceOp range 同空间。
|
|
149
|
-
*
|
|
150
|
-
* 无紧邻 metering 事件时回退到 range 数值扫描(历史数据 / 非压缩 replace,
|
|
151
|
-
* 如 tool-result pruner 的旧样式),保证既有行为不变。
|
|
152
|
-
*/
|
|
153
130
|
export function recomputeReplaceProvenance(events: SessionEvent[]): void {
|
|
154
131
|
for (let i = 0; i < events.length; i++) {
|
|
155
132
|
const event = events[i]!;
|
|
@@ -278,19 +255,14 @@ export function findSurfaceRepairs(events: readonly SessionEvent[]): {
|
|
|
278
255
|
isEventSeqLike(end);
|
|
279
256
|
const startIdx = shapeOk ? nodes.indexOf(start as number) : -1;
|
|
280
257
|
let endIdx = shapeOk ? nodes.indexOf(end as number) : -1;
|
|
281
|
-
|
|
282
|
-
// 当前 surface 中,但紧邻 metering 事件的 shadowedSeqs 仍给出被遮蔽
|
|
283
|
-
// 节点数量。按数量从 start 起夹取到当前 surface 的同长区间,保住压缩
|
|
284
|
-
// 语义(否则只能降级 append,被压缩历史全部回到派生历史)。
|
|
258
|
+
|
|
285
259
|
let clamped: number | undefined;
|
|
286
260
|
if (shapeOk && startIdx !== -1 && endIdx === -1) {
|
|
287
261
|
clamped = clampReplaceEnd(events, index, nodes, startIdx);
|
|
288
262
|
if (clamped !== undefined) endIdx = nodes.indexOf(clamped);
|
|
289
263
|
}
|
|
290
264
|
const rangeOk = shapeOk && startIdx !== -1 && endIdx !== -1 && startIdx <= endIdx;
|
|
291
|
-
|
|
292
|
-
// replace 的 provenance 因此永远无法满足 fold 校验——降级 append 是唯一
|
|
293
|
-
// 可加载形态。
|
|
265
|
+
|
|
294
266
|
const provenanceOk = event.type !== "assistant/message";
|
|
295
267
|
let rewriteOk = true;
|
|
296
268
|
if (rangeOk && event.type === "tool/result") {
|
|
@@ -314,14 +286,6 @@ export function findSurfaceRepairs(events: readonly SessionEvent[]): {
|
|
|
314
286
|
return { degradeToAppend, addAppendMarker, clearSurfaceOp, clampEnd };
|
|
315
287
|
}
|
|
316
288
|
|
|
317
|
-
/**
|
|
318
|
-
* 夹取一个 end 落在旧坐标空间的 replace 的结尾。
|
|
319
|
-
*
|
|
320
|
-
* 仅当 replace 紧邻一个 metering 事件且其 `shadowedSeqs` 是权威数量时成立;
|
|
321
|
-
* 夹取点是从 `start` 起的第 `shadowedSeqs.length` 个当前 surface 节点(不足
|
|
322
|
-
* 则取当前 surface 末尾)。无法夹取(start 也不在当前 surface)时返回
|
|
323
|
-
* `undefined`,由调用方降级为 append。
|
|
324
|
-
*/
|
|
325
289
|
function clampReplaceEnd(
|
|
326
290
|
events: readonly SessionEvent[],
|
|
327
291
|
index: number,
|
|
@@ -365,11 +329,6 @@ export function repairSurfaceOps(events: SessionEvent[]): void {
|
|
|
365
329
|
}
|
|
366
330
|
}
|
|
367
331
|
|
|
368
|
-
/**
|
|
369
|
-
* 读取时补全 assistant 结算字段:旧写入器不落库 `stream`(v2 才把流式记录
|
|
370
|
-
* 嵌入消息),读回时缺失即补空数组——上游 seed 校验要求 turn/step/stream
|
|
371
|
-
* 三者齐备,缺失会让整个会话加载失败。
|
|
372
|
-
*/
|
|
373
332
|
export function repairAssistantSettlement(events: SessionEvent[]): void {
|
|
374
333
|
for (const event of events) {
|
|
375
334
|
if (event.type !== "assistant/message" && event.type !== "assistant/attempt") continue;
|
|
@@ -378,13 +337,6 @@ export function repairAssistantSettlement(events: SessionEvent[]): void {
|
|
|
378
337
|
}
|
|
379
338
|
}
|
|
380
339
|
|
|
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
340
|
export function repairRequestHeaders(events: SessionEvent[]): void {
|
|
389
341
|
for (const event of events) {
|
|
390
342
|
if (event.type !== "request/header") continue;
|
|
@@ -406,17 +358,6 @@ export function repairRequestHeaders(events: SessionEvent[]): void {
|
|
|
406
358
|
}
|
|
407
359
|
}
|
|
408
360
|
|
|
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
361
|
export function syncMeteringRanges(events: SessionEvent[]): void {
|
|
421
362
|
for (let i = 1; i < events.length; i++) {
|
|
422
363
|
const metering = events[i - 1]!;
|
|
@@ -447,7 +388,6 @@ const PTC_EVENT_RENAMES: Record<string, string> = {
|
|
|
447
388
|
"tool/code-dispatch": "tool/ptc-dispatch",
|
|
448
389
|
};
|
|
449
390
|
|
|
450
|
-
/** 把消息的 `tools-code-mode` 插件来源改写为 `tools-ptc`(非该来源原样返回)。 */
|
|
451
391
|
function renamePtcMessageSource(message: unknown): unknown {
|
|
452
392
|
if (typeof message !== "object" || message === null || Array.isArray(message)) return message;
|
|
453
393
|
const record = message as Record<string, unknown>;
|
|
@@ -460,22 +400,12 @@ function renamePtcMessageSource(message: unknown): unknown {
|
|
|
460
400
|
return { ...record, source: { ...sourceRecord, plugin: "tools-ptc" } };
|
|
461
401
|
}
|
|
462
402
|
|
|
463
|
-
/** 宽类型事件视图:PTC 词汇归一涉及本包 SessionEventMap 之外的插件类型。 */
|
|
464
403
|
interface LegacyPtcEvent {
|
|
465
404
|
type: string;
|
|
466
405
|
data: unknown;
|
|
467
406
|
[key: string]: unknown;
|
|
468
407
|
}
|
|
469
408
|
|
|
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
409
|
export function renameLegacyPtcEvents(events: SessionEvent[]): void {
|
|
480
410
|
for (let index = 0; index < events.length; index++) {
|
|
481
411
|
const event = events[index] as unknown as LegacyPtcEvent;
|
|
@@ -517,11 +447,6 @@ export function renameLegacyPtcEvents(events: SessionEvent[]): void {
|
|
|
517
447
|
}
|
|
518
448
|
}
|
|
519
449
|
|
|
520
|
-
/**
|
|
521
|
-
* 读取视图修复总入口:形状补全(结算字段 / request header)→ PTC 词汇归一
|
|
522
|
-
* → surface 语义修复 → metering 对齐 → provenance 重算 → 孤儿 inbox splice
|
|
523
|
-
* 改写。全部只作用于内存视图,不落库。
|
|
524
|
-
*/
|
|
525
450
|
export function repairReadView(events: SessionEvent[]): void {
|
|
526
451
|
repairAssistantSettlement(events);
|
|
527
452
|
repairRequestHeaders(events);
|
|
@@ -539,20 +464,10 @@ interface InboxSpliceLike {
|
|
|
539
464
|
inserted?: Array<{ id?: unknown }>;
|
|
540
465
|
}
|
|
541
466
|
|
|
542
|
-
/**
|
|
543
|
-
* 定位无法从空状态增量重放的 agent/inbox/spliced 事件(孤儿操作)。
|
|
544
|
-
*
|
|
545
|
-
* 上游 Inbox 每次构造都从会话起点重放全部 inbox splice,失败即拒绝整个
|
|
546
|
-
* 会话(resume / 编辑重放不可用)。rewind 截断历史轮次后若残留 splice 引用
|
|
547
|
-
* 已被截断的排队消息(插入被删、消费保留,或反之),重放时越界或重复——
|
|
548
|
-
* 无法独立重放。
|
|
549
|
-
*/
|
|
550
467
|
export function orphanInboxSpliceSeqs(events: readonly SessionEvent[]): Set<number> {
|
|
551
468
|
const inbox: Record<string, Array<{ id: string }>> = { "next-turn": [], "next-step": [] };
|
|
552
469
|
const orphan = new Set<number>();
|
|
553
470
|
for (const raw of events) {
|
|
554
|
-
// agent/inbox/spliced 属 dsh-agent 类型扩展,不在本包 SessionEventMap——
|
|
555
|
-
// 用宽类型 duck-type 读取,避免判别联合窄化到 never。
|
|
556
471
|
const event = raw as unknown as { type: string; seq: number; data: InboxSpliceLike };
|
|
557
472
|
if (event.type !== "agent/inbox/spliced") continue;
|
|
558
473
|
const { target, start, removedCount, inserted } = event.data;
|
|
@@ -601,7 +516,6 @@ export function orphanInboxSpliceSeqs(events: readonly SessionEvent[]): Set<numb
|
|
|
601
516
|
return orphan;
|
|
602
517
|
}
|
|
603
518
|
|
|
604
|
-
/** 内存修复:把孤儿 inbox splice 改写为 no-op(调用方负责持久化)。 */
|
|
605
519
|
export function repairOrphanInboxSplices(events: SessionEvent[]): void {
|
|
606
520
|
const orphan = orphanInboxSpliceSeqs(events);
|
|
607
521
|
if (orphan.size === 0) return;
|
|
@@ -622,8 +536,6 @@ export function scanRows(
|
|
|
622
536
|
rows: readonly EventRow[],
|
|
623
537
|
base = 0,
|
|
624
538
|
): { preserved: SessionEvent[]; tornFrom?: number } {
|
|
625
|
-
// Pass 1:解析每行 data;JSON 非法的行是洞(seq/type 列即使在 data 损坏
|
|
626
|
-
// 时也存在)。
|
|
627
539
|
interface Parsed {
|
|
628
540
|
ok: boolean;
|
|
629
541
|
event?: SessionEvent;
|
|
@@ -636,7 +548,6 @@ export function scanRows(
|
|
|
636
548
|
}
|
|
637
549
|
});
|
|
638
550
|
|
|
639
|
-
// 最后一个合法 turn/end 的索引——洞在闭合轮内一律是已提交损坏。
|
|
640
551
|
let lastTurnEnd = -1;
|
|
641
552
|
for (let i = parsed.length - 1; i >= 0; i--) {
|
|
642
553
|
if (parsed[i]?.ok && rows[i]?.fType === "turn/end") {
|
|
@@ -645,8 +556,6 @@ export function scanRows(
|
|
|
645
556
|
}
|
|
646
557
|
}
|
|
647
558
|
|
|
648
|
-
// 保留连续前缀(含完整的中断轮);最后一个已提交边界之前的洞抛错,
|
|
649
|
-
// 之后的洞停止(torn tail)。
|
|
650
559
|
const preserved: SessionEvent[] = [];
|
|
651
560
|
for (let i = 0; i < rows.length; i++) {
|
|
652
561
|
const p = parsed[i];
|
|
@@ -655,19 +564,18 @@ export function scanRows(
|
|
|
655
564
|
throw new Error(
|
|
656
565
|
`corrupt session log: unparsable committed event at seq ${rows[i]?.fSequence}`,
|
|
657
566
|
);
|
|
658
|
-
break;
|
|
567
|
+
break;
|
|
659
568
|
}
|
|
660
569
|
if (p.event.seq !== base + i) {
|
|
661
570
|
if (i <= lastTurnEnd)
|
|
662
571
|
throw new Error(
|
|
663
572
|
`corrupt session log: seq gap in committed region (expected ${base + i}, got ${p.event.seq})`,
|
|
664
573
|
);
|
|
665
|
-
break;
|
|
574
|
+
break;
|
|
666
575
|
}
|
|
667
576
|
preserved.push(p.event);
|
|
668
577
|
}
|
|
669
578
|
|
|
670
|
-
// 保留前缀之外的行是未提交的 torn tail;其首个 seq 是物理删除起点。
|
|
671
579
|
return preserved.length < rows.length
|
|
672
580
|
? { preserved, tornFrom: base + preserved.length }
|
|
673
581
|
: { preserved };
|
|
@@ -696,21 +604,70 @@ export function toJsonlArtifact(
|
|
|
696
604
|
return lines.join("\n");
|
|
697
605
|
}
|
|
698
606
|
|
|
699
|
-
/**
|
|
700
|
-
* 事件行 JSON → `session/title` 的标题文本。存储层写路径保证 `f_data` 是完整
|
|
701
|
-
* 事件 JSON;形状不符(或损坏)按「无标题」处理——标题是派生显示数据。
|
|
702
|
-
* @param data - `t_events.f_data` 原文。
|
|
703
|
-
* @returns 标题文本,或 `undefined`。
|
|
704
|
-
*/
|
|
705
607
|
export function titleOfEventData(data: string): string | undefined {
|
|
706
608
|
try {
|
|
707
|
-
// 当前世代:`{...envelope, data:{title}}`;旧世代(v0-v2)的 f_data 直接
|
|
708
|
-
// 平铺事件 data,title 在顶层。两者都读,避免 legacy 会话的标题被清空。
|
|
709
609
|
const value = JSON.parse(data) as { data?: { title?: unknown }; title?: unknown };
|
|
710
610
|
const title = value.data?.title ?? value.title;
|
|
711
611
|
return typeof title === "string" ? title : undefined;
|
|
712
612
|
} catch {
|
|
713
|
-
// 写路径已保证 JSON;此处只可能是损坏数据,读作无标题。
|
|
714
613
|
return undefined;
|
|
715
614
|
}
|
|
716
615
|
}
|
|
616
|
+
|
|
617
|
+
/** 用量行的列(`t_event_usage`)。 */
|
|
618
|
+
export interface EventUsageRow {
|
|
619
|
+
fEventId: string;
|
|
620
|
+
fCreatedAt: number;
|
|
621
|
+
fProvider: string | null;
|
|
622
|
+
fModel: string | null;
|
|
623
|
+
fInputTokens: number;
|
|
624
|
+
fOutputTokens: number;
|
|
625
|
+
fCacheReadTokens: number;
|
|
626
|
+
fReasoningTokens: number;
|
|
627
|
+
fTotalTokens: number;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function numericField(value: unknown): number {
|
|
631
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function textField(value: unknown): string | null {
|
|
635
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* 一条事件行的用量(只有带 `usage` 的 `assistant/message` 才有):写路径据此记录
|
|
640
|
+
* `t_event_usage`,统计因此不必逐行解析 JSON。两种 `f_data` 结构(带信封 / 老格式)都认。
|
|
641
|
+
* @param event - 待写入的事件行。
|
|
642
|
+
* @returns 用量行,或该事件没有用量时的 undefined。
|
|
643
|
+
*/
|
|
644
|
+
export function usageRowOf(event: {
|
|
645
|
+
fEventId: string;
|
|
646
|
+
fCreatedAt: number;
|
|
647
|
+
fType: string;
|
|
648
|
+
fData: string;
|
|
649
|
+
}): EventUsageRow | undefined {
|
|
650
|
+
if (event.fType !== "assistant/message") return undefined;
|
|
651
|
+
let parsed: Record<string, unknown>;
|
|
652
|
+
try {
|
|
653
|
+
parsed = JSON.parse(event.fData) as Record<string, unknown>;
|
|
654
|
+
} catch {
|
|
655
|
+
return undefined;
|
|
656
|
+
}
|
|
657
|
+
const envelope = (parsed["data"] ?? parsed) as Record<string, unknown>;
|
|
658
|
+
const usage = envelope["usage"];
|
|
659
|
+
if (typeof usage !== "object" || usage === null) return undefined;
|
|
660
|
+
const source = (envelope["message"] as { source?: Record<string, unknown> } | undefined)?.source;
|
|
661
|
+
const row: EventUsageRow = {
|
|
662
|
+
fEventId: event.fEventId,
|
|
663
|
+
fCreatedAt: event.fCreatedAt,
|
|
664
|
+
fProvider: textField(source?.["provider"]),
|
|
665
|
+
fModel: textField(source?.["model"]),
|
|
666
|
+
fInputTokens: numericField((usage as Record<string, unknown>)["inputTokens"]),
|
|
667
|
+
fOutputTokens: numericField((usage as Record<string, unknown>)["outputTokens"]),
|
|
668
|
+
fCacheReadTokens: numericField((usage as Record<string, unknown>)["cacheReadTokens"]),
|
|
669
|
+
fReasoningTokens: numericField((usage as Record<string, unknown>)["reasoningTokens"]),
|
|
670
|
+
fTotalTokens: numericField((usage as Record<string, unknown>)["totalTokens"]),
|
|
671
|
+
};
|
|
672
|
+
return row;
|
|
673
|
+
}
|