@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.
- package/README.md +53 -27
- package/dist/artifact.d.mts +27 -2
- package/dist/artifact.mjs +2 -2
- package/dist/{backend-a0xDrUFG.d.mts → backend-E0bmTXpz.d.mts} +233 -27
- package/dist/{branch-Dlmbkfjj.mjs → branch-BjUR6wfe.mjs} +31 -15
- package/dist/deletion.d.mts +1 -1
- package/dist/deletion.mjs +1 -1
- package/dist/import.d.mts +1 -1
- package/dist/import.mjs +2 -3
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +4 -4
- package/dist/{log-C-rbdFwK.mjs → log-qCNkDjh5.mjs} +93 -22
- package/dist/{sqlite-C3LHLzYW.mjs → sqlite-zcXVyMxr.mjs} +85 -10
- package/dist/{src-Bn0038Sp.mjs → src-B9X8BKyh.mjs} +210 -38
- package/dist/storage.d.mts +2 -1
- package/dist/storage.mjs +1 -1
- package/dist/testing.d.mts +2 -11
- package/dist/testing.mjs +12 -29
- package/drizzle/postgres/20260921075900_v3_agent_preset/migration.sql +1 -0
- package/drizzle/postgres/20260922090001_v3_pinned_seq/migration.sql +2 -0
- package/drizzle/postgres/20260922093001_v3_last_event_at/migration.sql +10 -0
- package/drizzle/sqlite/20260921075855_v3_agent_preset/migration.sql +7 -0
- package/drizzle/sqlite/20260921075855_v3_agent_preset/snapshot.json +1041 -0
- package/drizzle/sqlite/20260922090000_v3_pinned_seq/migration.sql +4 -0
- package/drizzle/sqlite/20260922093000_v3_last_event_at/migration.sql +13 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +24 -22
- package/src/backend.ts +46 -1
- package/src/branch.ts +28 -11
- package/src/drizzle/postgres-v3.ts +1 -0
- package/src/drizzle/sqlite-v3.ts +1 -0
- package/src/entities/v3/event-usage.ts +5 -5
- package/src/entities/v3/sessions.ts +5 -0
- package/src/import.ts +3 -3
- package/src/index.ts +56 -22
- package/src/legacy.ts +14 -3
- package/src/log.ts +107 -22
- package/src/postgres.ts +65 -5
- package/src/rows.ts +125 -0
- package/src/schema.ts +3 -2
- package/src/session-query.ts +27 -1
- package/src/sqlite.ts +71 -3
- package/src/storage-takeover/projection-cache.ts +40 -15
- package/src/storage-takeover/repository.ts +34 -0
- package/src/storage-takeover/storage-backend.ts +3 -1
- package/src/testing/coordinator-contract.ts +2 -1
- package/src/testing.ts +0 -1
- package/src/write-guard.ts +5 -0
- 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.
|
|
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 (
|
|
144
|
+
if (
|
|
145
|
+
record === undefined ||
|
|
146
|
+
!predecessorIdentityMatches(record.identity, lifecycleIdentityOf(meta))
|
|
147
|
+
) {
|
|
133
148
|
return undefined;
|
|
134
149
|
}
|
|
135
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
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";
|
package/src/write-guard.ts
CHANGED
|
@@ -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) {
|
package/src/testing/helpers.ts
DELETED
|
@@ -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
|
-
}
|