@morlay/session-rdb 0.1.10-alpha.0 → 0.1.10-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/README.md +18 -3
- package/dist/artifact.d.mts +118 -2
- package/dist/artifact.mjs +5 -3
- package/dist/{log-TYgd8Xkr.d.mts → backend-BKtJ5sO5.d.mts} +183 -284
- package/dist/backend-rdb-BDCntVDy.mjs +1341 -0
- package/dist/branch-CG1PUlO6.mjs +833 -0
- package/dist/deletion-SSX7IqU8.mjs +35 -0
- package/dist/deletion.d.mts +1 -1
- package/dist/deletion.mjs +2 -2
- package/dist/export-CfJQjQfr.mjs +123 -0
- package/dist/export.d.mts +1 -1
- package/dist/export.mjs +31 -55
- package/dist/import.d.mts +1 -1
- package/dist/import.mjs +31 -76
- package/dist/index-CUD2ZpTA.d.mts +71 -0
- package/dist/index.d.mts +20 -2
- package/dist/index.mjs +1561 -4
- package/dist/open-YoyD_kah.mjs +561 -0
- package/dist/organize.d.mts +9 -0
- package/dist/organize.mjs +30 -0
- package/dist/route-DpfY0zq-.mjs +114 -0
- package/dist/storage.d.mts +41 -48
- package/dist/storage.mjs +3 -1
- package/dist/testing.d.mts +9 -1
- package/dist/testing.mjs +25 -4
- package/dist/usage-row-BQQM-_t6.mjs +36 -0
- package/dist/usage.d.mts +1 -1
- package/dist/usage.mjs +7 -40
- package/dist/write-guard-CYrY58GM.mjs +174 -0
- package/package.json +10 -14
- package/src/artifact.ts +4 -1
- package/src/backend-rdb.ts +331 -0
- package/src/backend.ts +32 -15
- package/src/{branch.ts → branch/index.ts} +109 -110
- package/src/branch/port.ts +62 -0
- package/src/branch/semantics.ts +69 -0
- package/src/db/open.ts +180 -0
- package/src/db/stats.ts +449 -0
- package/src/deletion-error.ts +17 -0
- package/src/deletion.ts +19 -72
- package/src/entities/index.ts +16 -1
- package/src/entities/v3/event-usage.ts +3 -3
- package/src/entities/v3/events.ts +3 -3
- package/src/entities/v3/index.ts +4 -16
- package/src/entities/v3/persistence-state.ts +3 -3
- package/src/entities/v3/schema-meta.ts +3 -3
- package/src/entities/v3/session-counts.ts +3 -3
- package/src/entities/v3/session-events.ts +3 -3
- package/src/entities/v3/session-projcache-rows.ts +3 -3
- package/src/entities/v3/session-usage.ts +3 -3
- package/src/entities/v3/sessions.ts +3 -3
- package/src/entities/v3/storage-records.ts +17 -0
- package/src/entities/v3/storage-units.ts +3 -3
- package/src/{schema.ts → event-shape.ts} +0 -46
- package/src/export.ts +32 -78
- package/src/handles.ts +349 -0
- package/src/http/route.ts +178 -0
- package/src/import-storages.ts +5 -2
- package/src/import.ts +63 -125
- package/src/index.ts +15 -1666
- package/src/legacy.ts +3 -1
- package/src/live-routing.ts +205 -0
- package/src/log/export.ts +84 -0
- package/src/{log.ts → log/repair.ts} +1 -268
- package/src/log/rows.ts +107 -0
- package/src/log/usage-row.ts +64 -0
- package/src/migrations.ts +196 -0
- package/src/organize.ts +38 -0
- package/src/read-path.ts +190 -0
- package/src/routes.ts +2 -1
- package/src/rows.ts +25 -77
- package/src/session-ops.ts +444 -0
- package/src/session-store.ts +675 -0
- package/src/stats-fold.ts +44 -0
- package/src/storage-takeover/index.ts +2 -5
- package/src/storage-takeover/legacy-domain-migration.ts +118 -0
- package/src/storage-takeover/projection-cache.ts +122 -71
- package/src/storage-takeover/read-model.ts +189 -0
- package/src/storage-takeover/repository.ts +236 -442
- package/src/storage-takeover/storage-backend.ts +44 -159
- package/src/storage-takeover/types.ts +45 -19
- package/src/storage-takeover/workspace-documents.ts +65 -0
- package/src/storage.ts +4 -2
- package/src/testing.ts +21 -1
- package/src/usage.ts +11 -65
- package/src/version.ts +13 -0
- package/src/write-path.ts +286 -0
- package/dist/branch-CoPCRKer.mjs +0 -401
- package/dist/deletion-CWl89TIR.mjs +0 -2249
- package/dist/gc.d.mts +0 -4
- package/dist/gc.mjs +0 -48
- package/dist/log-CcWy46VE.mjs +0 -558
- package/dist/routes-hZjTcX_R.mjs +0 -9
- package/dist/sqlite-BNRd1aqR.mjs +0 -1778
- package/drizzle/postgres/20260908072805_v2_initial/migration.sql +0 -58
- package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +0 -686
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +0 -1
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +0 -673
- package/drizzle/postgres/20260910120001_v3_storage_tables/migration.sql +0 -65
- package/drizzle/postgres/20260910120001_v3_storage_tables/snapshot.json +0 -1183
- package/drizzle/postgres/20260910130001_v3_session_title_backfill/migration.sql +0 -10
- package/drizzle/postgres/20260910130001_v3_session_title_backfill/snapshot.json +0 -1183
- package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +0 -14
- package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +0 -1309
- package/drizzle/postgres/20260921075900_v3_agent_preset/migration.sql +0 -1
- package/drizzle/postgres/20260922090001_v3_pinned_seq/migration.sql +0 -2
- package/drizzle/postgres/20260922093001_v3_last_event_at/migration.sql +0 -10
- package/drizzle/postgres/20260923120000_v3_usage_materialized/migration.sql +0 -48
- package/drizzle/postgres/20261008103001_v3_schedule_tasks/migration.sql +0 -11
- package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +0 -53
- package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +0 -492
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +0 -6
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +0 -513
- package/drizzle/sqlite/20260910120000_v3_storage_tables/migration.sql +0 -66
- package/drizzle/sqlite/20260910120000_v3_storage_tables/snapshot.json +0 -905
- package/drizzle/sqlite/20260910130000_v3_session_title_backfill/migration.sql +0 -13
- package/drizzle/sqlite/20260910130000_v3_session_title_backfill/snapshot.json +0 -905
- package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +0 -14
- package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +0 -1031
- package/drizzle/sqlite/20260921075855_v3_agent_preset/migration.sql +0 -7
- package/drizzle/sqlite/20260921075855_v3_agent_preset/snapshot.json +0 -1041
- package/drizzle/sqlite/20260922090000_v3_pinned_seq/migration.sql +0 -4
- package/drizzle/sqlite/20260922093000_v3_last_event_at/migration.sql +0 -13
- package/drizzle/sqlite/20260923120000_v3_usage_materialized/migration.sql +0 -48
- package/drizzle/sqlite/20261008103000_v3_schedule_tasks/migration.sql +0 -14
- package/src/adapters/index.ts +0 -12
- package/src/adapters/to-postgres.ts +0 -93
- package/src/adapters/to-sqlite.ts +0 -83
- package/src/adapters/types.ts +0 -44
- package/src/drizzle/postgres-v2.ts +0 -10
- package/src/drizzle/postgres-v3.ts +0 -18
- package/src/drizzle/sqlite-v2.ts +0 -9
- package/src/drizzle/sqlite-v3.ts +0 -18
- package/src/entities/v2/index.ts +0 -20
- package/src/entities/v2/session-events.ts +0 -10
- package/src/entities/v3/schedule-tasks.ts +0 -16
- package/src/entities/v3/workspace-sessions.ts +0 -19
- package/src/entities/v3/workspace-state.ts +0 -12
- package/src/entities/v3/workspaces.ts +0 -14
- package/src/gc.ts +0 -77
- package/src/postgres.ts +0 -1015
- package/src/sqlite.ts +0 -1096
package/src/legacy.ts
CHANGED
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
} from "@deepseek-ai/dsh-session";
|
|
7
7
|
import { createSessionFormatCatalogWithChildren } from "@deepseek-ai/dsh-session-format-catalog";
|
|
8
8
|
import type { EventRow, SessionRow } from "./backend.ts";
|
|
9
|
-
import { normalizeToCurrentShape, repairRequestHeaders
|
|
9
|
+
import { normalizeToCurrentShape, repairRequestHeaders } from "./log/repair.ts";
|
|
10
|
+
import { rowToMeta } from "./log/rows.ts";
|
|
11
|
+
import { scanRows } from "./log/export.ts";
|
|
10
12
|
|
|
11
13
|
function physicalHeader(row: SessionRow): Record<string, unknown> {
|
|
12
14
|
const common = {
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
import { assertVersion } from "@deepseek-ai/dsh-session-persistence";
|
|
3
|
+
import type { Session, SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
|
|
4
|
+
import type { SessionHandle } from "@deepseek-ai/dsh-session-persistence";
|
|
5
|
+
import type { SessionPersistenceRdb } from "./session-store.ts";
|
|
6
|
+
import { liveEventsOf } from "./branch/index.ts";
|
|
7
|
+
import { repairReadView } from "./log/repair.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* live 会话的写路由:把 `session/created|event|flush|disposed` 四个订阅与四张内存表收在一处。
|
|
11
|
+
*
|
|
12
|
+
* 会话级的「缓冲—句柄—失败终态」都收在这里;装配点是 `session-store.ts` 的构造函数(`new LiveRouting(ctx, this)`),
|
|
13
|
+
* `forget(id)` 在删除会话 / 回收孤儿会话时调用。句柄自己的批量窗口在 `handles.ts`。
|
|
14
|
+
*/
|
|
15
|
+
/** live 会话的写句柄:`SessionHandle` 加上批量窗口的入口。 */
|
|
16
|
+
interface LiveHandle extends SessionHandle {
|
|
17
|
+
enqueueLive(event: SessionEvent, reportBackgroundFailure: (error: unknown) => void): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class LiveRouting {
|
|
21
|
+
private readonly buffers = new Map<SessionId, SessionEvent[]>();
|
|
22
|
+
private readonly ready = new Map<SessionId, Promise<void>>();
|
|
23
|
+
// 持久化初始化失败是**终态**(cwd / 继承前缀冲突、id 碰撞、后端打不开都不自愈):记账一条 error、
|
|
24
|
+
// 丢掉已缓冲的事件,之后该会话的事件不再进内存缓冲(否则表现为「聊了一整轮、重启后全丢,
|
|
25
|
+
// 内存里还留一份无界副本」),flush 把失败抛回上游。
|
|
26
|
+
private readonly failures = new Map<SessionId, Error>();
|
|
27
|
+
private readonly dropWarned = new Set<SessionId>();
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
private readonly ctx: Context,
|
|
31
|
+
private readonly persistence: SessionPersistenceRdb,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
/** 丢掉一个会话的所有 live 记账(删除会话、孤儿会话回收时)。 */
|
|
35
|
+
forget(id: SessionId): void {
|
|
36
|
+
this.buffers.delete(id);
|
|
37
|
+
this.ready.delete(id);
|
|
38
|
+
this.failures.delete(id);
|
|
39
|
+
this.dropWarned.delete(id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
install(): void {
|
|
43
|
+
const ctx = this.ctx;
|
|
44
|
+
ctx.on("session/created", (session: Session) => {
|
|
45
|
+
this.buffers.set(session.id, []);
|
|
46
|
+
const ready = this.ensureHandle(session);
|
|
47
|
+
this.ready.set(session.id, ready);
|
|
48
|
+
this.watchReady(session, ready);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
for (const session of ctx.sessions.list()) {
|
|
52
|
+
this.buffers.set(session.id, []);
|
|
53
|
+
const ready = this.ensureHandle(session);
|
|
54
|
+
this.ready.set(session.id, ready);
|
|
55
|
+
this.watchReady(session, ready);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
ctx.on("session/event", (session: Session, event: SessionEvent) => {
|
|
59
|
+
const handle = this.persistence.tracker.writerOf(session.id);
|
|
60
|
+
if (handle !== undefined) {
|
|
61
|
+
handle.enqueueLive(event, (error) => {
|
|
62
|
+
ctx.logger.warn(
|
|
63
|
+
`session-rdb: background write for session "${session.id}" failed (buffered events retained): ${String(error)}`,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (this.failures.has(session.id)) {
|
|
69
|
+
if (!this.dropWarned.has(session.id)) {
|
|
70
|
+
this.dropWarned.add(session.id);
|
|
71
|
+
ctx.logger.warn(
|
|
72
|
+
`session-rdb: dropping events for live session "${session.id}" because persistence failed earlier`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
this.buffers.get(session.id)?.push(structuredClone(event));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
ctx.on("session/flush", (session: Session) => {
|
|
81
|
+
const failure = this.failures.get(session.id);
|
|
82
|
+
if (failure !== undefined) return Promise.reject(failure);
|
|
83
|
+
const handle = this.persistence.tracker.writerOf(session.id);
|
|
84
|
+
if (handle === undefined) {
|
|
85
|
+
const ready = this.ready.get(session.id);
|
|
86
|
+
if (ready === undefined) return undefined;
|
|
87
|
+
return ready.then(() => {
|
|
88
|
+
const settled = this.persistence.tracker.writerOf(session.id);
|
|
89
|
+
if (settled === undefined) return undefined;
|
|
90
|
+
return settled.drainLive().then(() => settled.flush());
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return handle.drainLive().then(() => handle.flush());
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
ctx.on("session/disposed", (session: Session) => {
|
|
97
|
+
const ready = this.ready.get(session.id);
|
|
98
|
+
this.forget(session.id);
|
|
99
|
+
const closeHandle = (): void => {
|
|
100
|
+
const handle = this.persistence.tracker.writerOf(session.id);
|
|
101
|
+
if (handle === undefined) return;
|
|
102
|
+
handle.close().catch((error: unknown) => {
|
|
103
|
+
ctx.logger.warn(
|
|
104
|
+
`session-rdb: final drain for session "${session.id}" failed: ${String(error)}`,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
if (ready === undefined) {
|
|
109
|
+
closeHandle();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
void ready.then(closeHandle, closeHandle);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
ctx.effect(
|
|
116
|
+
() => async () => {
|
|
117
|
+
await Promise.allSettled(this.ready.values());
|
|
118
|
+
this.buffers.clear();
|
|
119
|
+
this.ready.clear();
|
|
120
|
+
this.failures.clear();
|
|
121
|
+
this.dropWarned.clear();
|
|
122
|
+
await this.persistence.tracker.closeAll();
|
|
123
|
+
await this.persistence.close();
|
|
124
|
+
},
|
|
125
|
+
`${this.persistence.name} open handles`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private watchReady(session: Session, ready: Promise<void>): void {
|
|
130
|
+
void ready.catch((error: unknown) => {
|
|
131
|
+
const failure =
|
|
132
|
+
error instanceof Error
|
|
133
|
+
? error
|
|
134
|
+
: new Error(
|
|
135
|
+
typeof error === "string"
|
|
136
|
+
? error
|
|
137
|
+
: `live session "${session.id}" persistence failed`,
|
|
138
|
+
);
|
|
139
|
+
this.failures.set(session.id, failure);
|
|
140
|
+
this.buffers.delete(session.id);
|
|
141
|
+
this.ctx.logger.error(
|
|
142
|
+
`session-rdb: live session "${session.id}" persistence unavailable (${failure.message}); its events will not be persisted`,
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private async ensureHandle(session: Session): Promise<void> {
|
|
148
|
+
const persistence = this.persistence;
|
|
149
|
+
const id = session.header.id;
|
|
150
|
+
if (persistence.tracker.writerOf(id) !== undefined) return;
|
|
151
|
+
await persistence.ready;
|
|
152
|
+
const stored = await persistence.readLog(id, {});
|
|
153
|
+
let handle: LiveHandle;
|
|
154
|
+
if (stored === undefined) {
|
|
155
|
+
handle = (await persistence.create(session.header, {
|
|
156
|
+
inheritedEventCount: session.inheritedEventCount,
|
|
157
|
+
})) as LiveHandle;
|
|
158
|
+
const seed = liveEventsOf(session);
|
|
159
|
+
if (seed.length > 0) await handle.append(seed);
|
|
160
|
+
} else {
|
|
161
|
+
if (stored.meta.cwd !== session.header.cwd) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`session "${id}" is already persisted at a different cwd (persisted: ${String(stored.meta.cwd)}, live: ${String(session.header.cwd)}) (id collision)`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (stored.inheritedEventCount !== session.inheritedEventCount) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`session "${id}" is already persisted with a different inherited event count (id collision)`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
assertVersion(stored.meta);
|
|
172
|
+
|
|
173
|
+
repairReadView(stored.events);
|
|
174
|
+
const seed = liveEventsOf(session);
|
|
175
|
+
if (!seedCoversPrefix(seed, stored.events)) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`session "${id}" already has a persisted log on disk that does not match this live session (id collision)`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
handle = (await persistence.open(id, "write")) as LiveHandle;
|
|
181
|
+
|
|
182
|
+
const suffix = seed.slice(stored.events.length);
|
|
183
|
+
if (suffix.length > 0) await handle.append(suffix);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const buffered = this.buffers.get(id);
|
|
187
|
+
if (buffered !== undefined && buffered.length > 0) {
|
|
188
|
+
this.buffers.set(id, []);
|
|
189
|
+
for (const event of buffered) handle.enqueueLive(event, () => {});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function seedCoversPrefix(
|
|
195
|
+
seed: readonly SessionEvent[],
|
|
196
|
+
prefix: readonly SessionEvent[],
|
|
197
|
+
): boolean {
|
|
198
|
+
return (
|
|
199
|
+
prefix.length <= seed.length &&
|
|
200
|
+
prefix.every((event, index) => {
|
|
201
|
+
const seedEvent = seed[index];
|
|
202
|
+
return seedEvent !== undefined && JSON.stringify(seedEvent) === JSON.stringify(event);
|
|
203
|
+
})
|
|
204
|
+
);
|
|
205
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { SessionEvent, SessionHeader } from "@deepseek-ai/dsh-session";
|
|
2
|
+
import type { SessionFormatEvent, SessionFormatHeader } from "@deepseek-ai/dsh-session-format";
|
|
3
|
+
import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
4
|
+
import type { EventRow } from "../backend.ts";
|
|
5
|
+
import { rowToEvent } from "./rows.ts";
|
|
6
|
+
|
|
7
|
+
export function scanRows(
|
|
8
|
+
rows: readonly EventRow[],
|
|
9
|
+
base = 0,
|
|
10
|
+
): { preserved: SessionEvent[]; tornFrom?: number } {
|
|
11
|
+
// 已提交区(最后一个 `turn/end` 及之前)必须完整:坏行/断号只有在它之后才允许当作撕裂尾部丢掉。
|
|
12
|
+
// 倒序只为找到这个边界,先按行类型筛(绝大多数行直接跳过),再解析候选行确认它本身可读。
|
|
13
|
+
let lastTurnEnd = -1;
|
|
14
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
15
|
+
if (rows[i]?.fType !== "turn/end") continue;
|
|
16
|
+
try {
|
|
17
|
+
rowToEvent(rows[i] as EventRow);
|
|
18
|
+
lastTurnEnd = i;
|
|
19
|
+
break;
|
|
20
|
+
} catch {
|
|
21
|
+
/* 这个 turn/end 自身坏掉:继续往前找上一个可读的边界。 */
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const preserved: SessionEvent[] = [];
|
|
26
|
+
for (let i = 0; i < rows.length; i++) {
|
|
27
|
+
let event: SessionEvent;
|
|
28
|
+
try {
|
|
29
|
+
event = rowToEvent(rows[i] as EventRow);
|
|
30
|
+
} catch {
|
|
31
|
+
if (i <= lastTurnEnd)
|
|
32
|
+
throw new Error(
|
|
33
|
+
`corrupt session log: unparsable committed event at seq ${rows[i]?.fSequence}`,
|
|
34
|
+
);
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
if (event.seq !== base + i) {
|
|
38
|
+
if (i <= lastTurnEnd)
|
|
39
|
+
throw new Error(
|
|
40
|
+
`corrupt session log: seq gap in committed region (expected ${base + i}, got ${event.seq})`,
|
|
41
|
+
);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
preserved.push(event);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return preserved.length < rows.length
|
|
48
|
+
? { preserved, tornFrom: base + preserved.length }
|
|
49
|
+
: { preserved };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function toJsonlArtifact(
|
|
53
|
+
meta: SessionHeader,
|
|
54
|
+
inheritedEventCount: number,
|
|
55
|
+
events: readonly SessionEvent[],
|
|
56
|
+
): string {
|
|
57
|
+
const header = sessionFormatCatalog.encodeCurrentHeader(
|
|
58
|
+
{
|
|
59
|
+
...meta,
|
|
60
|
+
delegationDepth: meta.delegationDepth ?? 0,
|
|
61
|
+
} as unknown as SessionFormatHeader,
|
|
62
|
+
inheritedEventCount,
|
|
63
|
+
);
|
|
64
|
+
const lines = [JSON.stringify(header)];
|
|
65
|
+
for (const event of events) {
|
|
66
|
+
lines.push(
|
|
67
|
+
JSON.stringify(
|
|
68
|
+
sessionFormatCatalog.encodeCurrentEvent(event as unknown as SessionFormatEvent),
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function titleOfEventData(data: string): string | undefined {
|
|
76
|
+
try {
|
|
77
|
+
const value = JSON.parse(data) as { data?: { title?: unknown }; title?: unknown };
|
|
78
|
+
const title = value.data?.title ?? value.title;
|
|
79
|
+
return typeof title === "string" ? title : undefined;
|
|
80
|
+
} catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
@@ -1,132 +1,6 @@
|
|
|
1
1
|
import { SessionSeq } from "@deepseek-ai/dsh-session";
|
|
2
|
-
import type { SessionEvent
|
|
3
|
-
import type { SessionFormatEvent, SessionFormatHeader } from "@deepseek-ai/dsh-session-format";
|
|
4
|
-
import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
5
|
-
import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
|
|
6
|
-
import type { EventRow, SessionRow } from "./backend.ts";
|
|
7
|
-
import { localDayKey } from "./usage.ts";
|
|
2
|
+
import type { SessionEvent } from "@deepseek-ai/dsh-session";
|
|
8
3
|
|
|
9
|
-
function normalizeSurfaceOp(value: unknown): SurfaceOp {
|
|
10
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
11
|
-
return value as SurfaceOp;
|
|
12
|
-
}
|
|
13
|
-
const record = value as Record<string, unknown>;
|
|
14
|
-
if (record["op"] !== "replace") return value as SurfaceOp;
|
|
15
|
-
const startSeq = record["startSeq"] ?? record["start"];
|
|
16
|
-
const endSeq = record["endSeq"] ?? record["end"];
|
|
17
|
-
if (typeof startSeq !== "number" || typeof endSeq !== "number") return value as SurfaceOp;
|
|
18
|
-
return { op: "replace", startSeq: SessionSeq(startSeq), endSeq: SessionSeq(endSeq) };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function rowToMeta(row: SessionRow): SessionHeader {
|
|
22
|
-
if (!Number.isSafeInteger(row.fCreatedAt) || row.fCreatedAt < 0) {
|
|
23
|
-
throw new Error("stored session createdAt must be a non-negative safe integer");
|
|
24
|
-
}
|
|
25
|
-
return {
|
|
26
|
-
version: row.fVersion as SessionHeader["version"],
|
|
27
|
-
id: row.fSessionId as SessionId,
|
|
28
|
-
createdAt: row.fCreatedAt,
|
|
29
|
-
...(row.fCwd !== null ? { cwd: row.fCwd } : {}),
|
|
30
|
-
...(row.fParentSession !== null ? { parentSession: row.fParentSession as SessionId } : {}),
|
|
31
|
-
isSeeded: row.fSeedLength !== null,
|
|
32
|
-
...(row.fOrigin !== null ? { origin: row.fOrigin as "subagent" } : {}),
|
|
33
|
-
...(row.fDelegationDepth === null ? {} : { delegationDepth: row.fDelegationDepth }),
|
|
34
|
-
...(row.fAgentPreset === null ? {} : { agentPreset: row.fAgentPreset }),
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function sessionInsertRow(
|
|
39
|
-
storage: SessionStorageMetadata,
|
|
40
|
-
incarnation: string,
|
|
41
|
-
): {
|
|
42
|
-
fSessionId: string;
|
|
43
|
-
fHeadEventId: string;
|
|
44
|
-
fHeadSequence: number;
|
|
45
|
-
fVersion: number;
|
|
46
|
-
fCreatedAt: number;
|
|
47
|
-
fCwd: string | null;
|
|
48
|
-
fParentSession: string | null;
|
|
49
|
-
fSeedLength: number | null;
|
|
50
|
-
fOrigin: string | null;
|
|
51
|
-
fDelegationDepth: number | null;
|
|
52
|
-
fAgentPreset: string | null;
|
|
53
|
-
fIncarnation: string;
|
|
54
|
-
fRevision: number;
|
|
55
|
-
} {
|
|
56
|
-
const meta = storage.meta;
|
|
57
|
-
return {
|
|
58
|
-
fSessionId: meta.id,
|
|
59
|
-
fHeadEventId: "",
|
|
60
|
-
fHeadSequence: -1,
|
|
61
|
-
fVersion: meta.version,
|
|
62
|
-
fCreatedAt: meta.createdAt,
|
|
63
|
-
fCwd: meta.cwd ?? null,
|
|
64
|
-
fParentSession: meta.parentSession ?? null,
|
|
65
|
-
fSeedLength: meta.isSeeded ? storage.inheritedEventCount : null,
|
|
66
|
-
fOrigin: meta.origin ?? null,
|
|
67
|
-
fDelegationDepth: meta.delegationDepth ?? null,
|
|
68
|
-
fAgentPreset: meta.agentPreset ?? null,
|
|
69
|
-
fIncarnation: incarnation,
|
|
70
|
-
fRevision: 0,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function sessionConflictRow(storage: SessionStorageMetadata): {
|
|
75
|
-
fVersion: number;
|
|
76
|
-
fCreatedAt: number;
|
|
77
|
-
fCwd: string | null;
|
|
78
|
-
fParentSession: string | null;
|
|
79
|
-
fSeedLength: number | null;
|
|
80
|
-
fOrigin: string | null;
|
|
81
|
-
fDelegationDepth: number | null;
|
|
82
|
-
fAgentPreset: string | null;
|
|
83
|
-
} {
|
|
84
|
-
const meta = storage.meta;
|
|
85
|
-
return {
|
|
86
|
-
fVersion: meta.version,
|
|
87
|
-
fCreatedAt: meta.createdAt,
|
|
88
|
-
fCwd: meta.cwd ?? null,
|
|
89
|
-
fParentSession: meta.parentSession ?? null,
|
|
90
|
-
fSeedLength: meta.isSeeded ? storage.inheritedEventCount : null,
|
|
91
|
-
fOrigin: meta.origin ?? null,
|
|
92
|
-
fDelegationDepth: meta.delegationDepth ?? null,
|
|
93
|
-
fAgentPreset: meta.agentPreset ?? null,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function rowToEvent(row: EventRow): SessionEvent {
|
|
98
|
-
const surfaceOp =
|
|
99
|
-
row.fSurfaceOp !== null ? normalizeSurfaceOp(JSON.parse(row.fSurfaceOp) as unknown) : undefined;
|
|
100
|
-
const record = JSON.parse(row.fData) as unknown;
|
|
101
|
-
|
|
102
|
-
const full =
|
|
103
|
-
typeof record === "object" &&
|
|
104
|
-
record !== null &&
|
|
105
|
-
!Array.isArray(record) &&
|
|
106
|
-
typeof (record as Record<string, unknown>)["type"] === "string" &&
|
|
107
|
-
typeof (record as Record<string, unknown>)["seq"] === "number" &&
|
|
108
|
-
typeof (record as Record<string, unknown>)["time"] === "number" &&
|
|
109
|
-
"data" in (record as Record<string, unknown>);
|
|
110
|
-
if (full) {
|
|
111
|
-
// 直接改写出参而不是 `{...record}`:冷读一页就是几万次,spread 在这里是纯开销。
|
|
112
|
-
const event = record as SessionEvent & { seq: number; time: number; surfaceOp?: unknown };
|
|
113
|
-
event.seq = row.fSequence as SessionEvent["seq"];
|
|
114
|
-
event.time = row.fCreatedAt as SessionEvent["time"];
|
|
115
|
-
if (surfaceOp !== undefined) event.surfaceOp = surfaceOp;
|
|
116
|
-
return event as SessionEvent;
|
|
117
|
-
}
|
|
118
|
-
return {
|
|
119
|
-
type: row.fType as SessionEvent["type"],
|
|
120
|
-
seq: row.fSequence,
|
|
121
|
-
time: row.fCreatedAt,
|
|
122
|
-
data: record as SessionEvent["data"],
|
|
123
|
-
...(surfaceOp === undefined ? {} : { surfaceOp }),
|
|
124
|
-
} as SessionEvent;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// 与上游 `SURFACE_EVENT_TYPES`(`…/core/session/src/surface.ts`)同一集合:`developer/message` 承载
|
|
128
|
-
// 工具清单的增删通知,同样是模型可见的 surface 事件,漏掉它会把它的 `surfaceOp` 当「非 surface 事件带的
|
|
129
|
-
// 标记」清掉,随后的持久化校验直接拒绝整条会话(agent-loop 在工具集变化时会写这种事件)。
|
|
130
4
|
const SURFACE_EVENT_TYPES = new Set([
|
|
131
5
|
"system/message",
|
|
132
6
|
"developer/message",
|
|
@@ -644,144 +518,3 @@ export function repairOrphanInboxSplices(events: SessionEvent[]): void {
|
|
|
644
518
|
}
|
|
645
519
|
}
|
|
646
520
|
|
|
647
|
-
export function scanRows(
|
|
648
|
-
rows: readonly EventRow[],
|
|
649
|
-
base = 0,
|
|
650
|
-
): { preserved: SessionEvent[]; tornFrom?: number } {
|
|
651
|
-
// 已提交区(最后一个 `turn/end` 及之前)必须完整:坏行/断号只有在它之后才允许当作撕裂尾部丢掉。
|
|
652
|
-
// 倒序只为找到这个边界,先按行类型筛(绝大多数行直接跳过),再解析候选行确认它本身可读。
|
|
653
|
-
let lastTurnEnd = -1;
|
|
654
|
-
for (let i = rows.length - 1; i >= 0; i--) {
|
|
655
|
-
if (rows[i]?.fType !== "turn/end") continue;
|
|
656
|
-
try {
|
|
657
|
-
rowToEvent(rows[i] as EventRow);
|
|
658
|
-
lastTurnEnd = i;
|
|
659
|
-
break;
|
|
660
|
-
} catch {
|
|
661
|
-
/* 这个 turn/end 自身坏掉:继续往前找上一个可读的边界。 */
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
const preserved: SessionEvent[] = [];
|
|
666
|
-
for (let i = 0; i < rows.length; i++) {
|
|
667
|
-
let event: SessionEvent;
|
|
668
|
-
try {
|
|
669
|
-
event = rowToEvent(rows[i] as EventRow);
|
|
670
|
-
} catch {
|
|
671
|
-
if (i <= lastTurnEnd)
|
|
672
|
-
throw new Error(
|
|
673
|
-
`corrupt session log: unparsable committed event at seq ${rows[i]?.fSequence}`,
|
|
674
|
-
);
|
|
675
|
-
break;
|
|
676
|
-
}
|
|
677
|
-
if (event.seq !== base + i) {
|
|
678
|
-
if (i <= lastTurnEnd)
|
|
679
|
-
throw new Error(
|
|
680
|
-
`corrupt session log: seq gap in committed region (expected ${base + i}, got ${event.seq})`,
|
|
681
|
-
);
|
|
682
|
-
break;
|
|
683
|
-
}
|
|
684
|
-
preserved.push(event);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
return preserved.length < rows.length
|
|
688
|
-
? { preserved, tornFrom: base + preserved.length }
|
|
689
|
-
: { preserved };
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
export function toJsonlArtifact(
|
|
693
|
-
meta: SessionHeader,
|
|
694
|
-
inheritedEventCount: number,
|
|
695
|
-
events: readonly SessionEvent[],
|
|
696
|
-
): string {
|
|
697
|
-
const header = sessionFormatCatalog.encodeCurrentHeader(
|
|
698
|
-
{
|
|
699
|
-
...meta,
|
|
700
|
-
delegationDepth: meta.delegationDepth ?? 0,
|
|
701
|
-
} as unknown as SessionFormatHeader,
|
|
702
|
-
inheritedEventCount,
|
|
703
|
-
);
|
|
704
|
-
const lines = [JSON.stringify(header)];
|
|
705
|
-
for (const event of events) {
|
|
706
|
-
lines.push(
|
|
707
|
-
JSON.stringify(
|
|
708
|
-
sessionFormatCatalog.encodeCurrentEvent(event as unknown as SessionFormatEvent),
|
|
709
|
-
),
|
|
710
|
-
);
|
|
711
|
-
}
|
|
712
|
-
return lines.join("\n");
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
export function titleOfEventData(data: string): string | undefined {
|
|
716
|
-
try {
|
|
717
|
-
const value = JSON.parse(data) as { data?: { title?: unknown }; title?: unknown };
|
|
718
|
-
const title = value.data?.title ?? value.title;
|
|
719
|
-
return typeof title === "string" ? title : undefined;
|
|
720
|
-
} catch {
|
|
721
|
-
return undefined;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// 用量行的列(`t_event_usage`):读侧维度(本地日与两个归属标记)在写入时一起物化。
|
|
726
|
-
export interface EventUsageRow {
|
|
727
|
-
fEventId: string;
|
|
728
|
-
fCreatedAt: number;
|
|
729
|
-
fDay: string;
|
|
730
|
-
fProvider: string | null;
|
|
731
|
-
fModel: string | null;
|
|
732
|
-
fReferenced: number;
|
|
733
|
-
fSubagent: number;
|
|
734
|
-
fInputTokens: number;
|
|
735
|
-
fOutputTokens: number;
|
|
736
|
-
fCacheReadTokens: number;
|
|
737
|
-
fReasoningTokens: number;
|
|
738
|
-
fTotalTokens: number;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
function numericField(value: unknown): number {
|
|
742
|
-
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
function textField(value: unknown): string | null {
|
|
746
|
-
return typeof value === "string" && value !== "" ? value : null;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// 一条事件行的用量(只有带 `usage` 的 `assistant/message` 才有;没有用量时为 undefined):写路径据此记录
|
|
750
|
-
// `t_event_usage`,统计因此不必逐行解析 JSON,两种 `f_data` 结构(带信封 / 老格式)都认。读侧维度一起物化:
|
|
751
|
-
// 本地日由事件时间算,`f_referenced` 直接 1(这一批事件行与桥接行同事务),`f_subagent` 取写入它的会话是否子代理。
|
|
752
|
-
export function usageRowOf(
|
|
753
|
-
event: {
|
|
754
|
-
fEventId: string;
|
|
755
|
-
fCreatedAt: number;
|
|
756
|
-
fType: string;
|
|
757
|
-
fData: string;
|
|
758
|
-
},
|
|
759
|
-
subagent: boolean,
|
|
760
|
-
): EventUsageRow | undefined {
|
|
761
|
-
if (event.fType !== "assistant/message") return undefined;
|
|
762
|
-
let parsed: Record<string, unknown>;
|
|
763
|
-
try {
|
|
764
|
-
parsed = JSON.parse(event.fData) as Record<string, unknown>;
|
|
765
|
-
} catch {
|
|
766
|
-
return undefined;
|
|
767
|
-
}
|
|
768
|
-
const envelope = (parsed["data"] ?? parsed) as Record<string, unknown>;
|
|
769
|
-
const usage = envelope["usage"];
|
|
770
|
-
if (typeof usage !== "object" || usage === null) return undefined;
|
|
771
|
-
const source = (envelope["message"] as { source?: Record<string, unknown> } | undefined)?.source;
|
|
772
|
-
const row: EventUsageRow = {
|
|
773
|
-
fEventId: event.fEventId,
|
|
774
|
-
fCreatedAt: event.fCreatedAt,
|
|
775
|
-
fDay: localDayKey(event.fCreatedAt),
|
|
776
|
-
fProvider: textField(source?.["provider"]),
|
|
777
|
-
fModel: textField(source?.["model"]),
|
|
778
|
-
fReferenced: 1,
|
|
779
|
-
fSubagent: subagent ? 1 : 0,
|
|
780
|
-
fInputTokens: numericField((usage as Record<string, unknown>)["inputTokens"]),
|
|
781
|
-
fOutputTokens: numericField((usage as Record<string, unknown>)["outputTokens"]),
|
|
782
|
-
fCacheReadTokens: numericField((usage as Record<string, unknown>)["cacheReadTokens"]),
|
|
783
|
-
fReasoningTokens: numericField((usage as Record<string, unknown>)["reasoningTokens"]),
|
|
784
|
-
fTotalTokens: numericField((usage as Record<string, unknown>)["totalTokens"]),
|
|
785
|
-
};
|
|
786
|
-
return row;
|
|
787
|
-
}
|
package/src/log/rows.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { SessionSeq } from "@deepseek-ai/dsh-session";
|
|
2
|
+
import type { InsertRow, RowOf } from "@morlay/rdbkit";
|
|
3
|
+
import { sessions } from "../entities/index.ts";
|
|
4
|
+
import type { SessionEvent, SessionHeader, SessionId, SurfaceOp } from "@deepseek-ai/dsh-session";
|
|
5
|
+
import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
|
|
6
|
+
import type { EventRow, SessionRow } from "../backend.ts";
|
|
7
|
+
|
|
8
|
+
function normalizeSurfaceOp(value: unknown): SurfaceOp {
|
|
9
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
10
|
+
return value as SurfaceOp;
|
|
11
|
+
}
|
|
12
|
+
const record = value as Record<string, unknown>;
|
|
13
|
+
if (record["op"] !== "replace") return value as SurfaceOp;
|
|
14
|
+
const startSeq = record["startSeq"] ?? record["start"];
|
|
15
|
+
const endSeq = record["endSeq"] ?? record["end"];
|
|
16
|
+
if (typeof startSeq !== "number" || typeof endSeq !== "number") return value as SurfaceOp;
|
|
17
|
+
return { op: "replace", startSeq: SessionSeq(startSeq), endSeq: SessionSeq(endSeq) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function rowToMeta(row: SessionRow): SessionHeader {
|
|
21
|
+
if (!Number.isSafeInteger(row.fCreatedAt) || row.fCreatedAt < 0) {
|
|
22
|
+
throw new Error("stored session createdAt must be a non-negative safe integer");
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
version: row.fVersion as SessionHeader["version"],
|
|
26
|
+
id: row.fSessionId as SessionId,
|
|
27
|
+
createdAt: row.fCreatedAt,
|
|
28
|
+
...(row.fCwd !== null ? { cwd: row.fCwd } : {}),
|
|
29
|
+
...(row.fParentSession !== null ? { parentSession: row.fParentSession as SessionId } : {}),
|
|
30
|
+
isSeeded: row.fSeedLength !== null,
|
|
31
|
+
...(row.fOrigin !== null ? { origin: row.fOrigin as "subagent" } : {}),
|
|
32
|
+
...(row.fDelegationDepth === null ? {} : { delegationDepth: row.fDelegationDepth }),
|
|
33
|
+
...(row.fAgentPreset === null ? {} : { agentPreset: row.fAgentPreset }),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 新建会话行(列名即表定义里的列,值全部显式给出)。 */
|
|
38
|
+
export function sessionInsertColumns(
|
|
39
|
+
storage: SessionStorageMetadata,
|
|
40
|
+
incarnation: string,
|
|
41
|
+
): InsertRow<RowOf<typeof sessions>> {
|
|
42
|
+
return {
|
|
43
|
+
...sessionHeaderColumns(storage),
|
|
44
|
+
f_session_id: storage.meta.id,
|
|
45
|
+
f_head_event_id: "",
|
|
46
|
+
f_head_sequence: -1,
|
|
47
|
+
f_incarnation: incarnation,
|
|
48
|
+
f_revision: 0,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** header 的八列(`upsert` 时「变了的列才写」判据也用它)。 */
|
|
53
|
+
export function sessionHeaderColumns(storage: SessionStorageMetadata): {
|
|
54
|
+
f_version: number;
|
|
55
|
+
f_created_at: number;
|
|
56
|
+
f_cwd: string | null;
|
|
57
|
+
f_parent_session: string | null;
|
|
58
|
+
f_seed_length: number | null;
|
|
59
|
+
f_origin: string | null;
|
|
60
|
+
f_delegation_depth: number | null;
|
|
61
|
+
f_agent_preset: string | null;
|
|
62
|
+
} {
|
|
63
|
+
const meta = storage.meta;
|
|
64
|
+
return {
|
|
65
|
+
f_version: meta.version,
|
|
66
|
+
f_created_at: meta.createdAt,
|
|
67
|
+
f_cwd: meta.cwd ?? null,
|
|
68
|
+
f_parent_session: meta.parentSession ?? null,
|
|
69
|
+
f_seed_length: meta.isSeeded ? storage.inheritedEventCount : null,
|
|
70
|
+
f_origin: meta.origin ?? null,
|
|
71
|
+
f_delegation_depth: meta.delegationDepth ?? null,
|
|
72
|
+
f_agent_preset: meta.agentPreset ?? null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function rowToEvent(row: EventRow): SessionEvent {
|
|
77
|
+
const surfaceOp =
|
|
78
|
+
row.fSurfaceOp !== null ? normalizeSurfaceOp(JSON.parse(row.fSurfaceOp) as unknown) : undefined;
|
|
79
|
+
const record = JSON.parse(row.fData) as unknown;
|
|
80
|
+
|
|
81
|
+
const full =
|
|
82
|
+
typeof record === "object" &&
|
|
83
|
+
record !== null &&
|
|
84
|
+
!Array.isArray(record) &&
|
|
85
|
+
typeof (record as Record<string, unknown>)["type"] === "string" &&
|
|
86
|
+
typeof (record as Record<string, unknown>)["seq"] === "number" &&
|
|
87
|
+
typeof (record as Record<string, unknown>)["time"] === "number" &&
|
|
88
|
+
"data" in (record as Record<string, unknown>);
|
|
89
|
+
if (full) {
|
|
90
|
+
// 直接改写出参而不是 `{...record}`:冷读一页就是几万次,spread 在这里是纯开销。
|
|
91
|
+
const event = record as SessionEvent & { seq: number; time: number; surfaceOp?: unknown };
|
|
92
|
+
event.seq = row.fSequence as SessionEvent["seq"];
|
|
93
|
+
event.time = row.fCreatedAt as SessionEvent["time"];
|
|
94
|
+
if (surfaceOp !== undefined) event.surfaceOp = surfaceOp;
|
|
95
|
+
return event as SessionEvent;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
type: row.fType as SessionEvent["type"],
|
|
99
|
+
seq: row.fSequence,
|
|
100
|
+
time: row.fCreatedAt,
|
|
101
|
+
data: record as SessionEvent["data"],
|
|
102
|
+
...(surfaceOp === undefined ? {} : { surfaceOp }),
|
|
103
|
+
} as SessionEvent;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 与上游 `SURFACE_EVENT_TYPES`(`…/core/session/src/surface.ts`)同一集合:`developer/message` 承载
|
|
107
|
+
// 工具清单的增删通知,同样是模型可见的 surface 事件,漏掉它会把它的 `surfaceOp` 当「非 surface 事件带的
|