@morlay/session-rdb 0.0.21-alpha.4 → 0.0.21-alpha.6

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 (48) 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 +2 -19
  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/package.json +21 -21
  27. package/src/backend.ts +46 -1
  28. package/src/branch.ts +28 -11
  29. package/src/drizzle/postgres-v3.ts +1 -0
  30. package/src/drizzle/sqlite-v3.ts +1 -0
  31. package/src/entities/v3/event-usage.ts +5 -5
  32. package/src/entities/v3/sessions.ts +5 -0
  33. package/src/import.ts +3 -3
  34. package/src/index.ts +56 -22
  35. package/src/legacy.ts +14 -3
  36. package/src/log.ts +107 -22
  37. package/src/postgres.ts +65 -5
  38. package/src/rows.ts +125 -0
  39. package/src/schema.ts +3 -2
  40. package/src/session-query.ts +27 -1
  41. package/src/sqlite.ts +71 -3
  42. package/src/storage-takeover/projection-cache.ts +40 -15
  43. package/src/storage-takeover/repository.ts +34 -0
  44. package/src/storage-takeover/storage-backend.ts +3 -1
  45. package/src/testing/coordinator-contract.ts +2 -1
  46. package/src/testing.ts +0 -1
  47. package/src/write-guard.ts +5 -0
  48. package/src/testing/helpers.ts +0 -20
@@ -27,6 +27,12 @@ type CurrentCheckpointIdentity = CheckpointIdentity & {
27
27
  inheritedEventCount: number;
28
28
  };
29
29
 
