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: