@morlay/session-rdb 0.0.11 → 0.0.12

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.
@@ -0,0 +1,369 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { and, desc, eq, gte, sql } from "drizzle-orm";
3
+ import type { PgAsyncDatabase, PgAsyncTransaction, PgQueryResultHKT } from "drizzle-orm/pg-core";
4
+ import type { SessionId } from "@deepseek-ai/dsh-session";
5
+ import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
6
+ import {
7
+ type Backend,
8
+ type BackendTx,
9
+ type EventInsert,
10
+ type EventRow,
11
+ type SessionRow,
12
+ } from "./backend.ts";
13
+ import { SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID } from "./schema.ts";
14
+ import { createTablesSql, toPostgresSchema } from "./adapters/index.ts";
15
+ import { postgresTableDefs } from "./entities/index.ts";
16
+ import { sessionConflictRow, sessionInsertRow } from "./log.ts";
17
+
18
+ export interface PostgresBackendOptions {
19
+ identityBase: string;
20
+
21
+ schema?: string;
22
+
23
+ close: () => Promise<void>;
24
+ }
25
+
26
+ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> implements Backend {
27
+ readonly kind = "postgres" as const;
28
+ storeIdentity!: string;
29
+
30
+ private readonly tables: Record<string, any>;
31
+
32
+ constructor(
33
+ private readonly db: PgAsyncDatabase<THKT>,
34
+ private readonly options: PostgresBackendOptions,
35
+ ) {
36
+ this.tables = toPostgresSchema(postgresTableDefs, this.options.schema ?? "public");
37
+ }
38
+
39
+ async open(): Promise<void> {
40
+ const storeId = await this.db.transaction(async (tx) => {
41
+ const schema = this.options.schema ?? "public";
42
+ // 探测在建表之前:t_schema_meta 存在与否区分全新库与已有库。
43
+ // `to_regclass` 是数据库元数据查询,drizzle 无对应 API;schema 限定
44
+ // 显式引用(不依赖 search_path)。
45
+ const qualifiedMeta = schema === "public" ? "t_schema_meta" : `"${schema}".t_schema_meta`;
46
+ const probe = (await tx.execute(
47
+ sql`SELECT to_regclass(${qualifiedMeta}) IS NOT NULL AS exists`,
48
+ )) as unknown as { rows: { exists: boolean }[] };
49
+ const metaExists = probe.rows[0]?.exists === true;
50
+ // DDL 一次一条(PG 的 extended query protocol 拒绝多语句字符串),
51
+ // 逐条执行保持初始化原子。
52
+ for (const statement of createTablesSql("postgres", postgresTableDefs, schema)) {
53
+ await tx.execute(sql.raw(statement));
54
+ }
55
+ if (!metaExists) {
56
+ await tx
57
+ .insert(this.tables["t_schema_meta"])
58
+ .values([
59
+ { fKey: "schema_version", fValue: String(SCHEMA_VERSION) },
60
+ { fKey: "application_id", fValue: String(SESSION_PERSISTENCE_SQLITE_APPLICATION_ID) },
61
+ ])
62
+ .execute();
63
+ }
64
+ // 校验:缺版本行 = 有对象但未版本化 → 拒绝,不迁移。
65
+ const version = await this.readMeta(tx, "schema_version");
66
+ const applicationId = await this.readMeta(tx, "application_id");
67
+ if (version === undefined || applicationId === undefined) {
68
+ throw new Error("session database has an unversioned schema or application identity");
69
+ }
70
+ if (Number(version) !== SCHEMA_VERSION) {
71
+ throw new Error(
72
+ `session database has schema version ${version}, incompatible with this build (${SCHEMA_VERSION})`,
73
+ );
74
+ }
75
+ if (Number(applicationId) !== SESSION_PERSISTENCE_SQLITE_APPLICATION_ID) {
76
+ throw new Error(
77
+ `session database has application id ${applicationId}, expected ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`,
78
+ );
79
+ }
80
+ await tx
81
+ .insert(this.tables["t_persistence_state"])
82
+ .values({ fSingleton: 1, fStoreId: randomUUID() })
83
+ .onConflictDoNothing()
84
+ .execute();
85
+ const store = await tx
86
+ .select({ fStoreId: this.tables["t_persistence_state"].fStoreId })
87
+ .from(this.tables["t_persistence_state"])
88
+ .where(eq(this.tables["t_persistence_state"].fSingleton, 1))
89
+ .execute();
90
+ const storeId = store[0]?.fStoreId;
91
+ if (storeId === undefined || storeId.length === 0) {
92
+ throw new Error("session database has no valid store identity");
93
+ }
94
+ return storeId;
95
+ });
96
+ this.storeIdentity = `${this.options.identityBase}:store:${storeId}`;
97
+ }
98
+
99
+ async close(): Promise<void> {
100
+ await this.options.close();
101
+ }
102
+
103
+ async getSession(id: SessionId): Promise<SessionRow | undefined> {
104
+ return (
105
+ await this.db
106
+ .select()
107
+ .from(this.tables["t_sessions"])
108
+ .where(eq(this.tables["t_sessions"].fSessionId, id))
109
+ .execute()
110
+ )[0] as SessionRow | undefined;
111
+ }
112
+
113
+ async getSeqMapRows(id: SessionId): Promise<Array<{ fSequence: number; fOriginalSeq: number }>> {
114
+ return this.db
115
+ .select({
116
+ fSequence: this.tables["t_session_events"].fSequence,
117
+ fOriginalSeq: this.tables["t_session_events"].fOriginalSeq,
118
+ })
119
+ .from(this.tables["t_session_events"])
120
+ .where(eq(this.tables["t_session_events"].fSessionId, id))
121
+ .execute();
122
+ }
123
+
124
+ async getEventRows(id: SessionId, fromSequence?: number): Promise<EventRow[]> {
125
+ const scoped =
126
+ fromSequence === undefined
127
+ ? this.eventRows(this.db).where(eq(this.tables["t_session_events"].fSessionId, id))
128
+ : this.eventRows(this.db).where(
129
+ and(
130
+ eq(this.tables["t_session_events"].fSessionId, id),
131
+ gte(this.tables["t_session_events"].fSequence, fromSequence),
132
+ ),
133
+ );
134
+ return scoped
135
+ .orderBy(this.tables["t_session_events"].fSequence)
136
+ .execute() as unknown as EventRow[];
137
+ }
138
+
139
+ async listSessions(): Promise<SessionRow[]> {
140
+ return this.db.select().from(this.tables["t_sessions"]).execute() as unknown as Promise<
141
+ SessionRow[]
142
+ >;
143
+ }
144
+
145
+ async transaction<T>(fn: (tx: BackendTx) => Promise<T>): Promise<T> {
146
+ return this.db.transaction(async (tx) => fn(this.txFor(tx)));
147
+ }
148
+
149
+ private txFor(tx: PgAsyncTransaction<THKT>): BackendTx {
150
+ return {
151
+ upsertSession: (storage, incarnation) => this.upsertSession(tx, storage, incarnation),
152
+ getHead: (id) => this.getHead(tx, id),
153
+ getSeedLength: (id) => this.getSeedLength(tx, id),
154
+ updateSeedLength: (id, seedLength) => this.updateSeedLength(tx, id, seedLength),
155
+ insertEvents: (events) => this.insertEvents(tx, events),
156
+ insertBridges: (rows) => this.insertBridges(tx, rows),
157
+ updateHead: (id, headEventId, headSequence) =>
158
+ this.updateHead(tx, id, headEventId, headSequence),
159
+ bumpRevision: (id) => this.bumpRevision(tx, id),
160
+ deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(tx, id, fromSequence),
161
+ getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence),
162
+ getLastBridge: (id) => this.getLastBridge(tx, id),
163
+ };
164
+ }
165
+
166
+ // --- meta helpers ---
167
+
168
+ private async readMeta(exec: PgAsyncDatabase<THKT>, key: string): Promise<string | undefined> {
169
+ const rows = await exec
170
+ .select({ fValue: this.tables["t_schema_meta"].fValue })
171
+ .from(this.tables["t_schema_meta"])
172
+ .where(eq(this.tables["t_schema_meta"].fKey, key))
173
+ .execute();
174
+ return rows[0]?.fValue;
175
+ }
176
+
177
+ // --- row primitives (transaction-internal) ---
178
+
179
+ private async upsertSession(
180
+ exec: PgAsyncDatabase<THKT>,
181
+ storage: SessionStorageMetadata,
182
+ incarnation: string,
183
+ ): Promise<void> {
184
+ await exec
185
+ .insert(this.tables["t_sessions"])
186
+ .values(sessionInsertRow(storage, incarnation))
187
+ .onConflictDoUpdate({
188
+ target: this.tables["t_sessions"].fSessionId,
189
+ set: sessionConflictRow(storage),
190
+ })
191
+ .execute();
192
+ }
193
+
194
+ private async getHead(
195
+ exec: PgAsyncDatabase<THKT>,
196
+ id: SessionId,
197
+ ): Promise<Pick<SessionRow, "fHeadEventId" | "fHeadSequence">> {
198
+ const head = (
199
+ await exec
200
+ .select({
201
+ fHeadEventId: this.tables["t_sessions"].fHeadEventId,
202
+ fHeadSequence: this.tables["t_sessions"].fHeadSequence,
203
+ })
204
+ .from(this.tables["t_sessions"])
205
+ .where(eq(this.tables["t_sessions"].fSessionId, id))
206
+ .execute()
207
+ )[0] as Pick<SessionRow, "fHeadEventId" | "fHeadSequence"> | undefined;
208
+ /* v8 ignore next -- appendBatch/commitRepair always materialize the row before reading the head */
209
+ if (head === undefined) throw new Error(`session "${id}" has no materialized row`);
210
+ return head;
211
+ }
212
+
213
+ private async getSeedLength(exec: PgAsyncDatabase<THKT>, id: SessionId): Promise<number | null> {
214
+ const row = (
215
+ await exec
216
+ .select({ fSeedLength: this.tables["t_sessions"].fSeedLength })
217
+ .from(this.tables["t_sessions"])
218
+ .where(eq(this.tables["t_sessions"].fSessionId, id))
219
+ .execute()
220
+ )[0] as { fSeedLength: number | null } | undefined;
221
+ /* v8 ignore next -- rewind always materializes the row before reading the seed length */
222
+ if (row === undefined) throw new Error(`session "${id}" has no materialized row`);
223
+ return row.fSeedLength;
224
+ }
225
+
226
+ private async updateSeedLength(
227
+ exec: PgAsyncDatabase<THKT>,
228
+ id: SessionId,
229
+ seedLength: number,
230
+ ): Promise<void> {
231
+ await exec
232
+ .update(this.tables["t_sessions"])
233
+ .set({ fSeedLength: seedLength })
234
+ .where(eq(this.tables["t_sessions"].fSessionId, id))
235
+ .execute();
236
+ }
237
+
238
+ private static readonly INSERT_BATCH_ROWS = 1000;
239
+
240
+ private async insertEvents(exec: PgAsyncDatabase<THKT>, events: EventInsert[]): Promise<void> {
241
+ if (events.length === 0) return;
242
+ for (let i = 0; i < events.length; i += PostgresBackend.INSERT_BATCH_ROWS) {
243
+ await exec
244
+ .insert(this.tables["t_events"])
245
+ .values(
246
+ events.slice(i, i + PostgresBackend.INSERT_BATCH_ROWS).map((event) => ({ ...event })),
247
+ )
248
+ .execute();
249
+ }
250
+ }
251
+
252
+ private async insertBridges(
253
+ exec: PgAsyncDatabase<THKT>,
254
+ rows: Array<{
255
+ fSessionId: SessionId;
256
+ fEventId: string;
257
+ fSequence: number;
258
+ fOriginalSeq: number;
259
+ fSurfaceOp: string | null;
260
+ }>,
261
+ ): Promise<void> {
262
+ if (rows.length === 0) return;
263
+ for (let i = 0; i < rows.length; i += PostgresBackend.INSERT_BATCH_ROWS) {
264
+ await exec
265
+ .insert(this.tables["t_session_events"])
266
+ .values(rows.slice(i, i + PostgresBackend.INSERT_BATCH_ROWS).map((row) => ({ ...row })))
267
+ .execute();
268
+ }
269
+ }
270
+
271
+ private async updateHead(
272
+ exec: PgAsyncDatabase<THKT>,
273
+ id: SessionId,
274
+ headEventId: string,
275
+ headSequence: number,
276
+ ): Promise<void> {
277
+ await exec
278
+ .update(this.tables["t_sessions"])
279
+ .set({ fHeadEventId: headEventId, fHeadSequence: headSequence })
280
+ .where(eq(this.tables["t_sessions"].fSessionId, id))
281
+ .execute();
282
+ }
283
+
284
+ private async bumpRevision(exec: PgAsyncDatabase<THKT>, id: SessionId): Promise<void> {
285
+ await exec
286
+ .update(this.tables["t_sessions"])
287
+ .set({ fRevision: sql`${this.tables["t_sessions"].fRevision} + 1` })
288
+ .where(eq(this.tables["t_sessions"].fSessionId, id))
289
+ .execute();
290
+ }
291
+
292
+ private async deleteBridgeTail(
293
+ exec: PgAsyncDatabase<THKT>,
294
+ id: SessionId,
295
+ fromSequence: number,
296
+ ): Promise<void> {
297
+ await exec
298
+ .delete(this.tables["t_session_events"])
299
+ .where(
300
+ and(
301
+ eq(this.tables["t_session_events"].fSessionId, id),
302
+ gte(this.tables["t_session_events"].fSequence, fromSequence),
303
+ ),
304
+ )
305
+ .execute();
306
+ }
307
+
308
+ private async getPrevBridge(
309
+ exec: PgAsyncDatabase<THKT>,
310
+ id: SessionId,
311
+ sequence: number,
312
+ ): Promise<{ fEventId: string; fSequence: number } | undefined> {
313
+ return (
314
+ await exec
315
+ .select({
316
+ fEventId: this.tables["t_session_events"].fEventId,
317
+ fSequence: this.tables["t_session_events"].fSequence,
318
+ })
319
+ .from(this.tables["t_session_events"])
320
+ .where(
321
+ and(
322
+ eq(this.tables["t_session_events"].fSessionId, id),
323
+ eq(this.tables["t_session_events"].fSequence, sequence),
324
+ ),
325
+ )
326
+ .execute()
327
+ )[0] as { fEventId: string; fSequence: number } | undefined;
328
+ }
329
+
330
+ private async getLastBridge(
331
+ exec: PgAsyncDatabase<THKT>,
332
+ id: SessionId,
333
+ ): Promise<{ fEventId: string; fSequence: number } | undefined> {
334
+ return (
335
+ await exec
336
+ .select({
337
+ fEventId: this.tables["t_session_events"].fEventId,
338
+ fSequence: this.tables["t_session_events"].fSequence,
339
+ })
340
+ .from(this.tables["t_session_events"])
341
+ .where(eq(this.tables["t_session_events"].fSessionId, id))
342
+ .orderBy(desc(this.tables["t_session_events"].fSequence))
343
+ .limit(1)
344
+ .execute()
345
+ )[0] as { fEventId: string; fSequence: number } | undefined;
346
+ }
347
+
348
+ private eventRows(exec: PgAsyncDatabase<THKT>) {
349
+ return exec
350
+ .select({
351
+ fEventId: this.tables["t_session_events"].fEventId,
352
+ fSequence: this.tables["t_session_events"].fSequence,
353
+ fOriginalSeq: this.tables["t_session_events"].fOriginalSeq,
354
+ fType: this.tables["t_events"].fType,
355
+ fKind: this.tables["t_events"].fKind,
356
+ fRole: this.tables["t_events"].fRole,
357
+ fName: this.tables["t_events"].fName,
358
+ fActionId: this.tables["t_events"].fActionId,
359
+ fCreatedAt: this.tables["t_events"].fCreatedAt,
360
+ fData: this.tables["t_events"].fData,
361
+ fSurfaceOp: this.tables["t_session_events"].fSurfaceOp,
362
+ })
363
+ .from(this.tables["t_session_events"])
364
+ .innerJoin(
365
+ this.tables["t_events"],
366
+ eq(this.tables["t_session_events"].fEventId, this.tables["t_events"].fEventId),
367
+ );
368
+ }
369
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,234 @@
1
+ import type { SessionEvent } from "@deepseek-ai/dsh-session";
2
+ import { toSqliteSchema } from "./adapters/index.ts";
3
+ import { sqliteTableDefs } from "./entities/index.ts";
4
+
5
+ export const SCHEMA_VERSION = 2;
6
+
7
+ export const SESSION_PERSISTENCE_SQLITE_APPLICATION_ID = 0x44534850;
8
+
9
+ export const EPHEMERAL_EVENT_TYPES = ["assistant/chunk"] as const;
10
+
11
+ export const EVENT_ENCODING = "json";
12
+
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ const sqliteTables: Record<string, any> = toSqliteSchema(sqliteTableDefs);
15
+
16
+ export const tPersistenceState = sqliteTables["t_persistence_state"]!;
17
+
18
+ export const tSessions = sqliteTables["t_sessions"]!;
19
+
20
+ export const tEvents = sqliteTables["t_events"]!;
21
+
22
+ export const tSessionEvents = sqliteTables["t_session_events"]!;
23
+
24
+ export type { SessionRow } from "./backend.ts";
25
+
26
+ export type { EventRow } from "./backend.ts";
27
+
28
+ export type JournalMode = "wal" | "delete" | "truncate" | "persist";
29
+
30
+ export const DEFAULT_BUSY_TIMEOUT_MS = 5000;
31
+
32
+ export function isEphemeralType(type: string): boolean {
33
+ return (EPHEMERAL_EVENT_TYPES as readonly string[]).includes(type);
34
+ }
35
+
36
+ export function isPersistedEvent(event: SessionEvent): boolean {
37
+ return (
38
+ !isEphemeralType(event.type) &&
39
+ (event as SessionEvent & { ignorable?: unknown }).ignorable !== true
40
+ );
41
+ }
42
+
43
+ export type EventKind =
44
+ | "message"
45
+ | "thinking"
46
+ | "turn"
47
+ | "tool"
48
+ | "request"
49
+ | "config"
50
+ | "audit"
51
+ | "lifecycle"
52
+ | "inbox"
53
+ | "compaction"
54
+ | "llm"
55
+ | "subagent"
56
+ | "team"
57
+ | "workflow"
58
+ | "goal"
59
+ | "schedule"
60
+ | "todo"
61
+ | "web";
62
+
63
+ export type EventRole = "user" | "assistant" | "tool";
64
+
65
+ export function eventKind(event: { type: string; data?: unknown }): EventKind | "" {
66
+ switch (event.type) {
67
+ case "user/message":
68
+ return "message";
69
+ case "assistant/message": {
70
+ const content = (event.data as { message?: { content?: unknown[] } }).message?.content;
71
+ if (content?.some((block) => (block as { type?: string })?.type === "reasoning")) {
72
+ return "thinking";
73
+ }
74
+ return "message";
75
+ }
76
+ case "turn/start":
77
+ case "turn/end":
78
+ case "step/start":
79
+ case "step/end":
80
+ case "session/end-seed":
81
+ return "turn";
82
+ case "tool/call":
83
+ case "tool/result":
84
+ case "tool/code-dispatch-start":
85
+ case "tool/code-dispatch":
86
+ return "tool";
87
+ case "request/header":
88
+ case "request/context":
89
+ return "request";
90
+ case "model/selection":
91
+ case "permission/preset":
92
+ case "approval/policy":
93
+ case "sandbox/mode":
94
+ case "plan/mode":
95
+ case "agent-preset/selected":
96
+ return "config";
97
+ case "approval/asked":
98
+ case "approval/decided":
99
+ case "command/run":
100
+ case "command/done":
101
+ case "hook/invoked":
102
+ case "hook/result":
103
+ case "feedback/record":
104
+ return "audit";
105
+ case "session/title":
106
+ case "session/title-llm-request":
107
+ case "session-log-deepseek/delivery-accepted":
108
+ return "lifecycle";
109
+ case "agent/inbox/spliced":
110
+ return "inbox";
111
+ case "compaction/start":
112
+ case "compaction/end":
113
+ case "compaction/summary":
114
+ case "compaction/prune":
115
+ return "compaction";
116
+ case "llm/retry":
117
+ case "llm/retry-started":
118
+ return "llm";
119
+ case "subagent/descriptor":
120
+ case "subagent/model-selection-policy":
121
+ return "subagent";
122
+ case "team/member":
123
+ case "team/task":
124
+ case "team/message/queued":
125
+ case "team/message/delivered":
126
+ return "team";
127
+ case "tool-workflow/run-start":
128
+ case "tool-workflow/run-end":
129
+ case "tool-workflow/agent-start":
130
+ case "tool-workflow/agent-end":
131
+ return "workflow";
132
+ case "goal/change":
133
+ return "goal";
134
+ case "schedule/change":
135
+ return "schedule";
136
+ case "todo/write":
137
+ return "todo";
138
+ case "web/deepseek-search-llm-request":
139
+ return "web";
140
+ default:
141
+ return ""; // unknown plugin-merged type
142
+ }
143
+ }
144
+
145
+ export function eventDimensions(event: SessionEvent): {
146
+ kind: string;
147
+ role: string;
148
+ name: string;
149
+ actionId: string;
150
+ } {
151
+ const kind = eventKind(event);
152
+ // 插件合并类型不在 core 的判别联合内,type/data 经结构化视图访问。
153
+ const type = event.type as string;
154
+ const data = event.data as Record<string, unknown>;
155
+ switch (type) {
156
+ case "user/message":
157
+ return { kind, role: "user", name: "", actionId: "" };
158
+ case "assistant/message":
159
+ return { kind, role: "assistant", name: "", actionId: "" };
160
+ case "tool/result": {
161
+ // append 可能携带旧形状(无 message);可选链容忍。
162
+ const message = data["message"] as { content?: Array<{ toolCallId?: string }> } | undefined;
163
+ return { kind, role: "tool", name: "", actionId: message?.content?.[0]?.toolCallId ?? "" };
164
+ }
165
+ case "tool/call":
166
+ return {
167
+ kind,
168
+ role: "",
169
+ name: typeof data["name"] === "string" ? data["name"] : "",
170
+ actionId: typeof data["callId"] === "string" ? data["callId"] : "",
171
+ };
172
+ case "tool/code-dispatch-start":
173
+ case "tool/code-dispatch":
174
+ return {
175
+ kind,
176
+ role: "",
177
+ name: "",
178
+ actionId: typeof data["subCallId"] === "string" ? data["subCallId"] : "",
179
+ };
180
+ case "command/run":
181
+ case "command/done":
182
+ return {
183
+ kind,
184
+ role: "",
185
+ name: type === "command/run" && typeof data["name"] === "string" ? data["name"] : "",
186
+ actionId: typeof data["commandId"] === "string" ? data["commandId"] : "",
187
+ };
188
+ case "approval/asked":
189
+ case "approval/decided":
190
+ return {
191
+ kind,
192
+ role: "",
193
+ name: "",
194
+ actionId: typeof data["id"] === "string" ? data["id"] : "",
195
+ };
196
+ case "hook/invoked":
197
+ case "hook/result":
198
+ return {
199
+ kind,
200
+ role: "",
201
+ name: "",
202
+ actionId: typeof data["handlerId"] === "string" ? data["handlerId"] : "",
203
+ };
204
+ case "llm/retry":
205
+ case "llm/retry-started":
206
+ return {
207
+ kind,
208
+ role: "",
209
+ name: "",
210
+ actionId: typeof data["retryId"] === "string" ? data["retryId"] : "",
211
+ };
212
+ case "tool-workflow/run-start":
213
+ case "tool-workflow/run-end":
214
+ case "tool-workflow/agent-start":
215
+ case "tool-workflow/agent-end":
216
+ return {
217
+ kind,
218
+ role: "",
219
+ name: "",
220
+ actionId: typeof data["runId"] === "string" ? data["runId"] : "",
221
+ };
222
+ case "todo/write":
223
+ return { kind, role: "", name: "todos", actionId: "" };
224
+ case "subagent/descriptor":
225
+ return {
226
+ kind,
227
+ role: "",
228
+ name: typeof data["label"] === "string" ? data["label"] : "",
229
+ actionId: "",
230
+ };
231
+ default:
232
+ return { kind, role: "", name: "", actionId: "" };
233
+ }
234
+ }