30
+ /** header 能证明的生命周期身份(上游 0.1.7 的 header-only 匹配口径)。 */
31
+ type LifecycleIdentity = CheckpointIdentity & {
32
+ formatVersion: number;
33
+ isSeeded: boolean;
34
+ };
35
+
30
36
  export interface ProjectionCacheConfig {
31
37
  writeEveryEvents: number;
32
38
 
@@ -95,16 +101,26 @@ export class SessionProjectionCacheRdb extends Service {
95
101
  return identityMatches(record.identity, expected) ? record : undefined;
96
102
  }
97
103
 
104
+ /**
105
+ * header-only 读(会话列表这类):上游 0.1.7 起只给 header 与 keys——它要求的是**生命周期**身份,
106
+ * 不再要求调用方提供 inherited cut(那个 cut 只有写路径与 hydration 用得上)。
107
+ */
98
108
  cachedSnapshot(
99
109
  meta: SessionHeader,
100
- inheritedEventCount: SessionLogOffset,
101
110
  keys?: readonly Extract<keyof SessionProjectionMap, string>[],
102
111
  ): ProjectionSnapshot | undefined {
103
- const record = this.recordFor(meta.id, identityOf(meta, inheritedEventCount));
112
+ const record = this.recordForLifecycle(meta);
104
113
  const snapshot = record === undefined ? undefined : this.viewRecord(record, keys);
105
114
  return this.withDirectTitle(meta.id, keys, snapshot);
106
115
  }
107
116
 
117
+ private recordForLifecycle(meta: SessionHeader): StoredProjcacheEntry | undefined {
118
+ const record = this.lookup(meta.id);
119
+ return record === undefined || !identityMatches(record.identity, lifecycleIdentityOf(meta))
120
+ ? undefined
121
+ : record;
122
+ }
123
+
108
124
  private withDirectTitle(
109
125
  id: SessionId,
110
126
  keys: readonly Extract<keyof SessionProjectionMap, string>[] | undefined,
@@ -123,17 +139,15 @@ export class SessionProjectionCacheRdb extends Service {
123
139
  return { asOfSeq: asOfSeq as SessionSeqCursor, values };
124
140
  }
125
141
 
126
- cachedPredecessorTitle(
127
- meta: SessionHeader,
128
- inheritedEventCount: SessionLogOffset,
129
- ): ProjectionSnapshot | undefined {
130
- const expected = identityOf(meta, inheritedEventCount);
142
+ cachedPredecessorTitle(meta: SessionHeader): ProjectionSnapshot | undefined {
131
143
  const record = this.lookup(meta.id);
132
- if (record === undefined || !predecessorIdentityMatches(record.identity, expected)) {
144
+ if (
145
+ record === undefined ||
146
+ !predecessorIdentityMatches(record.identity, lifecycleIdentityOf(meta))
147
+ ) {
133
148
  return undefined;
134
149
  }
135
- const title = this.viewRecord(record, [PREDECESSOR_TITLE_KEY]);
136
- return title === undefined ? undefined : { ...title, asOfSeq: -1 };
150
+ return this.viewRecord(record, [PREDECESSOR_TITLE_KEY]);
137
151
  }
138
152
 
139
153
  private viewRecord(
@@ -322,6 +336,16 @@ function detachJson(
322
336
  return JSON.parse(text) as Record<string, ProjectionCheckpointRow>;
323
337
  }
324
338
 
339
+ /** header 能证明的生命周期身份(不含 inherited cut):header-only 读只比对它。 */
340
+ function lifecycleIdentityOf(header: SessionHeader): LifecycleIdentity {
341
+ return {
342
+ formatVersion: header.version,
343
+ createdAt: header.createdAt,
344
+ ...(header.cwd === undefined ? {} : { cwd: header.cwd }),
345
+ isSeeded: header.isSeeded,
346
+ };
347
+ }
348
+
325
349
  function identityOf(
326
350
  header: SessionHeader,
327
351
  inheritedEventCount: SessionLogOffset,
@@ -339,7 +363,7 @@ function identityOf(
339
363
  };
340
364
  }
341
365
 
342
- function identityMatches(stored: CheckpointIdentity, expected: CurrentCheckpointIdentity): boolean {
366
+ function identityMatches(stored: CheckpointIdentity, expected: LifecycleIdentity): boolean {
343
367
  return (
344
368
  stored.formatVersion === expected.formatVersion && lifecycleIdentityMatches(stored, expected)
345
369
  );
@@ -347,7 +371,7 @@ function identityMatches(stored: CheckpointIdentity, expected: CurrentCheckpoint
347
371
 
348
372
  function predecessorIdentityMatches(
349
373
  stored: CheckpointIdentity,
350
- expected: CurrentCheckpointIdentity,
374
+ expected: LifecycleIdentity,
351
375
  ): boolean {
352
376
  const predecessor =
353
377
  stored.formatVersion === undefined || stored.formatVersion < expected.formatVersion;
@@ -356,12 +380,13 @@ function predecessorIdentityMatches(
356
380
 
357
381
  function lifecycleIdentityMatches(
358
382
  stored: CheckpointIdentity,
359
- expected: CurrentCheckpointIdentity,
383
+ expected: LifecycleIdentity,
360
384
  ): boolean {
385
+ // header-only 读的匹配口径(上游 0.1.7):只看 formatVersion 与生命周期字段,**不比对 inherited cut**
386
+ // ——那个 cut 只有写路径与 hydration 用得上。
361
387
  return (
362
388
  stored.createdAt === expected.createdAt &&
363
389
  stored.cwd === expected.cwd &&
364
- (stored.isSeeded ?? false) === expected.isSeeded &&
365
- (stored.inheritedEventCount ?? 0) === expected.inheritedEventCount
390
+ (stored.isSeeded ?? false) === expected.isSeeded
366
391
  );
367
392
  }
@@ -219,11 +219,19 @@ export function createStorageRepository(host: StorageRepositoryHost): StorageRep
219
219
  .where(isNotNull(tSessions.fArchivedAt))
220
220
  .orderBy(tSessions.fArchivedAt),
221
221
  );
222
+ const pinned = await allRows<{ fSessionId: string; fPinnedSeq: number | null }>(
223
+ db
224
+ .select({ fSessionId: tSessions.fSessionId, fPinnedSeq: tSessions.fPinnedSeq })
225
+ .from(tSessions)
226
+ .where(isNotNull(tSessions.fPinnedSeq))
227
+ .orderBy(tSessions.fPinnedSeq),
228
+ );
222
229
  const pendingMutation = pendingMutationOf(row);
223
230
  return {
224
231
  initialized: row.fInitialized !== 0,
225
232
  workspaceIds: ordered.map((entry) => entry.fWorkspaceId as WorkspaceId),
226
233
  archivedSessionIds: archives.map((entry) => entry.fSessionId as SessionId),
234
+ pinnedSessionIds: pinned.map((entry) => entry.fSessionId as SessionId),
227
235
  ...(pendingMutation === undefined ? {} : { pendingMutation }),
228
236
  };
229
237
  },
@@ -263,6 +271,32 @@ export function createStorageRepository(host: StorageRepositoryHost): StorageRep
263
271
  );
264
272
  }
265
273
 
274
+ // 钉住:未在集合里的清掉,集合里的按数组位置写序号(读回即按它排序)。
275
+ // 找不到会话行的 id 跳过:pin 只作用于已存在的会话(与归档不同,不凭空造行)。
276
+ // 旧 storages 文档(0.1.6 时代)没有这个集合,导入路径按空处理。
277
+ const pinned = state.pinnedSessionIds ?? [];
278
+ await runQuery(
279
+ pinned.length === 0
280
+ ? db
281
+ .update(tSessions)
282
+ .set({ fPinnedSeq: null })
283
+ .where(isNotNull(tSessions.fPinnedSeq))
284
+ : db
285
+ .update(tSessions)
286
+ .set({ fPinnedSeq: null })
287
+ .where(
288
+ and(isNotNull(tSessions.fPinnedSeq), notInArray(tSessions.fSessionId, pinned)),
289
+ ),
290
+ );
291
+ for (const [position, sessionId] of pinned.entries()) {
292
+ await runQuery(
293
+ db
294
+ .update(tSessions)
295
+ .set({ fPinnedSeq: position })
296
+ .where(eq(tSessions.fSessionId, sessionId)),
297
+ );
298
+ }
299
+
266
300
  const archived = state.archivedSessionIds;
267
301
  await runQuery(
268
302
  archived.length === 0
@@ -178,7 +178,8 @@ function workspaceStateOf(value: unknown): WorkspaceDomainState {
178
178
  state === null ||
179
179
  typeof state.initialized !== "boolean" ||
180
180
  !Array.isArray(state.workspaceIds) ||
181
- !Array.isArray(state.archivedSessionIds)
181
+ !Array.isArray(state.archivedSessionIds) ||
182
+ !Array.isArray(state.pinnedSessionIds)
182
183
  ) {
183
184
  throw new TypeError("workspace registry state does not match the stored shape");
184
185
  }
@@ -186,6 +187,7 @@ function workspaceStateOf(value: unknown): WorkspaceDomainState {
186
187
  initialized: state.initialized,
187
188
  workspaceIds: state.workspaceIds.map((id) => id as WorkspaceId),
188
189
  archivedSessionIds: state.archivedSessionIds.map((id) => id as SessionId),
190
+ pinnedSessionIds: state.pinnedSessionIds.map((id) => id as SessionId),
189
191
  ...(state.pendingMutation === undefined ? {} : { pendingMutation: state.pendingMutation }),
190
192
  };
191
193
  }
@@ -143,7 +143,8 @@ export function runCoordinatorContract(
143
143
  { surfaceOp: "append" },
144
144
  );
145
145
  expect(() => {
146
- (ev.data as { content: { type: "text"; text: string }[] }).content[0]!.text = "HACKED";
146
+ (ev.data as unknown as { content: { type: "text"; text: string }[] }).content[0]!.text =
147
+ "HACKED";
147
148
  }).toThrow(TypeError);
148
149
  session.append("turn/end", { turn: 1, reason: { kind: "completed" } });
149
150
  await ctx.sessions.flush(session);
package/src/testing.ts CHANGED
@@ -1,4 +1,3 @@
1
- export { EmptySettings } from "./testing/helpers.ts";
2
1
  export type { ContractBackend } from "./testing/contract.ts";
3
2
  export { appendLog, meta, oneTurnLog, runPersistenceContract } from "./testing/contract.ts";
4
3
  export type { CoordinatorFixture } from "./testing/coordinator-contract.ts";
@@ -7,6 +7,11 @@ export class WriteGuard {
7
7
  this.headSeqs.set(id, head);
8
8
  }
9
9
 
10
+ /** 本实例有没有这条会话的 head 记录(迁移重写用它决定是"登记基准"还是"严格比较")。 */
11
+ has(id: SessionId): boolean {
12
+ return this.headSeqs.has(id);
13
+ }
14
+
10
15
  assertNoConcurrentWriter(id: SessionId, storedHead: number): void {
11
16
  const known = this.headSeqs.get(id);
12
17
  if (known === undefined) {
@@ -1,20 +0,0 @@
1
- import { Context } from "@deepseek-ai/cordis";
2
- import { SettingsProvider, type SettingsNamespace } from "@deepseek-ai/dsh-settings";
3
-
4
- export class EmptySettings extends SettingsProvider {
5
- constructor(ctx: Context) {
6
- super(ctx);
7
- }
8
-
9
- get writable(): boolean {
10
- return true;
11
- }
12
-
13
- protected async load(): Promise<Record<string, unknown>> {
14
- return {};
15
- }
16
-
17
- protected persist(_ns: SettingsNamespace, _section: Record<string, unknown>): Promise<void> {
18
- return Promise.resolve();
19
- }
20
- }