@morlay/session-rdb 0.0.21-alpha.5 → 0.0.21-alpha.7

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 (50) hide show
  1. package/README.md +53 -27
  2. package/dist/artifact.d.mts +27 -2
  3. package/dist/artifact.mjs +2 -2
  4. package/dist/{backend-a0xDrUFG.d.mts → backend-E0bmTXpz.d.mts} +233 -27
  5. package/dist/{branch-Dlmbkfjj.mjs → branch-BjUR6wfe.mjs} +31 -15
  6. package/dist/deletion.d.mts +1 -1
  7. package/dist/deletion.mjs +1 -1
  8. package/dist/import.d.mts +1 -1
  9. package/dist/import.mjs +2 -3
  10. package/dist/index.d.mts +2 -2
  11. package/dist/index.mjs +4 -4
  12. package/dist/{log-C-rbdFwK.mjs → log-qCNkDjh5.mjs} +93 -22
  13. package/dist/{sqlite-C3LHLzYW.mjs → sqlite-zcXVyMxr.mjs} +85 -10
  14. package/dist/{src-Bn0038Sp.mjs → src-B9X8BKyh.mjs} +210 -38
  15. package/dist/storage.d.mts +2 -1
  16. package/dist/storage.mjs +1 -1
  17. package/dist/testing.d.mts +2 -11
  18. package/dist/testing.mjs +12 -29
  19. package/drizzle/postgres/20260921075900_v3_agent_preset/migration.sql +1 -0
  20. package/drizzle/postgres/20260922090001_v3_pinned_seq/migration.sql +2 -0
  21. package/drizzle/postgres/20260922093001_v3_last_event_at/migration.sql +10 -0
  22. package/drizzle/sqlite/20260921075855_v3_agent_preset/migration.sql +7 -0
  23. package/drizzle/sqlite/20260921075855_v3_agent_preset/snapshot.json +1041 -0
  24. package/drizzle/sqlite/20260922090000_v3_pinned_seq/migration.sql +4 -0
  25. package/drizzle/sqlite/20260922093000_v3_last_event_at/migration.sql +13 -0
  26. package/locale/en.json +6 -0
  27. package/locale/zh.json +6 -0
  28. package/package.json +24 -22
  29. package/src/backend.ts +46 -1
  30. package/src/branch.ts +28 -11
  31. package/src/drizzle/postgres-v3.ts +1 -0
  32. package/src/drizzle/sqlite-v3.ts +1 -0
  33. package/src/entities/v3/event-usage.ts +5 -5
  34. package/src/entities/v3/sessions.ts +5 -0
  35. package/src/import.ts +3 -3
  36. package/src/index.ts +56 -22
  37. package/src/legacy.ts +14 -3
  38. package/src/log.ts +107 -22
  39. package/src/postgres.ts +65 -5
  40. package/src/rows.ts +125 -0
  41. package/src/schema.ts +3 -2
  42. package/src/session-query.ts +27 -1
  43. package/src/sqlite.ts +71 -3
  44. package/src/storage-takeover/projection-cache.ts +40 -15
  45. package/src/storage-takeover/repository.ts +34 -0
  46. package/src/storage-takeover/storage-backend.ts +3 -1
  47. package/src/testing/coordinator-contract.ts +2 -1
  48. package/src/testing.ts +0 -1
  49. package/src/write-guard.ts +5 -0
  50. package/src/testing/helpers.ts +0 -20
@@ -1,7 +1,7 @@
1
- import { c as repairRequestHeaders, d as rowToMeta, f as scanRows } from "./log-C-rbdFwK.mjs";
1
+ import { d as repairRequestHeaders, h as scanRows, i as normalizeToCurrentShape, m as rowToMeta } from "./log-qCNkDjh5.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
4
- import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
4
+ import { createSessionFormatCatalogWithChildren } from "@deepseek-ai/dsh-session-format-catalog";
5
5
  import { SessionBranch, SessionBranchError, balanceRewindPrefix, rewindKeepLength } from "@morlay/session-branch";
6
6
  //#region src/legacy.ts
7
7
  function physicalHeader(row) {
@@ -13,7 +13,8 @@ function physicalHeader(row) {
13
13
  ...row.fCwd !== null ? { cwd: row.fCwd } : {},
14
14
  ...row.fParentSession !== null ? { parentSession: row.fParentSession } : {},
15
15
  ...row.fOrigin !== null ? { origin: row.fOrigin } : {},
16
- delegationDepth: row.fDelegationDepth ?? 0
16
+ delegationDepth: row.fDelegationDepth ?? 0,
17
+ ...row.fAgentPreset !== null ? { agentPreset: row.fAgentPreset } : {}
17
18
  };
18
19
  return row.fVersion < 2 ? {
19
20
  ...common,
@@ -34,8 +35,14 @@ function physicalEvent(row) {
34
35
  ...row.fSurfaceOp !== null ? { surfaceOp: JSON.parse(row.fSurfaceOp) } : {}
35
36
  };
36
37
  }
38
+ /**
39
+ * 读旧格式用的 catalog:v3→v4 这条边要求显式子会话证据(上游 0.1.7 起),本仓库声明
40
+ * **无子会话证据**——v3 的目录事实由父会话自己在成功路径上写成 `subagent/catalog` 事件,
41
+ * 迁移的 child evidence 只是补偿入口;我们没有更权威的子集合来源可给(见 ADR-跟随上游session-format-v4)。
42
+ */
43
+ const restoreCatalog = createSessionFormatCatalogWithChildren([]);
37
44
  function convertLegacyRows(row, eventRows) {
38
- const restore = sessionFormatCatalog.createRestore(physicalHeader(row), {
45
+ const restore = restoreCatalog.createRestore(physicalHeader(row), {
39
46
  recovery: "strict",
40
47
  validation: "transformed"
41
48
  });
@@ -53,6 +60,7 @@ function isLegacyVersion(version) {
53
60
  function adoptLegacyRows(row, eventRows) {
54
61
  const { preserved, tornFrom } = scanRows(eventRows, 0);
55
62
  repairRequestHeaders(preserved);
63
+ normalizeToCurrentShape(preserved);
56
64
  return {
57
65
  meta: {
58
66
  ...rowToMeta(row),
@@ -187,19 +195,27 @@ var SessionBranchRdbProvider = class {
187
195
  };
188
196
  const seed = [...renumber(prefix, 0), ...renumber(seedSuffix, prefix.length)];
189
197
  const internals = this.persistence.internals();
190
- const sourceRows = await internals.backend.getEventRows(sourceId);
191
- const sourceEventIds = new Map(sourceRows.map((row) => [row.fSequence, row.fEventId]));
198
+ const reusable = !source.migrated && source.events.length === source.storedCount;
192
199
  const reuse = /* @__PURE__ */ new Map();
193
- for (const event of prefix) {
194
- const eventId = sourceEventIds.get(event.seq);
195
- if (eventId !== void 0) reuse.set(event.seq, eventId);
196
- }
200
+ if (reusable) {
201
+ const sourceRows = await internals.backend.getEventRows(sourceId);
202
+ const sourceEventIds = new Map(sourceRows.map((row) => [row.fSequence, row.fEventId]));
203
+ for (const event of prefix) {
204
+ const eventId = sourceEventIds.get(event.seq);
205
+ if (eventId !== void 0) reuse.set(event.seq, eventId);
206
+ }
207
+ } else if (prefix.length > 0) this.live.warn?.(`session-rdb: fork "${sourceId}" copies event rows instead of reusing them — its read view comes from a migration, so stored seqs need not line up`);
197
208
  internals.registerReuseEventIds(childId, reuse);
198
- const handle = await this.persistence.create(childMeta, { inheritedEventCount: SessionLogOffset(prefix.length) });
199
209
  try {
200
- if (seed.length > 0) await handle.append(seed);
201
- } finally {
202
- await handle.close();
210
+ const handle = await this.persistence.create(childMeta, { inheritedEventCount: SessionLogOffset(prefix.length) });
211
+ try {
212
+ if (seed.length > 0) await handle.append(seed);
213
+ } finally {
214
+ await handle.close();
215
+ }
216
+ } catch (error) {
217
+ internals.dropReuseEventIds(childId);
218
+ throw error;
203
219
  }
204
220
  await this.persistence.rebuildEventCounts(childId);
205
221
  return childId;
@@ -379,4 +395,4 @@ var SessionBranchRdb = class extends SessionBranch {
379
395
  }
380
396
  };
381
397
  //#endregion
382
- export { truncateLiveSession as a, isLegacyVersion as c, replaceLiveSessionLog as i, SessionBranchRdbProvider as n, adoptLegacyRows as o, locateTurnEnd as r, convertLegacyRows as s, SessionBranchRdb as t };
398
+ export { truncateLiveSession as a, isLegacyVersion as c, replaceLiveSessionLog as i, restoreCatalog as l, SessionBranchRdbProvider as n, adoptLegacyRows as o, locateTurnEnd as r, convertLegacyRows as s, SessionBranchRdb as t };
@@ -1,4 +1,4 @@
1
- import { d as SessionPersistenceRdb } from "./backend-a0xDrUFG.mjs";
1
+ import { p as SessionPersistenceRdb } from "./backend-E0bmTXpz.mjs";
2
2
  import { Context } from "@deepseek-ai/cordis";
3
3
  //#region src/deletion.d.ts
4
4
  declare const SESSION_DELETE_PATH = "/api/session.delete";
package/dist/deletion.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { i as registerSessionDeletion, r as SESSION_DELETE_PATH } from "./src-Bn0038Sp.mjs";
1
+ import { a as registerSessionDeletion, i as SESSION_DELETE_PATH } from "./src-B9X8BKyh.mjs";
2
2
  export { SESSION_DELETE_PATH, registerSessionDeletion };
package/dist/import.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { d as SessionPersistenceRdb } from "./backend-a0xDrUFG.mjs";
1
+ import { p as SessionPersistenceRdb } from "./backend-E0bmTXpz.mjs";
2
2
  import { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
3
3
  import { Session, SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
4
4
  import { Context } from "@deepseek-ai/cordis";
package/dist/import.mjs CHANGED
@@ -1,8 +1,7 @@
1
- import { i as replaceLiveSessionLog } from "./branch-Dlmbkfjj.mjs";
1
+ import { i as replaceLiveSessionLog, l as restoreCatalog } from "./branch-BjUR6wfe.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
4
4
  import { parseSessionFormatLogFilename } from "@deepseek-ai/dsh-session-format";
5
- import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
6
5
  import { balanceRewindPrefix } from "@morlay/session-branch";
7
6
  import { unzip } from "fflate";
8
7
  //#region src/import.ts
@@ -20,7 +19,7 @@ function parseJsonlArtifact(content) {
20
19
  }
21
20
  let restore;
22
21
  try {
23
- restore = sessionFormatCatalog.createRestore(header, {
22
+ restore = restoreCatalog.createRestore(header, {
24
23
  recovery: "strict",
25
24
  validation: "transformed"
26
25
  });
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { S as SCHEMA_VERSION, c as ProjectionCacheOptions, d as SessionPersistenceRdb, f as SessionPersistenceRdbInternals, h as locateTurnEnd, l as SessionDeletionError, m as SessionBranchRdbProvider, p as SessionBranchRdb, s as Config, u as SessionDeletionErrorCode } from "./backend-a0xDrUFG.mjs";
2
- export { Config, ProjectionCacheOptions, SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionDeletionError, SessionDeletionErrorCode, SessionPersistenceRdb, SessionPersistenceRdb as default, SessionPersistenceRdbInternals, locateTurnEnd };
1
+ import { D as SCHEMA_VERSION, _ as locateTurnEnd, b as SessionRowsItem, d as SessionDeletionError, f as SessionDeletionErrorCode, g as SessionBranchRdbProvider, h as SessionBranchRdb, l as Config, m as SessionPersistenceRdbInternals, p as SessionPersistenceRdb, u as ProjectionCacheOptions, x as SessionRowsValue, y as SESSION_ROWS_PATH } from "./backend-E0bmTXpz.mjs";
2
+ export { Config, ProjectionCacheOptions, SCHEMA_VERSION, SESSION_ROWS_PATH, SessionBranchRdb, SessionBranchRdbProvider, SessionDeletionError, SessionDeletionErrorCode, SessionPersistenceRdb, SessionPersistenceRdb as default, SessionPersistenceRdbInternals, type SessionRowsItem, type SessionRowsValue, locateTurnEnd };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { p as SCHEMA_VERSION } from "./sqlite-C3LHLzYW.mjs";
2
- import { n as SessionPersistenceRdb, t as SessionDeletionError } from "./src-Bn0038Sp.mjs";
3
- import { n as SessionBranchRdbProvider, r as locateTurnEnd, t as SessionBranchRdb } from "./branch-Dlmbkfjj.mjs";
4
- export { SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionDeletionError, SessionPersistenceRdb, SessionPersistenceRdb as default, locateTurnEnd };
1
+ import { p as SCHEMA_VERSION } from "./sqlite-zcXVyMxr.mjs";
2
+ import { n as SessionPersistenceRdb, r as SESSION_ROWS_PATH, t as SessionDeletionError } from "./src-B9X8BKyh.mjs";
3
+ import { n as SessionBranchRdbProvider, r as locateTurnEnd, t as SessionBranchRdb } from "./branch-BjUR6wfe.mjs";
4
+ export { SCHEMA_VERSION, SESSION_ROWS_PATH, SessionBranchRdb, SessionBranchRdbProvider, SessionDeletionError, SessionPersistenceRdb, SessionPersistenceRdb as default, locateTurnEnd };
@@ -24,7 +24,8 @@ function rowToMeta(row) {
24
24
  ...row.fParentSession !== null ? { parentSession: row.fParentSession } : {},
25
25
  isSeeded: row.fSeedLength !== null,
26
26
  ...row.fOrigin !== null ? { origin: row.fOrigin } : {},
27
- ...row.fDelegationDepth === null ? {} : { delegationDepth: row.fDelegationDepth }
27
+ ...row.fDelegationDepth === null ? {} : { delegationDepth: row.fDelegationDepth },
28
+ ...row.fAgentPreset === null ? {} : { agentPreset: row.fAgentPreset }
28
29
  };
29
30
  }
30
31
  function sessionInsertRow(storage, incarnation) {
@@ -40,6 +41,7 @@ function sessionInsertRow(storage, incarnation) {
40
41
  fSeedLength: meta.isSeeded ? storage.inheritedEventCount : null,
41
42
  fOrigin: meta.origin ?? null,
42
43
  fDelegationDepth: meta.delegationDepth ?? null,
44
+ fAgentPreset: meta.agentPreset ?? null,
43
45
  fIncarnation: incarnation,
44
46
  fRevision: 0
45
47
  };
@@ -53,7 +55,8 @@ function sessionConflictRow(storage) {
53
55
  fParentSession: meta.parentSession ?? null,
54
56
  fSeedLength: meta.isSeeded ? storage.inheritedEventCount : null,
55
57
  fOrigin: meta.origin ?? null,
56
- fDelegationDepth: meta.delegationDepth ?? null
58
+ fDelegationDepth: meta.delegationDepth ?? null,
59
+ fAgentPreset: meta.agentPreset ?? null
57
60
  };
58
61
  }
59
62
  function rowToEvent(row) {
@@ -144,28 +147,15 @@ function toolResultRewriteContentOnly(original, replacement) {
144
147
  const replacementData = replacement.data;
145
148
  const originalMessage = originalData["message"];
146
149
  const replacementMessage = replacementData["message"];
147
- const originalContent = Array.isArray(originalMessage?.content) ? originalMessage.content : void 0;
148
- const replacementContent = Array.isArray(replacementMessage?.content) ? replacementMessage.content : void 0;
149
- if (originalContent === void 0 || replacementContent === void 0) return false;
150
- return isDeepEqualJson({
151
- ...originalData,
152
- message: {
153
- ...originalMessage,
154
- content: [{
155
- ...originalContent[0],
156
- content: null
157
- }]
158
- }
159
- }, {
160
- ...replacementData,
150
+ if (!Array.isArray(originalMessage?.content) || !Array.isArray(replacementMessage?.content)) return false;
151
+ const rest = (data, message) => ({
152
+ ...data,
161
153
  message: {
162
- ...replacementMessage,
163
- content: [{
164
- ...replacementContent[0],
165
- content: null
166
- }]
154
+ ...message,
155
+ content: null
167
156
  }
168
157
  });
158
+ return isDeepEqualJson(rest(originalData, originalMessage), rest(replacementData, replacementMessage));
169
159
  }
170
160
  function findSurfaceRepairs(events) {
171
161
  const nodes = [];
@@ -355,6 +345,87 @@ function renameLegacyPtcEvents(events) {
355
345
  }
356
346
  }
357
347
  }
348
+ /**
349
+ * 把**旧代(v3 及更早)的消息形状**归一到当前格式(v4):迁移链(严格)拒绝那些形状时,读路径仍要
350
+ * 能把它们读出来——只动字段与标记,不改坐标、不改内容。
351
+ *
352
+ * 三件事:
353
+ * - `system/message` 的 source:`{ kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' }` → `{ kind: 'system-prompt' }`
354
+ * (v4 起 system 消息只认这个 kind);
355
+ * - `tool/result` 的消息:v3 是 user 角色、结果块包在 `content[0]`;v4 是 `role: 'tool'` + 顶层
356
+ * `toolCallId` + `content` 就是结果块;
357
+ * - 本仓库自己写过、上游已删除的事件类型(见 `LEGACY_OWN_EVENT_TYPES`)标 `ignorable: true`——v4 的
358
+ * 校验对未知类型只接受可忽略信封。
359
+ *
360
+ * 只认这张白名单:**其它未知类型保持 fail loud**(那是「数据来自更新的 harness」的信号,不能吞掉)。
361
+ */
362
+ function normalizeToCurrentShape(events) {
363
+ for (const event of events) {
364
+ const type = event.type;
365
+ if (LEGACY_OWN_EVENT_TYPES.has(type)) {
366
+ event.ignorable = true;
367
+ continue;
368
+ }
369
+ if (type === "system/message") {
370
+ const message = event.data["message"];
371
+ liftSystemMessageSource(message);
372
+ continue;
373
+ }
374
+ if (type === "tool/result") {
375
+ const message = event.data["message"];
376
+ liftToolResultMessage(message);
377
+ }
378
+ }
379
+ }
380
+ function liftSystemMessageSource(message) {
381
+ if (typeof message !== "object" || message === null) return;
382
+ const record = message;
383
+ const source = record["source"];
384
+ if (typeof source !== "object" || source === null) return;
385
+ if (source["kind"] !== "plugin") return;
386
+ record["source"] = { kind: "system-prompt" };
387
+ }
388
+ function liftToolResultMessage(message) {
389
+ if (typeof message !== "object" || message === null) return;
390
+ const record = message;
391
+ if (record["role"] === "tool" && typeof record["toolCallId"] === "string") return;
392
+ const source = record["source"];
393
+ const block = Array.isArray(record["content"]) ? record["content"][0] : void 0;
394
+ if (typeof block !== "object" || block === null) return;
395
+ const lifted = block;
396
+ const callId = lifted["toolCallId"] ?? source?.["callId"];
397
+ if (typeof callId !== "string") return;
398
+ record["role"] = "tool";
399
+ record["toolCallId"] = callId;
400
+ record["content"] = Array.isArray(lifted["content"]) ? lifted["content"] : [];
401
+ if (typeof lifted["isError"] === "boolean") record["isError"] = lifted["isError"];
402
+ record["source"] = {
403
+ kind: "tool",
404
+ callId
405
+ };
406
+ }
407
+ /**
408
+ * 本仓库写过、后来连同机制一起删除的事件类型:读的时候标 `ignorable: true` 让校验跳过它们。
409
+ * 新增历史类型时登记到这里——**不要**用「所有未知类型」代替(那会掩盖更新版本的未知事件)。
410
+ */
411
+ const LEGACY_OWN_EVENT_TYPES = /* @__PURE__ */ new Set(["session-branch/version"]);
412
+ /** 行里是否还带着旧代消息形状(`session-branch/version` 这类自造事件也算)——写路径曾把回退视图的结果
413
+ * 以当前版本号落库,所以**版本号不可信**,读之前要按内容判一次。 */
414
+ function hasLegacyShape(events) {
415
+ for (const event of events) {
416
+ const type = event.type;
417
+ if (LEGACY_OWN_EVENT_TYPES.has(type)) return true;
418
+ if (type === "system/message") {
419
+ if ((event.data["message"]?.["source"])?.["kind"] === "plugin") return true;
420
+ continue;
421
+ }
422
+ if (type === "tool/result") {
423
+ const message = event.data["message"];
424
+ if (message !== void 0 && message["role"] !== "tool") return true;
425
+ }
426
+ }
427
+ return false;
428
+ }
358
429
  function repairReadView(events) {
359
430
  repairAssistantSettlement(events);
360
431
  repairRequestHeaders(events);
@@ -512,4 +583,4 @@ function usageRowOf(event) {
512
583
  };
513
584
  }
514
585
  //#endregion
515
- export { toJsonlArtifact as _, repairAssistantSettlement as a, repairRequestHeaders as c, rowToMeta as d, scanRows as f, titleOfEventData as g, syncMeteringRanges as h, renameLegacyPtcEvents as i, repairSurfaceOps as l, sessionInsertRow as m, orphanInboxSpliceSeqs as n, repairOrphanInboxSplices as o, sessionConflictRow as p, recomputeReplaceProvenance as r, repairReadView as s, findSurfaceRepairs as t, rowToEvent as u, usageRowOf as v };
586
+ export { sessionInsertRow as _, orphanInboxSpliceSeqs as a, toJsonlArtifact as b, repairAssistantSettlement as c, repairRequestHeaders as d, repairSurfaceOps as f, sessionConflictRow as g, scanRows as h, normalizeToCurrentShape as i, repairOrphanInboxSplices as l, rowToMeta as m, findSurfaceRepairs as n, recomputeReplaceProvenance as o, rowToEvent as p, hasLegacyShape as r, renameLegacyPtcEvents as s, LEGACY_OWN_EVENT_TYPES as t, repairReadView as u, syncMeteringRanges as v, usageRowOf as x, titleOfEventData as y };
@@ -1,4 +1,4 @@
1
- import { g as titleOfEventData, m as sessionInsertRow, p as sessionConflictRow, v as usageRowOf } from "./log-C-rbdFwK.mjs";
1
+ import { _ as sessionInsertRow, g as sessionConflictRow, x as usageRowOf, y as titleOfEventData } from "./log-qCNkDjh5.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
4
4
  import { and, desc, eq, gte, inArray, isNotNull, lt, notInArray, sql } from "drizzle-orm";
@@ -16,6 +16,10 @@ var WriteGuard = class {
16
16
  confirmHead(id, head) {
17
17
  this.headSeqs.set(id, head);
18
18
  }
19
+ /** 本实例有没有这条会话的 head 记录(迁移重写用它决定是"登记基准"还是"严格比较")。 */
20
+ has(id) {
21
+ return this.headSeqs.has(id);
22
+ }
19
23
  assertNoConcurrentWriter(id, storedHead) {
20
24
  const known = this.headSeqs.get(id);
21
25
  if (known === void 0) {
@@ -189,6 +193,7 @@ const sessions = {
189
193
  f_seed_length: { type: "integer" },
190
194
  f_origin: { type: "text" },
191
195
  f_delegation_depth: { type: "integer" },
196
+ f_agent_preset: { type: "text" },
192
197
  f_incarnation: {
193
198
  type: "text",
194
199
  notNull: true
@@ -198,6 +203,8 @@ const sessions = {
198
203
  notNull: true
199
204
  },
200
205
  f_archived_at: { type: "bigint" },
206
+ f_pinned_seq: { type: "integer" },
207
+ f_last_event_at: { type: "bigint" },
201
208
  f_title: { type: "text" },
202
209
  f_title_seq: { type: "integer" }
203
210
  }
@@ -468,23 +475,28 @@ const eventUsage = {
468
475
  f_model: { type: "text" },
469
476
  f_input_tokens: {
470
477
  type: "integer",
471
- notNull: true
478
+ notNull: true,
479
+ default: 0
472
480
  },
473
481
  f_output_tokens: {
474
482
  type: "integer",
475
- notNull: true
483
+ notNull: true,
484
+ default: 0
476
485
  },
477
486
  f_cache_read_tokens: {
478
487
  type: "integer",
479
- notNull: true
488
+ notNull: true,
489
+ default: 0
480
490
  },
481
491
  f_reasoning_tokens: {
482
492
  type: "integer",
483
- notNull: true
493
+ notNull: true,
494
+ default: 0
484
495
  },
485
496
  f_total_tokens: {
486
497
  type: "integer",
487
- notNull: true
498
+ notNull: true,
499
+ default: 0
488
500
  }
489
501
  },
490
502
  indexes: { idx_event_usage_created_at: { columns: ["f_created_at"] } }
@@ -616,7 +628,7 @@ function eventDimensions(event) {
616
628
  kind,
617
629
  role: "tool",
618
630
  name: "",
619
- actionId: data["message"]?.content?.[0]?.toolCallId ?? ""
631
+ actionId: data["message"]?.toolCallId ?? ""
620
632
  };
621
633
  case "tool/call": return {
622
634
  kind,
@@ -962,11 +974,16 @@ function createStorageRepository(host) {
962
974
  if (row === void 0) return null;
963
975
  const ordered = await allRows(db.select().from(tWorkspaces).where(gte(tWorkspaces.fPosition, 0)).orderBy(tWorkspaces.fPosition));
964
976
  const archives = await allRows(db.select({ fSessionId: tSessions.fSessionId }).from(tSessions).where(isNotNull(tSessions.fArchivedAt)).orderBy(tSessions.fArchivedAt));
977
+ const pinned = await allRows(db.select({
978
+ fSessionId: tSessions.fSessionId,
979
+ fPinnedSeq: tSessions.fPinnedSeq
980
+ }).from(tSessions).where(isNotNull(tSessions.fPinnedSeq)).orderBy(tSessions.fPinnedSeq));
965
981
  const pendingMutation = pendingMutationOf(row);
966
982
  return {
967
983
  initialized: row.fInitialized !== 0,
968
984
  workspaceIds: ordered.map((entry) => entry.fWorkspaceId),
969
985
  archivedSessionIds: archives.map((entry) => entry.fSessionId),
986
+ pinnedSessionIds: pinned.map((entry) => entry.fSessionId),
970
987
  ...pendingMutation === void 0 ? {} : { pendingMutation }
971
988
  };
972
989
  },
@@ -986,6 +1003,9 @@ function createStorageRepository(host) {
986
1003
  }));
987
1004
  await runQuery(db.update(tWorkspaces).set({ fPosition: -1 }));
988
1005
  for (const [position, workspaceId] of state.workspaceIds.entries()) await runQuery(db.update(tWorkspaces).set({ fPosition: position }).where(eq(tWorkspaces.fWorkspaceId, workspaceId)));
1006
+ const pinned = state.pinnedSessionIds ?? [];
1007
+ await runQuery(pinned.length === 0 ? db.update(tSessions).set({ fPinnedSeq: null }).where(isNotNull(tSessions.fPinnedSeq)) : db.update(tSessions).set({ fPinnedSeq: null }).where(and(isNotNull(tSessions.fPinnedSeq), notInArray(tSessions.fSessionId, pinned))));
1008
+ for (const [position, sessionId] of pinned.entries()) await runQuery(db.update(tSessions).set({ fPinnedSeq: position }).where(eq(tSessions.fSessionId, sessionId)));
989
1009
  const archived = state.archivedSessionIds;
990
1010
  await runQuery(archived.length === 0 ? db.update(tSessions).set({ fArchivedAt: null }).where(isNotNull(tSessions.fArchivedAt)) : db.update(tSessions).set({ fArchivedAt: null }).where(and(isNotNull(tSessions.fArchivedAt), notInArray(tSessions.fSessionId, archived))));
991
1011
  const stamp = Date.now();
@@ -1141,6 +1161,19 @@ function backfillEventUsage(db) {
1141
1161
  AND (json_extract(e.f_data, '$.data.usage') IS NOT NULL
1142
1162
  OR json_extract(e.f_data, '$.usage') IS NOT NULL)`);
1143
1163
  }
1164
+ function sessionListRowOf(row) {
1165
+ return {
1166
+ sessionId: typeof row["session_id"] === "string" ? row["session_id"] : String(row["session_id"]),
1167
+ title: typeof row["title"] === "string" ? row["title"] : null,
1168
+ origin: typeof row["origin"] === "string" ? row["origin"] : null,
1169
+ cwd: typeof row["cwd"] === "string" ? row["cwd"] : null,
1170
+ createdAt: Number(row["created_at"]),
1171
+ updatedAt: Number(row["updated_at"]),
1172
+ archived: row["archived"] === 1 || row["archived"] === true,
1173
+ subagent: row["origin"] === "subagent",
1174
+ workspace: typeof row["workspace"] === "string" ? row["workspace"] : null
1175
+ };
1176
+ }
1144
1177
  /** SQL 的缺失求和是 NULL:没有 usage 字段的行使该字段计 0。 */
1145
1178
  function tokenTotalsOf(row) {
1146
1179
  return {
@@ -1369,7 +1402,7 @@ var SqliteBackend = class SqliteBackend {
1369
1402
  insertEvents: (events) => this.insertEvents(events),
1370
1403
  insertBridges: (rows) => this.insertBridges(rows),
1371
1404
  updateHead: (id, headEventId, headSequence) => this.updateHead(id, headEventId, headSequence),
1372
- bumpRevision: (id) => this.bumpRevision(id),
1405
+ bumpRevision: (id, lastEventAt) => this.bumpRevision(id, lastEventAt),
1373
1406
  refreshTitle: (id) => this.refreshTitle(id),
1374
1407
  deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(id, fromSequence),
1375
1408
  getPrevBridge: (id, sequence) => this.getPrevBridge(id, sequence),
@@ -1418,8 +1451,11 @@ var SqliteBackend = class SqliteBackend {
1418
1451
  fHeadSequence: headSequence
1419
1452
  }).where(eq(tSessions.fSessionId, id)).run();
1420
1453
  }
1421
- async bumpRevision(id) {
1422
- this.db.update(tSessions).set({ fRevision: sql`${tSessions.fRevision} + 1` }).where(eq(tSessions.fSessionId, id)).run();
1454
+ async bumpRevision(id, lastEventAt) {
1455
+ this.db.update(tSessions).set({
1456
+ fRevision: sql`${tSessions.fRevision} + 1`,
1457
+ ...lastEventAt === void 0 ? {} : { fLastEventAt: sql`MAX(COALESCE(${tSessions.fLastEventAt}, 0), ${lastEventAt})` }
1458
+ }).where(eq(tSessions.fSessionId, id)).run();
1423
1459
  }
1424
1460
  async refreshTitle(id) {
1425
1461
  const row = this.db.select({
@@ -1474,6 +1510,45 @@ var SqliteBackend = class SqliteBackend {
1474
1510
  * 沿 `t_events` 的事件类型数——两者限定在**有 token 用量的会话**里,时间范围各自按自己的
1475
1511
  * `f_created_at`(同一时刻写入,口径一致)。
1476
1512
  */
1513
+ async listSessionRows(query = {}) {
1514
+ const needle = query.query?.trim().toLowerCase() ?? "";
1515
+ const like = `%${needle}%`;
1516
+ const where = `WHERE (? = '' OR lower(COALESCE(s.f_title, '')) LIKE ? OR lower(COALESCE(w.f_title, '')) LIKE ?)
1517
+ AND (? = 1 OR s.f_origin IS NOT 'subagent')`;
1518
+ const filterParams = [
1519
+ needle,
1520
+ like,
1521
+ like,
1522
+ query.includeSubagents === true ? 1 : 0
1523
+ ];
1524
+ const limit = query.limit ?? 20;
1525
+ const offset = query.offset ?? 0;
1526
+ const rows = this.db.$client.prepare(`SELECT s.f_session_id AS session_id,
1527
+ s.f_title AS title,
1528
+ s.f_origin AS origin,
1529
+ s.f_cwd AS cwd,
1530
+ s.f_created_at AS created_at,
1531
+ (s.f_archived_at IS NOT NULL) AS archived,
1532
+ COALESCE(s.f_last_event_at, s.f_created_at) AS updated_at,
1533
+ w.f_title AS workspace
1534
+ FROM t_sessions s
1535
+ LEFT JOIN t_workspaces w ON w.f_workspace_id = (
1536
+ SELECT b.f_workspace_id FROM t_workspace_sessions b
1537
+ WHERE b.f_session_id = s.f_session_id ORDER BY b.f_position LIMIT 1)
1538
+ ${where}
1539
+ ORDER BY updated_at DESC, s.f_session_id
1540
+ LIMIT ? OFFSET ?`).all(...filterParams, limit, offset);
1541
+ const counted = this.db.$client.prepare(`SELECT count(*) AS n
1542
+ FROM t_sessions s
1543
+ LEFT JOIN t_workspaces w ON w.f_workspace_id = (
1544
+ SELECT b.f_workspace_id FROM t_workspace_sessions b
1545
+ WHERE b.f_session_id = s.f_session_id ORDER BY b.f_position LIMIT 1)
1546
+ ${where}`).get(...filterParams);
1547
+ return {
1548
+ items: rows.map((row) => sessionListRowOf(row)),
1549
+ total: Number(counted.n)
1550
+ };
1551
+ }
1477
1552
  async usageReport(sinceMs) {
1478
1553
  const usageSince = sinceMs === void 0 ? "" : " AND u.f_created_at >= ?";
1479
1554
  const params = sinceMs === void 0 ? [] : [sinceMs];