@morlay/session-rdb 0.1.10-alpha.0 → 0.1.10-alpha.3

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.
Files changed (142) hide show
  1. package/README.md +18 -3
  2. package/dist/artifact.d.mts +118 -2
  3. package/dist/artifact.mjs +5 -3
  4. package/dist/{log-TYgd8Xkr.d.mts → backend-BKtJ5sO5.d.mts} +183 -284
  5. package/dist/backend-rdb-BDCntVDy.mjs +1341 -0
  6. package/dist/branch-CG1PUlO6.mjs +833 -0
  7. package/dist/deletion-SSX7IqU8.mjs +35 -0
  8. package/dist/deletion.d.mts +1 -1
  9. package/dist/deletion.mjs +2 -2
  10. package/dist/export-CfJQjQfr.mjs +123 -0
  11. package/dist/export.d.mts +1 -1
  12. package/dist/export.mjs +31 -55
  13. package/dist/import.d.mts +1 -1
  14. package/dist/import.mjs +31 -76
  15. package/dist/index-CUD2ZpTA.d.mts +71 -0
  16. package/dist/index.d.mts +20 -2
  17. package/dist/index.mjs +1561 -4
  18. package/dist/open-YoyD_kah.mjs +561 -0
  19. package/dist/organize.d.mts +9 -0
  20. package/dist/organize.mjs +30 -0
  21. package/dist/route-DpfY0zq-.mjs +114 -0
  22. package/dist/storage.d.mts +41 -48
  23. package/dist/storage.mjs +3 -1
  24. package/dist/testing.d.mts +9 -1
  25. package/dist/testing.mjs +25 -4
  26. package/dist/usage-row-BQQM-_t6.mjs +36 -0
  27. package/dist/usage.d.mts +1 -1
  28. package/dist/usage.mjs +7 -40
  29. package/dist/write-guard-CYrY58GM.mjs +174 -0
  30. package/package.json +32 -36
  31. package/src/artifact.ts +4 -1
  32. package/src/backend-rdb.ts +331 -0
  33. package/src/backend.ts +32 -15
  34. package/src/{branch.ts → branch/index.ts} +109 -110
  35. package/src/branch/port.ts +62 -0
  36. package/src/branch/semantics.ts +69 -0
  37. package/src/db/open.ts +180 -0
  38. package/src/db/stats.ts +449 -0
  39. package/src/deletion-error.ts +17 -0
  40. package/src/deletion.ts +19 -72
  41. package/src/entities/index.ts +16 -1
  42. package/src/entities/v3/event-usage.ts +3 -3
  43. package/src/entities/v3/events.ts +3 -3
  44. package/src/entities/v3/index.ts +4 -16
  45. package/src/entities/v3/persistence-state.ts +3 -3
  46. package/src/entities/v3/schema-meta.ts +3 -3
  47. package/src/entities/v3/session-counts.ts +3 -3
  48. package/src/entities/v3/session-events.ts +3 -3
  49. package/src/entities/v3/session-projcache-rows.ts +3 -3
  50. package/src/entities/v3/session-usage.ts +3 -3
  51. package/src/entities/v3/sessions.ts +3 -3
  52. package/src/entities/v3/storage-records.ts +17 -0
  53. package/src/entities/v3/storage-units.ts +3 -3
  54. package/src/{schema.ts → event-shape.ts} +0 -46
  55. package/src/export.ts +32 -78
  56. package/src/handles.ts +349 -0
  57. package/src/http/route.ts +178 -0
  58. package/src/import-storages.ts +5 -2
  59. package/src/import.ts +63 -125
  60. package/src/index.ts +15 -1666
  61. package/src/legacy.ts +3 -1
  62. package/src/live-routing.ts +205 -0
  63. package/src/log/export.ts +84 -0
  64. package/src/{log.ts → log/repair.ts} +1 -268
  65. package/src/log/rows.ts +107 -0
  66. package/src/log/usage-row.ts +64 -0
  67. package/src/migrations.ts +196 -0
  68. package/src/organize.ts +38 -0
  69. package/src/read-path.ts +190 -0
  70. package/src/routes.ts +2 -1
  71. package/src/rows.ts +25 -77
  72. package/src/session-ops.ts +444 -0
  73. package/src/session-store.ts +675 -0
  74. package/src/stats-fold.ts +44 -0
  75. package/src/storage-takeover/index.ts +2 -5
  76. package/src/storage-takeover/legacy-domain-migration.ts +118 -0
  77. package/src/storage-takeover/projection-cache.ts +122 -71
  78. package/src/storage-takeover/read-model.ts +189 -0
  79. package/src/storage-takeover/repository.ts +236 -442
  80. package/src/storage-takeover/storage-backend.ts +44 -159
  81. package/src/storage-takeover/types.ts +45 -19
  82. package/src/storage-takeover/workspace-documents.ts +65 -0
  83. package/src/storage.ts +4 -2
  84. package/src/testing.ts +21 -1
  85. package/src/usage.ts +11 -65
  86. package/src/version.ts +13 -0
  87. package/src/write-path.ts +286 -0
  88. package/dist/branch-CoPCRKer.mjs +0 -401
  89. package/dist/deletion-CWl89TIR.mjs +0 -2249
  90. package/dist/gc.d.mts +0 -4
  91. package/dist/gc.mjs +0 -48
  92. package/dist/log-CcWy46VE.mjs +0 -558
  93. package/dist/routes-hZjTcX_R.mjs +0 -9
  94. package/dist/sqlite-BNRd1aqR.mjs +0 -1778
  95. package/drizzle/postgres/20260908072805_v2_initial/migration.sql +0 -58
  96. package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +0 -686
  97. package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +0 -1
  98. package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +0 -673
  99. package/drizzle/postgres/20260910120001_v3_storage_tables/migration.sql +0 -65
  100. package/drizzle/postgres/20260910120001_v3_storage_tables/snapshot.json +0 -1183
  101. package/drizzle/postgres/20260910130001_v3_session_title_backfill/migration.sql +0 -10
  102. package/drizzle/postgres/20260910130001_v3_session_title_backfill/snapshot.json +0 -1183
  103. package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +0 -14
  104. package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +0 -1309
  105. package/drizzle/postgres/20260921075900_v3_agent_preset/migration.sql +0 -1
  106. package/drizzle/postgres/20260922090001_v3_pinned_seq/migration.sql +0 -2
  107. package/drizzle/postgres/20260922093001_v3_last_event_at/migration.sql +0 -10
  108. package/drizzle/postgres/20260923120000_v3_usage_materialized/migration.sql +0 -48
  109. package/drizzle/postgres/20261008103001_v3_schedule_tasks/migration.sql +0 -11
  110. package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +0 -53
  111. package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +0 -492
  112. package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +0 -6
  113. package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +0 -513
  114. package/drizzle/sqlite/20260910120000_v3_storage_tables/migration.sql +0 -66
  115. package/drizzle/sqlite/20260910120000_v3_storage_tables/snapshot.json +0 -905
  116. package/drizzle/sqlite/20260910130000_v3_session_title_backfill/migration.sql +0 -13
  117. package/drizzle/sqlite/20260910130000_v3_session_title_backfill/snapshot.json +0 -905
  118. package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +0 -14
  119. package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +0 -1031
  120. package/drizzle/sqlite/20260921075855_v3_agent_preset/migration.sql +0 -7
  121. package/drizzle/sqlite/20260921075855_v3_agent_preset/snapshot.json +0 -1041
  122. package/drizzle/sqlite/20260922090000_v3_pinned_seq/migration.sql +0 -4
  123. package/drizzle/sqlite/20260922093000_v3_last_event_at/migration.sql +0 -13
  124. package/drizzle/sqlite/20260923120000_v3_usage_materialized/migration.sql +0 -48
  125. package/drizzle/sqlite/20261008103000_v3_schedule_tasks/migration.sql +0 -14
  126. package/src/adapters/index.ts +0 -12
  127. package/src/adapters/to-postgres.ts +0 -93
  128. package/src/adapters/to-sqlite.ts +0 -83
  129. package/src/adapters/types.ts +0 -44
  130. package/src/drizzle/postgres-v2.ts +0 -10
  131. package/src/drizzle/postgres-v3.ts +0 -18
  132. package/src/drizzle/sqlite-v2.ts +0 -9
  133. package/src/drizzle/sqlite-v3.ts +0 -18
  134. package/src/entities/v2/index.ts +0 -20
  135. package/src/entities/v2/session-events.ts +0 -10
  136. package/src/entities/v3/schedule-tasks.ts +0 -16
  137. package/src/entities/v3/workspace-sessions.ts +0 -19
  138. package/src/entities/v3/workspace-state.ts +0 -12
  139. package/src/entities/v3/workspaces.ts +0 -14
  140. package/src/gc.ts +0 -77
  141. package/src/postgres.ts +0 -1015
  142. package/src/sqlite.ts +0 -1096
@@ -0,0 +1,561 @@
1
+ import { a as sessionProjcacheRows, c as sessions, i as catalog, l as persistenceState, n as backfillUsageTables, o as storageRecords, p as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, r as createReadModel, s as storageUnits, t as RdbBackend } from "./backend-rdb-BDCntVDy.mjs";
2
+ import { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
3
+ import { randomUUID } from "node:crypto";
4
+ import { E, defineTable, ensureSchema, openDatabase, runMigrations } from "@morlay/rdbkit";
5
+ import { chmod, mkdir, open, stat } from "node:fs/promises";
6
+ import { dirname, resolve } from "node:path";
7
+ // src/storage-takeover/legacy-domain-migration.ts
8
+ /**
9
+ * 把旧语义表的内容写进通用域存储(`workspace` / `schedule` / 上一版 `t_storage_globals` 的域)。
10
+ * @param rows - 旧表读出来的行。
11
+ * @param repository - 通用域存储。
12
+ */
13
+ async function migrateLegacyDomainStorage(rows, repository) {
14
+ if (rows.workspaces.length > 0 || rows.state !== void 0) {
15
+ for (const row of rows.workspaces) {
16
+ const sessionIds = rows.links.filter((link) => link.workspaceId === row.id).sort((left, right) => left.position - right.position).map((link) => link.sessionId);
17
+ await repository.putDomainRecord("workspace", "workspaces", row.id, {
18
+ path: row.path,
19
+ title: row.title,
20
+ sessionIds,
21
+ createdAt: row.createdAt,
22
+ updatedAt: row.updatedAt
23
+ });
24
+ }
25
+ const pending = rows.state?.pendingOperation;
26
+ const pendingMutation = pending === "create" || pending === "delete" ? {
27
+ operation: pending,
28
+ workspaceId: rows.state?.pendingWorkspaceId ?? ""
29
+ } : void 0;
30
+ await repository.writeDomainGlobal("workspace", {
31
+ initialized: rows.state === void 0 ? true : Boolean(rows.state.initialized),
32
+ workspaceIds: rows.workspaces.filter((row) => row.position >= 0).sort((left, right) => left.position - right.position).map((row) => row.id),
33
+ archivedSessionIds: rows.archived.map((id) => id),
34
+ pinnedSessionIds: rows.pinned.sort((left, right) => left.seq - right.seq).map((row) => row.id),
35
+ ...pendingMutation === void 0 ? {} : { pendingMutation }
36
+ });
37
+ await repository.insertUnitVersion("workspace", 2);
38
+ }
39
+ for (const row of rows.tasks) await repository.putDomainRecord("schedule", "tasks", row.id, {
40
+ sessionId: row.sessionId,
41
+ record: JSON.parse(row.record),
42
+ status: row.status === "inactive" ? "inactive" : "active",
43
+ ...row.lastDelivery === null ? {} : { lastDelivery: JSON.parse(row.lastDelivery) },
44
+ ...row.deliveryHistory === null ? {} : { deliveryHistory: JSON.parse(row.deliveryHistory) }
45
+ });
46
+ if (rows.tasks.length > 0) await repository.insertUnitVersion("schedule", 1);
47
+ for (const row of rows.globals) {
48
+ await repository.writeDomainGlobal(row.domain, row.value);
49
+ await repository.insertUnitVersion(row.domain, 1);
50
+ }
51
+ }
52
+ // src/storage-takeover/repository.ts
53
+ function identityOfSession(row) {
54
+ return {
55
+ formatVersion: row.fVersion,
56
+ createdAt: row.fCreatedAt,
57
+ ...row.fCwd === null ? {} : { cwd: row.fCwd },
58
+ isSeeded: row.fSeedLength !== null,
59
+ ...row.fSeedLength === null ? {} : { inheritedEventCount: SessionLogOffset(row.fSeedLength) }
60
+ };
61
+ }
62
+ function seqCursor(seq) {
63
+ return seq === -1 ? -1 : SessionSeq(seq);
64
+ }
65
+ function createStorageRepository(host) {
66
+ const records = storageRecords.table(host.database);
67
+ const projcache = sessionProjcacheRows.table(host.database);
68
+ const sessionRows = sessions.table(host.database);
69
+ const units = storageUnits.table(host.database);
70
+ /** 读一个域的 global(空表名那一行);没有就返回 `null`(域层据此用 initial)。 */
71
+ async function readGlobal(domain) {
72
+ for await (const row of records.select({ value: records.column("f_value") }).where(E.and(E.eq(records.f_domain, domain), E.eq(records.f_table, GLOBAL_TABLE)))) return JSON.parse(row.value);
73
+ return null;
74
+ }
75
+ const GLOBAL_TABLE = "";
76
+ const WORKSPACE_DOMAIN = "workspace";
77
+ const WORKSPACE_TABLE = "workspaces";
78
+ async function syncSessionFlags(database, state) {
79
+ const table = sessions.table(database);
80
+ const pinned = state.pinnedSessionIds ?? [];
81
+ await table.update({ f_pinned_seq: null }, E.isNotNull(table.f_pinned_seq));
82
+ for (const [position, sessionId] of pinned.entries()) await table.update({ f_pinned_seq: position }, E.eq(table.f_session_id, sessionId));
83
+ const archived = state.archivedSessionIds;
84
+ await table.update({ f_archived_at: null }, E.isNotNull(table.f_archived_at));
85
+ const stamp = Date.now();
86
+ for (const sessionId of archived) {
87
+ await table.insertIgnore({
88
+ f_session_id: sessionId,
89
+ f_head_event_id: "",
90
+ f_head_sequence: -1,
91
+ f_version: SESSION_FORMAT_VERSION,
92
+ f_created_at: stamp,
93
+ f_cwd: null,
94
+ f_parent_session: null,
95
+ f_seed_length: null,
96
+ f_origin: null,
97
+ f_delegation_depth: null,
98
+ f_incarnation: randomUUID(),
99
+ f_revision: 0,
100
+ f_archived_at: stamp
101
+ }, ["f_session_id"]);
102
+ await table.update({ f_archived_at: stamp }, E.eq(table.f_session_id, sessionId));
103
+ }
104
+ }
105
+ return {
106
+ async readUnitVersion(name) {
107
+ for await (const row of units.select({ version: units.column("f_version") }).where(E.eq(units.f_name, name))) return row.version;
108
+ },
109
+ async insertUnitVersion(name, version) {
110
+ await host.writeAtomically(async () => {
111
+ await units.insertIgnore({
112
+ f_name: name,
113
+ f_version: version
114
+ }, ["f_name"]);
115
+ });
116
+ },
117
+ async listDomainRecords(domain) {
118
+ const result = [];
119
+ for await (const row of records.select({
120
+ table: records.column("f_table"),
121
+ key: records.column("f_key"),
122
+ value: records.column("f_value")
123
+ }).where(E.eq(records.f_domain, domain))) {
124
+ if (row.table === "") continue;
125
+ result.push({
126
+ table: row.table,
127
+ key: row.key,
128
+ value: JSON.parse(row.value)
129
+ });
130
+ }
131
+ return result;
132
+ },
133
+ async readDomainGlobal(domain) {
134
+ return await readGlobal(domain);
135
+ },
136
+ async putDomainRecord(domain, table, key, value) {
137
+ await host.writeAtomically(async () => {
138
+ await records.upsert({
139
+ f_domain: domain,
140
+ f_table: table,
141
+ f_key: key,
142
+ f_value: JSON.stringify(value)
143
+ }, [
144
+ "f_domain",
145
+ "f_table",
146
+ "f_key"
147
+ ], ["f_value"]);
148
+ });
149
+ },
150
+ async deleteDomainRecord(domain, table, key) {
151
+ await host.writeAtomically(async () => {
152
+ await records.delete(E.and(E.eq(records.f_domain, domain), E.eq(records.f_table, table), E.eq(records.f_key, key)));
153
+ });
154
+ },
155
+ async deleteDomainRecords(domain, table, keys) {
156
+ if (keys.length === 0) return;
157
+ await host.writeAtomically(async () => {
158
+ await records.delete(E.and(E.eq(records.f_domain, domain), E.eq(records.f_table, table), E.inList(records.f_key, [...new Set(keys)])));
159
+ });
160
+ },
161
+ async writeDomainGlobal(domain, value) {
162
+ await host.writeAtomically(async () => {
163
+ await records.upsert({
164
+ f_domain: domain,
165
+ f_table: "",
166
+ f_key: "",
167
+ f_value: JSON.stringify(value)
168
+ }, [
169
+ "f_domain",
170
+ "f_table",
171
+ "f_key"
172
+ ], ["f_value"]);
173
+ if (domain === WORKSPACE_DOMAIN) await syncSessionFlags(host.database, value);
174
+ });
175
+ },
176
+ async listWorkspaceIndex() {
177
+ const rows = [];
178
+ for await (const row of records.select({
179
+ key: records.column("f_key"),
180
+ value: records.column("f_value")
181
+ }).where(E.and(E.eq(records.f_domain, WORKSPACE_DOMAIN), E.eq(records.f_table, WORKSPACE_TABLE)))) rows.push({
182
+ id: row.key,
183
+ record: JSON.parse(row.value)
184
+ });
185
+ const state = await readGlobal(WORKSPACE_DOMAIN);
186
+ const order = new Map((state?.workspaceIds ?? []).map((id, position) => [String(id), position]));
187
+ return rows.sort((left, right) => (order.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (order.get(right.id) ?? Number.MAX_SAFE_INTEGER)).map(({ id, record }) => ({
188
+ id,
189
+ title: record.title,
190
+ sessionIds: [...record.sessionIds]
191
+ }));
192
+ },
193
+ async loadProjcache() {
194
+ const entries = /* @__PURE__ */ new Map();
195
+ for await (const row of sessionRows.select({
196
+ sessionId: sessionRows.f_session_id,
197
+ version: sessionRows.f_version,
198
+ createdAt: sessionRows.f_created_at,
199
+ cwd: sessionRows.f_cwd,
200
+ seedLength: sessionRows.f_seed_length,
201
+ title: sessionRows.f_title,
202
+ titleSeq: sessionRows.f_title_seq
203
+ })) entries.set(row.sessionId, {
204
+ sessionId: SessionId(row.sessionId),
205
+ identity: identityOfSession({
206
+ fVersion: row.version,
207
+ fCreatedAt: Number(row.createdAt),
208
+ fCwd: row.cwd,
209
+ fSeedLength: row.seedLength
210
+ }),
211
+ rows: {},
212
+ ...typeof row.title === "string" && typeof row.titleSeq === "number" ? { title: {
213
+ title: row.title,
214
+ seq: row.titleSeq
215
+ } } : {}
216
+ });
217
+ if (entries.size === 0) return [];
218
+ for await (const row of projcache.select({
219
+ sessionId: projcache.f_session_id,
220
+ key: projcache.f_key,
221
+ ver: projcache.f_ver,
222
+ seq: projcache.f_seq,
223
+ val: projcache.f_val
224
+ })) {
225
+ const entry = entries.get(row.sessionId);
226
+ if (entry === void 0) continue;
227
+ entry.rows[row.key] = {
228
+ ver: row.ver,
229
+ seq: seqCursor(row.seq),
230
+ val: JSON.parse(row.val)
231
+ };
232
+ }
233
+ return [...entries.values()].filter((entry) => Object.keys(entry.rows).length > 0);
234
+ },
235
+ async readSessionTitle(sessionId) {
236
+ const table = sessions.table(host.database);
237
+ for await (const row of table.select({
238
+ title: table.f_title,
239
+ seq: table.f_title_seq
240
+ }).where(E.eq(table.f_session_id, sessionId))) return row.title === null || row.seq === null ? void 0 : {
241
+ title: row.title,
242
+ seq: row.seq
243
+ };
244
+ },
245
+ async putProjcache(sessionId, rows) {
246
+ const values = Object.entries(rows).map(([key, row]) => ({
247
+ f_session_id: sessionId,
248
+ f_key: key,
249
+ f_ver: row.ver,
250
+ f_seq: row.seq,
251
+ f_val: JSON.stringify(row.val)
252
+ }));
253
+ await host.writeAtomically(async () => {
254
+ await projcache.delete(E.eq(projcache.f_session_id, sessionId));
255
+ if (values.length > 0) await projcache.insertMany(values);
256
+ });
257
+ },
258
+ async patchProjcache(sessionId, changed, removedKeys) {
259
+ const values = Object.entries(changed).map(([key, row]) => ({
260
+ f_session_id: sessionId,
261
+ f_key: key,
262
+ f_ver: row.ver,
263
+ f_seq: row.seq,
264
+ f_val: JSON.stringify(row.val)
265
+ }));
266
+ const touched = [...Object.keys(changed), ...removedKeys];
267
+ if (touched.length === 0) return;
268
+ await host.writeAtomically(async () => {
269
+ await projcache.delete(E.and(E.eq(projcache.f_session_id, sessionId), E.inList(projcache.f_key, touched)));
270
+ if (values.length > 0) await projcache.insertMany(values);
271
+ });
272
+ },
273
+ async pruneStaleProjcache() {
274
+ const stale = [];
275
+ for await (const row of projcache.select({ sessionId: projcache.f_session_id }).where(E.and(E.lt(projcache.f_seq, 0), E.inSubquery(projcache.f_session_id, sessionRows.select({ id: sessionRows.f_session_id }).where(E.gte(sessionRows.f_head_sequence, 0)).toFragment())))) stale.push(row.sessionId);
276
+ if (stale.length === 0) return 0;
277
+ await host.writeAtomically(async () => {
278
+ await projcache.delete(E.inList(projcache.f_session_id, stale));
279
+ });
280
+ return stale.length;
281
+ }
282
+ };
283
+ }
284
+ // src/migrations.ts
285
+ const legacyWorkspaces = defineTable({
286
+ name: "t_workspaces",
287
+ columns: {
288
+ f_workspace_id: {
289
+ type: "text",
290
+ primaryKey: true
291
+ },
292
+ f_path: { type: "text" },
293
+ f_title: { type: "text" },
294
+ f_created_at: { type: "text" },
295
+ f_updated_at: { type: "text" },
296
+ f_position: { type: "integer" }
297
+ }
298
+ });
299
+ const legacyLinks = defineTable({
300
+ name: "t_workspace_sessions",
301
+ columns: {
302
+ f_workspace_id: { type: "text" },
303
+ f_session_id: { type: "text" },
304
+ f_position: { type: "integer" }
305
+ }
306
+ });
307
+ const legacyWorkspaceState = defineTable({
308
+ name: "t_workspace_state",
309
+ columns: {
310
+ f_singleton: {
311
+ type: "integer",
312
+ primaryKey: true
313
+ },
314
+ f_initialized: { type: "integer" },
315
+ f_pending_operation: { type: "text" },
316
+ f_pending_workspace_id: { type: "text" }
317
+ }
318
+ });
319
+ const legacyScheduleTasks = defineTable({
320
+ name: "t_schedule_tasks",
321
+ columns: {
322
+ f_id: {
323
+ type: "text",
324
+ primaryKey: true
325
+ },
326
+ f_session_id: { type: "text" },
327
+ f_status: { type: "text" },
328
+ f_record: { type: "text" },
329
+ f_last_delivery: { type: "text" },
330
+ f_delivery_history: { type: "text" }
331
+ }
332
+ });
333
+ const legacyGlobals = defineTable({
334
+ name: "t_storage_globals",
335
+ columns: {
336
+ f_domain: {
337
+ type: "text",
338
+ primaryKey: true
339
+ },
340
+ f_value: { type: "text" }
341
+ }
342
+ });
343
+ /**
344
+ * 旧语义表(workspace / schedule / cua 授权域)→ 通用域存储的一次性搬运。
345
+ *
346
+ * 旧表保留可回退;是否搬过由 `t_schema_migrations` 账本决定(只跑一次),不再需要 `t_schema_meta` 标记。
347
+ */
348
+ function sessionMigrations() {
349
+ return [{
350
+ id: "20261028-legacy-domain-storage",
351
+ up: async (tx) => {
352
+ const objects = new Set(await tx.objectNames());
353
+ if (!objects.has(legacyWorkspaces.name)) return false;
354
+ const workspaces = [];
355
+ if (objects.has(legacyWorkspaces.name)) {
356
+ const table = tx.table(legacyWorkspaces);
357
+ for await (const row of table.select({
358
+ id: table.f_workspace_id,
359
+ path: table.f_path,
360
+ title: table.f_title,
361
+ createdAt: table.f_created_at,
362
+ updatedAt: table.f_updated_at,
363
+ position: table.f_position
364
+ })) workspaces.push({
365
+ id: String(row.id),
366
+ path: String(row.path ?? ""),
367
+ title: String(row.title ?? ""),
368
+ createdAt: String(row.createdAt ?? ""),
369
+ updatedAt: String(row.updatedAt ?? ""),
370
+ position: Number(row.position ?? 0)
371
+ });
372
+ }
373
+ const links = [];
374
+ if (objects.has(legacyLinks.name)) {
375
+ const table = tx.table(legacyLinks);
376
+ for await (const row of table.select({
377
+ workspaceId: table.f_workspace_id,
378
+ sessionId: table.f_session_id,
379
+ position: table.f_position
380
+ })) links.push({
381
+ workspaceId: String(row.workspaceId),
382
+ sessionId: String(row.sessionId),
383
+ position: Number(row.position ?? 0)
384
+ });
385
+ }
386
+ let state;
387
+ if (objects.has(legacyWorkspaceState.name)) {
388
+ const table = tx.table(legacyWorkspaceState);
389
+ for await (const row of table.select({
390
+ initialized: table.f_initialized,
391
+ pendingOperation: table.f_pending_operation,
392
+ pendingWorkspaceId: table.f_pending_workspace_id
393
+ }).where(E.eq(table.f_singleton, 1)).limit(1)) state = {
394
+ initialized: Number(row.initialized ?? 0),
395
+ pendingOperation: row.pendingOperation,
396
+ pendingWorkspaceId: row.pendingWorkspaceId
397
+ };
398
+ }
399
+ const tasks = [];
400
+ if (objects.has(legacyScheduleTasks.name)) {
401
+ const table = tx.table(legacyScheduleTasks);
402
+ for await (const row of table.select({
403
+ id: table.f_id,
404
+ sessionId: table.f_session_id,
405
+ status: table.f_status,
406
+ record: table.f_record,
407
+ lastDelivery: table.f_last_delivery,
408
+ deliveryHistory: table.f_delivery_history
409
+ })) tasks.push({
410
+ id: String(row.id),
411
+ sessionId: String(row.sessionId ?? ""),
412
+ status: String(row.status ?? ""),
413
+ record: String(row.record ?? ""),
414
+ lastDelivery: row.lastDelivery,
415
+ deliveryHistory: row.deliveryHistory
416
+ });
417
+ }
418
+ const globals = [];
419
+ if (objects.has(legacyGlobals.name)) {
420
+ const table = tx.table(legacyGlobals);
421
+ for await (const row of table.select({
422
+ domain: table.f_domain,
423
+ value: table.f_value
424
+ })) globals.push({
425
+ domain: String(row.domain),
426
+ value: JSON.parse(String(row.value))
427
+ });
428
+ }
429
+ const sessionRows = sessions.table(tx);
430
+ const archived = [];
431
+ for await (const row of sessionRows.select({ id: sessionRows.f_session_id }).where(E.isNotNull(sessionRows.f_archived_at)).orderBy(sessionRows.f_archived_at)) archived.push(String(row.id));
432
+ const pinned = [];
433
+ for await (const row of sessionRows.select({
434
+ id: sessionRows.f_session_id,
435
+ seq: sessionRows.f_pinned_seq
436
+ }).where(E.isNotNull(sessionRows.f_pinned_seq)).orderBy(sessionRows.f_pinned_seq)) pinned.push({
437
+ id: String(row.id),
438
+ seq: Number(row.seq)
439
+ });
440
+ const storage = createStorageRepository({
441
+ database: tx,
442
+ writeAtomically: async (fn) => await fn(tx)
443
+ });
444
+ await migrateLegacyDomainStorage({
445
+ workspaces,
446
+ links,
447
+ state,
448
+ archived,
449
+ pinned,
450
+ tasks,
451
+ globals
452
+ }, storage);
453
+ }
454
+ }];
455
+ }
456
+ // src/db/open.ts
457
+ /**
458
+ * 写事务按介质串行:SQLite 的 `BEGIN IMMEDIATE` 并发会直接 busy,PG 的 `serializable` 并发会冲突;
459
+ * 同一个库(进程内多个实例)共用一条队列。
460
+ */
461
+ const writeQueues = /* @__PURE__ */ new Map();
462
+ function enqueueWrite(key, fn) {
463
+ const run = (writeQueues.get(key) ?? Promise.resolve()).then(fn);
464
+ writeQueues.set(key, run.then(() => void 0, () => void 0));
465
+ return run;
466
+ }
467
+ /** 打开数据库、对齐结构、跑迁移、落 identity(连接、库身份、回填都在这一步)。 */
468
+ async function openRdbBackend(config) {
469
+ const schema = config.type === "postgres" ? config.schema ?? "public" : "";
470
+ const actualPath = config.type === "sqlite" && config.path !== ":memory:" ? resolve(config.path) : void 0;
471
+ if (actualPath !== void 0) {
472
+ await mkdir(dirname(actualPath), {
473
+ recursive: true,
474
+ mode: 448
475
+ });
476
+ try {
477
+ await (await open(actualPath, "wx", 384)).close();
478
+ await chmod(actualPath, 384);
479
+ } catch (error) {
480
+ if (error.code !== "EEXIST") throw error;
481
+ }
482
+ }
483
+ const database = await openDatabase(config.type === "sqlite" ? {
484
+ dialect: "sqlite",
485
+ path: actualPath ?? config.path,
486
+ ...config.journalMode === void 0 ? {} : { journalMode: config.journalMode },
487
+ applyJournalMode: false,
488
+ busyTimeout: config.busyTimeout ?? 5e3,
489
+ cacheSizeKb: 65536,
490
+ mmapSize: 268435456
491
+ } : {
492
+ dialect: "postgres",
493
+ connectionString: config.connectionString,
494
+ ...config.schema === void 0 ? {} : { schema: config.schema },
495
+ isolationLevel: "serializable"
496
+ });
497
+ try {
498
+ let onDisk = 0;
499
+ if (config.type === "sqlite") {
500
+ onDisk = Number(await database.sqlitePragma("user_version") ?? 0);
501
+ const applicationId = Number(await database.sqlitePragma("application_id") ?? 0);
502
+ if (onDisk !== 0 && onDisk !== 3) throw new Error(`session database at "${config.path}" has schema version ${onDisk}, incompatible with this build (3)`);
503
+ if (onDisk >= 1 && applicationId !== 1146308688) throw new Error(`session database at "${config.path}" has application id ${applicationId}, expected ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`);
504
+ if (onDisk === 0) {
505
+ const objects = await database.objectNames();
506
+ if (applicationId !== 0 && applicationId !== 1146308688 || objects.length > 0) throw new Error(`session database at "${config.path}" has an unversioned schema or application identity`);
507
+ }
508
+ }
509
+ const writeKey = config.type === "sqlite" ? `sqlite:${config.path}` : `postgres:${config.connectionString}:${schema}`;
510
+ const inWrite = (fn) => enqueueWrite(writeKey, () => database.transaction(fn));
511
+ if (actualPath !== void 0) for (const suffix of [
512
+ "-wal",
513
+ "-shm",
514
+ "-journal"
515
+ ]) try {
516
+ await chmod(`${actualPath}${suffix}`, 384);
517
+ } catch {}
518
+ let storeId;
519
+ await inWrite(async (scoped) => {
520
+ if (config.type === "sqlite" && onDisk === 0) {
521
+ await scoped.setSqlitePragma("application_id", SESSION_PERSISTENCE_SQLITE_APPLICATION_ID);
522
+ await scoped.setSqlitePragma("user_version", 3);
523
+ }
524
+ await ensureSchema(scoped, catalog);
525
+ await runMigrations(scoped, {
526
+ catalog,
527
+ migrations: sessionMigrations()
528
+ });
529
+ await backfillUsageTables(scoped);
530
+ const state = persistenceState.table(scoped);
531
+ await state.insertIgnore({
532
+ f_singleton: 1,
533
+ f_store_id: randomUUID()
534
+ }, ["f_singleton"]);
535
+ for await (const row of state.select({ storeId: state.f_store_id })) storeId = row.storeId;
536
+ });
537
+ if (storeId === void 0 || storeId.length === 0) throw new Error("session database has no valid store identity");
538
+ if (config.type === "sqlite") await database.setSqlitePragma("journal_mode", (config.journalMode ?? "wal").toLowerCase());
539
+ const storage = createStorageRepository({
540
+ database,
541
+ writeAtomically: (fn) => inWrite((scoped) => fn(scoped))
542
+ });
543
+ const storeIdentity = config.type === "sqlite" ? actualPath === void 0 ? `memory:store:${storeId}` : await fileIdentity(actualPath, storeId) : `${config.type}:${config.connectionString}:${schema}:store:${storeId}`;
544
+ return new RdbBackend({
545
+ kind: config.type,
546
+ database,
547
+ storage,
548
+ readModel: createReadModel(database),
549
+ storeIdentity
550
+ });
551
+ } catch (error) {
552
+ await database.close();
553
+ throw error;
554
+ }
555
+ }
556
+ /** 库文件身份:`file:dev:ino:birthtimeNs:store:<storeId>`(符号链接指向同一 inode 时身份相同)。 */
557
+ async function fileIdentity(path, storeId) {
558
+ const identity = await stat(path, { bigint: true });
559
+ return `file:${identity.dev}:${identity.ino}:${identity.birthtimeNs}:store:${storeId}`;
560
+ }
561
+ export { openRdbBackend as t };
@@ -0,0 +1,9 @@
1
+ import { H as SESSION_ORGANIZE_PATH, k as SessionPersistenceRdb } from "./backend-BKtJ5sO5.mjs";
2
+ import { Context } from "@deepseek-ai/cordis";
3
+ /**
4
+ * 整理数据通道:停 agent → 回收孤儿(会话 / 事件行)→ 重算派生统计 → VACUUM。
5
+ *
6
+ * 不只是清理孤儿:三张派生统计表是可销毁重建的,整理时一并整表重算,修掉旁路累加漏记或旧数据漂移。
7
+ */
8
+ export declare function registerSessionOrganize(ctx: Context, persistence: SessionPersistenceRdb): void;
9
+ export { SESSION_ORGANIZE_PATH };
@@ -0,0 +1,30 @@
1
+ import { n as mountJsonRoute, s as SESSION_ORGANIZE_PATH } from "./route-DpfY0zq-.mjs";
2
+ // src/organize.ts
3
+ async function stopRunningAgents(ctx) {
4
+ const running = ctx.get("agents")?.list() ?? [];
5
+ for (const agent of running) agent.cancel?.({ kind: "user" }, { keepInbox: true });
6
+ await Promise.all(running.map((agent) => agent.whenIdle()));
7
+ return running.length;
8
+ }
9
+ /**
10
+ * 整理数据通道:停 agent → 回收孤儿(会话 / 事件行)→ 重算派生统计 → VACUUM。
11
+ *
12
+ * 不只是清理孤儿:三张派生统计表是可销毁重建的,整理时一并整表重算,修掉旁路累加漏记或旧数据漂移。
13
+ */
14
+ function registerSessionOrganize(ctx, persistence) {
15
+ mountJsonRoute(ctx, {
16
+ path: SESSION_ORGANIZE_PATH,
17
+ handle: async () => {
18
+ const stoppedAgents = await stopRunningAgents(ctx);
19
+ const { orphanSessions, orphanEvents, rebuiltSessions } = await persistence.organize();
20
+ return { body: {
21
+ stoppedAgents,
22
+ orphanSessions,
23
+ orphanEvents,
24
+ rebuiltSessions,
25
+ vacuumed: true
26
+ } };
27
+ }
28
+ });
29
+ }
30
+ export { SESSION_ORGANIZE_PATH, registerSessionOrganize };