From c26ca6acb639fbbeac8ef5624338cb5c19d9e0f7 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Thu, 20 Aug 2026 10:49:43 +0800 Subject: [PATCH] fix(session-projection-cache): drain in-flight writes on disposal; sync stale lockfile and generated docs - Track fire-and-forget durable writes and await them at plugin disposal so a late flush can never land after teardown (fixes the ENOTEMPTY cleanup race in the disposal test). - Drop the now-async-less Service.init and flushSoft void operators to keep lint clean, and remove the redundant dsh-storage-json devDependency. - Regenerate the stale pnpm lockfile and the config/persistence/module-graph catalogs (with zh mirrors and pairing records) that the per-session cache merge left out of sync, and fix the session-projection type-equiv doc blocks to match the source. - Add coverage for the unrelated-log-identity and no-per-session-directory (sqlite) cold-read paths. --- docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 16 +++---- docs/config-catalog.zh.md | 16 +++---- docs/module-graph.i18n.yaml | 4 +- docs/module-graph.md | 3 +- docs/module-graph.zh.md | 3 +- docs/persistence-catalog.i18n.yaml | 4 +- docs/persistence-catalog.md | 4 +- docs/persistence-catalog.zh.md | 4 +- docs/subsystems/session-projection.i18n.yaml | 4 +- docs/subsystems/session-projection.md | 8 ++-- docs/subsystems/session-projection.zh.md | 8 ++-- .../session-projection-cache/package.json | 5 +-- .../session-projection-cache/src/index.ts | 34 +++++++++----- .../tests/cache.spec.ts | 44 ++++++++++++++++++- pnpm-lock.yaml | 3 -- 16 files changed, 102 insertions(+), 62 deletions(-) diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 3b2d7bd652..31775c0ed5 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: 1d9a4a7b0839b92ac7445a1a4cfb74fa70f0227f -config-catalog.zh.md: b93bcd9c9ff9776ef5b5f7c2b467e596eee13cff +config-catalog.md: f0f989a56dd063c583694cfc08ffa6a023fa7936 +config-catalog.zh.md: a371c6d09f0aa73623646f30d36ced37effa0183 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 1d9a4a7b08..f0f989a56d 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -632,7 +632,7 @@ Source: [`packages/fs/fs-sandbox/src/index.ts:49`](../packages/fs/fs-sandbox/src ## `@deepseek-ai/dsh-goal` -Requires: `agents` · `sessionProjections` +Requires: `agents` ```ts config-catalog /** Deployment defaults for goal creation. */ @@ -642,7 +642,7 @@ export interface Config { } ``` -Source: [`packages/goal/goal/src/index.ts:115`](../packages/goal/goal/src/index.ts) +Source: [`packages/goal/goal/src/index.ts:116`](../packages/goal/goal/src/index.ts) @@ -1398,7 +1398,7 @@ export interface PresetSpec { Depends on: [`ApprovalPolicy`](subsystems/approval.md) · [`SandboxMode`](subsystems/sandbox.md) -Source: [`packages/interaction/permission-presets/src/index.ts:116`](../packages/interaction/permission-presets/src/index.ts) +Source: [`packages/interaction/permission-presets/src/index.ts:112`](../packages/interaction/permission-presets/src/index.ts) @@ -1702,7 +1702,7 @@ Source: [`packages/session/session-persistence-sqlite/src/index.ts:70`](../packa ## `@deepseek-ai/dsh-session-projection-cache` -Requires: `storageDomain` · `sessionProjections` · `sessionPersistence` · `sessions` +Requires: `sessionProjections` · `sessionPersistence` · `sessions` ```ts config-catalog /** @@ -1719,13 +1719,13 @@ export interface Config { } ``` -Source: [`packages/session/session-projection-cache/src/index.ts:42`](../packages/session/session-projection-cache/src/index.ts) +Source: [`packages/session/session-projection-cache/src/index.ts:47`](../packages/session/session-projection-cache/src/index.ts) ## `@deepseek-ai/dsh-session-query-sqlite` -Requires: `sessions` · `sessionProjections` +Requires: `sessions` ```ts config-catalog /** Combined session-query configuration backed by SQLite full-text search. */ @@ -1851,7 +1851,7 @@ export interface Config { } ``` -Source: [`packages/session/session-title/src/index.ts:54`](../packages/session/session-title/src/index.ts) +Source: [`packages/session/session-title/src/index.ts:53`](../packages/session/session-title/src/index.ts) @@ -2049,7 +2049,7 @@ export interface Config { } ``` -Source: [`packages/storage/storage-json/src/index.ts:27`](../packages/storage/storage-json/src/index.ts) +Source: [`packages/storage/storage-json/src/index.ts:28`](../packages/storage/storage-json/src/index.ts) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index b93bcd9c9f..a371c6d09f 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -634,7 +634,7 @@ export type Config = LocalConfig ## `@deepseek-ai/dsh-goal` -需要:`agents` · `sessionProjections` +需要:`agents` ```ts config-catalog /** Deployment defaults for goal creation. */ @@ -644,7 +644,7 @@ export interface Config { } ``` -来源:[`packages/goal/goal/src/index.ts:115`](../packages/goal/goal/src/index.ts) +来源:[`packages/goal/goal/src/index.ts:116`](../packages/goal/goal/src/index.ts) @@ -1400,7 +1400,7 @@ export interface PresetSpec { 依赖:[`ApprovalPolicy`](subsystems/approval.md) · [`SandboxMode`](subsystems/sandbox.md) -来源:[`packages/interaction/permission-presets/src/index.ts:116`](../packages/interaction/permission-presets/src/index.ts) +来源:[`packages/interaction/permission-presets/src/index.ts:112`](../packages/interaction/permission-presets/src/index.ts) @@ -1704,7 +1704,7 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' ## `@deepseek-ai/dsh-session-projection-cache` -需要:`storageDomain` · `sessionProjections` · `sessionPersistence` · `sessions` +需要:`sessionProjections` · `sessionPersistence` · `sessions` ```ts config-catalog /** @@ -1721,13 +1721,13 @@ export interface Config { } ``` -来源:[`packages/session/session-projection-cache/src/index.ts:42`](../packages/session/session-projection-cache/src/index.ts) +来源:[`packages/session/session-projection-cache/src/index.ts:47`](../packages/session/session-projection-cache/src/index.ts) ## `@deepseek-ai/dsh-session-query-sqlite` -需要:`sessions` · `sessionProjections` +需要:`sessions` ```ts config-catalog /** Combined session-query configuration backed by SQLite full-text search. */ @@ -1853,7 +1853,7 @@ export interface Config { } ``` -来源:[`packages/session/session-title/src/index.ts:54`](../packages/session/session-title/src/index.ts) +来源:[`packages/session/session-title/src/index.ts:53`](../packages/session/session-title/src/index.ts) @@ -2051,7 +2051,7 @@ export interface Config { } ``` -来源:[`packages/storage/storage-json/src/index.ts:27`](../packages/storage/storage-json/src/index.ts) +来源:[`packages/storage/storage-json/src/index.ts:28`](../packages/storage/storage-json/src/index.ts) diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index dc5bfbaf41..faa62b492e 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/module-graph.md -module-graph.md: 8422bbae71ab14fdac7b11eba17a1a2fc065302c -module-graph.zh.md: b16d6963e3c8fb597fbe3992fc90bd59bf67a85c +module-graph.md: 1228a7fe6be159c96fcc17c3b8b4397bb9762f5c +module-graph.zh.md: 2a2069c077bcf903dae9143458705407b6b7b1dd diff --git a/docs/module-graph.md b/docs/module-graph.md index 8422bbae71..1228a7fe6b 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -507,7 +507,6 @@ flowchart TD pkg_session_projection_cache --> pkg_session pkg_session_projection_cache --> pkg_session_persistence pkg_session_projection_cache --> pkg_session_projection - pkg_session_projection_cache --> pkg_storage_domain pkg_session_stats --> pkg_invariants pkg_session_stats --> pkg_llm pkg_session_stats --> pkg_session @@ -1509,7 +1508,7 @@ flowchart TD | [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) | | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) | | [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) | -| [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain) | +| [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection) | | [`session-stats`](../packages/session/session-stats) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) | | [`shell`](../packages/shell/shell) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess) | | [`workspace`](../packages/workspace/workspace) | `workspace` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index b16d6963e3..2a2069c077 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -509,7 +509,6 @@ flowchart TD pkg_session_projection_cache --> pkg_session pkg_session_projection_cache --> pkg_session_persistence pkg_session_projection_cache --> pkg_session_projection - pkg_session_projection_cache --> pkg_storage_domain pkg_session_stats --> pkg_invariants pkg_session_stats --> pkg_llm pkg_session_stats --> pkg_session @@ -1511,7 +1510,7 @@ flowchart TD | [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) | | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) | | [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) | -| [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain) | +| [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection) | | [`session-stats`](../packages/session/session-stats) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) | | [`shell`](../packages/shell/shell) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess) | | [`workspace`](../packages/workspace/workspace) | `workspace` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index bee586ef93..aa98286482 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/persistence-catalog.md -persistence-catalog.md: db96226b648afecd65e74f50e14a82d036f7de74 -persistence-catalog.zh.md: 0a247b4c4c5bf7eedbf8b7b8120bac3828090bc6 +persistence-catalog.md: ffd61bc1ea7e2a1e0e1edd60c067c8e84fc7fdbc +persistence-catalog.zh.md: bfbf896520f575052ecbff3424ae62f454fd55df diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index db96226b64..ffd61bc1ea 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -115,7 +115,7 @@ Sources: [`packages/core/session/src/types.ts:336`](../packages/core/session/src } ``` -Source: [`packages/core/agent/src/types.ts:33`](../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:40`](../packages/core/agent/src/types.ts) ### `agent-preset/*` @@ -649,7 +649,7 @@ Source: [`packages/core/session/src/types.ts:332`](../packages/core/session/src/ Types: [SessionTitleEventData](subsystems/session-title.md) -Source: [`packages/session/session-title/src/index.ts:75`](../packages/session/session-title/src/index.ts) +Source: [`packages/session/session-title/src/index.ts:74`](../packages/session/session-title/src/index.ts) diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 0a247b4c4c..bfbf896520 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -117,7 +117,7 @@ export type SessionEvent = { } ``` -来源:[`packages/core/agent/src/types.ts:33`](../packages/core/agent/src/types.ts) +来源:[`packages/core/agent/src/types.ts:40`](../packages/core/agent/src/types.ts) ### `agent-preset/*` @@ -651,7 +651,7 @@ export type SessionEvent = { 类型:[SessionTitleEventData](subsystems/session-title.md) -来源:[`packages/session/session-title/src/index.ts:75`](../packages/session/session-title/src/index.ts) +来源:[`packages/session/session-title/src/index.ts:74`](../packages/session/session-title/src/index.ts) diff --git a/docs/subsystems/session-projection.i18n.yaml b/docs/subsystems/session-projection.i18n.yaml index a7a2283d34..eb1b489e85 100644 --- a/docs/subsystems/session-projection.i18n.yaml +++ b/docs/subsystems/session-projection.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/session-projection.md -session-projection.md: f4fed9421f22c13961f95ba50dac35353b784dca -session-projection.zh.md: 4076edc073748cab1f3da6b6474a962aa2fa0e3b +session-projection.md: 6056b54970857de8d5edf21188c1a7098e06241d +session-projection.zh.md: a39f2c57a2778752779c96e058e2a6ea012a5a44 diff --git a/docs/subsystems/session-projection.md b/docs/subsystems/session-projection.md index f4fed9421f..6056b54970 100644 --- a/docs/subsystems/session-projection.md +++ b/docs/subsystems/session-projection.md @@ -27,8 +27,6 @@ interface ProjectionDefinition< key: K /** Validates persisted state before it seeds a fold. */ stateSchema: ZodType - /** Persist a host-only unit. Client-visible units are always persisted. */ - persist?: boolean /** * State for the empty log. * @returns the initial state. @@ -70,15 +68,15 @@ The whole-value event rule is load-bearing: a state-carrying log event carries t ```ts type-equiv /** - * One consistent read cut over every registered client-visible unit for one session. + * One consistent read cut over every registered unit for one session. * `asOfSeq` is the shared watermark — the seq of the last event every value * reflects (`-1` for an empty log, mirroring `session/subscribed.lastSeq`). */ interface ProjectionSnapshot { /** Seq of the last event the values reflect; -1 for an empty log. */ asOfSeq: number - /** Whole current client value per registered key. */ - values: Partial + /** Whole current value per registered key. */ + values: Partial } ``` diff --git a/docs/subsystems/session-projection.zh.md b/docs/subsystems/session-projection.zh.md index 4076edc073..a39f2c57a2 100644 --- a/docs/subsystems/session-projection.zh.md +++ b/docs/subsystems/session-projection.zh.md @@ -27,8 +27,6 @@ interface ProjectionDefinition< key: K /** Validates persisted state before it seeds a fold. */ stateSchema: ZodType - /** Persist a host-only unit. Client-visible units are always persisted. */ - persist?: boolean /** * State for the empty log. * @returns the initial state. @@ -70,15 +68,15 @@ interface ProjectionDefinition< ```ts type-equiv /** - * One consistent read cut over every registered client-visible unit for one session. + * One consistent read cut over every registered unit for one session. * `asOfSeq` is the shared watermark — the seq of the last event every value * reflects (`-1` for an empty log, mirroring `session/subscribed.lastSeq`). */ interface ProjectionSnapshot { /** Seq of the last event the values reflect; -1 for an empty log. */ asOfSeq: number - /** Whole current client value per registered key. */ - values: Partial + /** Whole current value per registered key. */ + values: Partial } ``` diff --git a/packages/session/session-projection-cache/package.json b/packages/session/session-projection-cache/package.json index e5ba957726..e61e30021e 100644 --- a/packages/session/session-projection-cache/package.json +++ b/packages/session/session-projection-cache/package.json @@ -44,11 +44,10 @@ "@deepseek-ai/cordis": "workspace:^" }, "devDependencies": { + "@deepseek-ai/cordis": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence": "workspace:^", - "@deepseek-ai/dsh-session-projection": "workspace:^", - "@deepseek-ai/dsh-storage-json": "workspace:^", - "@deepseek-ai/cordis": "workspace:^" + "@deepseek-ai/dsh-session-projection": "workspace:^" } } diff --git a/packages/session/session-projection-cache/src/index.ts b/packages/session/session-projection-cache/src/index.ts index 213cbcb254..d55498c07f 100644 --- a/packages/session/session-projection-cache/src/index.ts +++ b/packages/session/session-projection-cache/src/index.ts @@ -82,12 +82,15 @@ export class SessionProjectionCache extends Service { private readonly dirty = new Map() + /** In-flight durable writes, drained on disposal so a late flush can never land after teardown. */ + private readonly inFlight = new Set>() + constructor(ctx: Context, public config: Config) { super(ctx, 'sessionProjectionCache') } /** Install the write-behind listeners. */ - protected async [Service.init](): Promise { + protected [Service.init](): void { this.installWritePath() } @@ -238,18 +241,18 @@ export class SessionProjectionCache extends Service { // one), count/interval throttle the in-turn stream. this.ctx.on('session/event', (session: Session, event: SessionEvent) => { if (event.type === 'turn/end') { - void this.flushSoft(session, 'turn/end') + this.flushSoft(session, 'turn/end') return } const state = this.dirty.get(session) ?? { pending: 0, timer: undefined } this.dirty.set(session, state) state.pending += 1 if (state.pending >= this.config.writeEveryEvents) { - void this.flushSoft(session, 'count threshold') + this.flushSoft(session, 'count threshold') return } state.timer ??= setTimeout(() => { - void this.flushSoft(session, 'interval') + this.flushSoft(session, 'interval') }, this.config.writeIntervalMs) }) @@ -258,17 +261,20 @@ export class SessionProjectionCache extends Service { // flushSoft's synchronous prefix reads and resets the dirty state, so // dropping it (timer already cleared by markClean) right after is safe. this.ctx.on('session/disposed', (session: Session) => { - void this.flushSoft(session, 'detach') + this.flushSoft(session, 'detach') this.markClean(session) this.dirty.delete(session) }) - // Clear pending timers with the plugin (their sessions outlive the cache). + // With the plugin (their sessions outlive the cache): clear pending + // timers, stop accepting new work, and drain in-flight durable writes so + // a late flush can never land after disposal (or overwrite a successor). this.ctx.effect(() => () => { for (const state of this.dirty.values()) { if (state.timer !== undefined) clearTimeout(state.timer) } this.dirty.clear() + return Promise.allSettled([...this.inFlight]) }, 'sessionProjectionCache.timers') } @@ -277,12 +283,16 @@ export class SessionProjectionCache extends Service { * the throttle triggers only fire dirty (markClean clears the timer with * the counter) and the two mandatory points write unconditionally. */ - private async flushSoft(session: Session, trigger: string): Promise { - try { - await this.write(session) - } catch (error) { - this.ctx.logger.warn(`session projection cache: ${trigger} write for "${session.id}" failed (cache stays stale): ${String(error)}`) - } + private flushSoft(session: Session, trigger: string): void { + const run = (async () => { + try { + await this.write(session) + } catch (error) { + this.ctx.logger.warn(`session projection cache: ${trigger} write for "${session.id}" failed (cache stays stale): ${String(error)}`) + } + })() + this.inFlight.add(run) + void run.finally(() => this.inFlight.delete(run)) } /** Reset one session's dirty bookkeeping (its checkpoint is being written). */ diff --git a/packages/session/session-projection-cache/tests/cache.spec.ts b/packages/session/session-projection-cache/tests/cache.spec.ts index d338a26535..3f1a86cf0b 100644 --- a/packages/session/session-projection-cache/tests/cache.spec.ts +++ b/packages/session/session-projection-cache/tests/cache.spec.ts @@ -360,15 +360,27 @@ describe('SessionProjectionCache cold read', () => { it('discards a record bound to a different log lifecycle and refolds from the actual log', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-projcache-')) roots.push(root) - const logs = new Map([['reborn', storedLog([['real']])]]) // stored header stamps createdAt 0 + const logs = new Map([ + // stored headers stamp createdAt 0 + ['reborn', storedLog([['real']])], + ['reborn-stale', storedLog([['real']])], + ]) // A checkpoint from a PRIOR lifecycle of the same id (different createdAt): // its rows pass every watermark check, but the identity does not match. await seedRecord(root, 'reborn', { 'cache-test/marks': { ver: 1, seq: 2, val: { marks: ['phantom'] } } }, { createdAt: 999 }) + await seedRecord(root, 'reborn-stale', { 'cache-test/marks': { ver: 1, seq: 2, val: { marks: ['phantom'] } } }, { createdAt: 999 }) const { cache, root: sameRoot } = await harness({ root, logs }) + // Caller header agrees with the stored log (createdAt 0): the record is + // rejected at read, so the fold seeds from the log alone. const snapshot = await cache.coldSnapshot(headerOf(SessionId('reborn'))) expect(snapshot.values['cache-test/marks']).toEqual({ marks: ['real'] }) - // The write-back rebinds the record to the actual log's identity. + // Caller header matches the STALE record (createdAt 999): the record is + // read, then discarded whole because the log's identity disagrees. + const staleHeader = await cache.coldSnapshot(headerOf(SessionId('reborn-stale'), 999)) + expect(staleHeader.values['cache-test/marks']).toEqual({ marks: ['real'] }) + // The write-back rebinds each record to the actual log's identity. expect((await storedRecord(sameRoot, SessionId('reborn')))?.identity).toEqual({ createdAt: 0 }) + expect((await storedRecord(sameRoot, SessionId('reborn-stale')))?.identity).toEqual({ createdAt: 0 }) }) it('cachedSnapshot returns undefined when every stored row is version-mismatched', async () => { @@ -435,4 +447,32 @@ describe('SessionProjectionCache cold read', () => { await expect(ctx.sessionProjectionCache.coldSnapshot(headerOf(SessionId('bare')))) .resolves.toEqual({ asOfSeq: 2, values: {} }) }) + + it('disables the durable cache for a backend without a per-session directory (sqlite)', async () => { + // locate() undefined = no per-session artifact: live writes no-op, cold + // reads fall to the full-log rung, and no write-back ever lands. + const root = await mkdtemp(join(tmpdir(), 'dsh-projcache-')) + roots.push(root) + const logs = new Map([['flat', storedLog([['a']])]]) + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + ctx.sessionProjections.register(marksUnit()) + ctx.provide('sessionPersistence', { ...fakePersistence(root, logs), locate: () => undefined } as never) + await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 }) + const cache = ctx.sessionProjectionCache + // A live mandatory-point write no-ops: no cache file lands. + const session = ctx.sessions.create(SessionId('flat')) + mark(session, ['live']) + endTurn(session) + await settle() + expect(await storedRows(root, session.id)).toBeUndefined() + // The cold read refolds the whole log with no write-back. + const snapshot = await cache.coldSnapshot(headerOf(SessionId('flat'))) + expect(snapshot.values['cache-test/marks']).toEqual({ marks: ['a'] }) + expect(await storedRecord(root, SessionId('flat'))).toBeUndefined() + // The listing read finds no usable row. + expect(await cache.cachedSnapshot(headerOf(SessionId('flat')))).toBeUndefined() + }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 619627377c..2b392ab3ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6371,9 +6371,6 @@ importers: '@deepseek-ai/dsh-session-projection': specifier: workspace:^ version: link:../session-projection - '@deepseek-ai/dsh-storage': - specifier: workspace:^ - version: link:../../storage/storage packages/session/session-stats: dependencies: