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.
This commit is contained in:
parent
30334322eb
commit
c26ca6acb6
16 changed files with 102 additions and 62 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
<a id="deepseek-aidsh-headless"></a>
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
<a id="deepseek-aidsh-persona"></a>
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
<a id="deepseek-aidsh-session-query-sqlite"></a>
|
||||
|
||||
## `@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)
|
||||
|
||||
<a id="deepseek-aidsh-session-title-all-prompts-llm"></a>
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
<a id="deepseek-aidsh-storage-sqlite"></a>
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
<a id="deepseek-aidsh-headless"></a>
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
<a id="deepseek-aidsh-persona"></a>
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
<a id="deepseek-aidsh-session-query-sqlite"></a>
|
||||
|
||||
## `@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)
|
||||
|
||||
<a id="deepseek-aidsh-session-title-all-prompts-llm"></a>
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
<a id="deepseek-aidsh-storage-sqlite"></a>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
<a id="sessiontitle-llm-request--log-only"></a>
|
||||
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
|||
}
|
||||
```
|
||||
|
||||
来源:[`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<T extends SessionEventType = SessionEventType> = {
|
|||
|
||||
类型:[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)
|
||||
|
||||
<a id="sessiontitle-llm-request--log-only"></a>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -27,8 +27,6 @@ interface ProjectionDefinition<
|
|||
key: K
|
||||
/** Validates persisted state before it seeds a fold. */
|
||||
stateSchema: ZodType<S>
|
||||
/** 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<SessionProjectionMap>
|
||||
/** Whole current value per registered key. */
|
||||
values: Partial<ProjectionValues>
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -27,8 +27,6 @@ interface ProjectionDefinition<
|
|||
key: K
|
||||
/** Validates persisted state before it seeds a fold. */
|
||||
stateSchema: ZodType<S>
|
||||
/** 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<SessionProjectionMap>
|
||||
/** Whole current value per registered key. */
|
||||
values: Partial<ProjectionValues>
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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:^"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,12 +82,15 @@ export class SessionProjectionCache extends Service {
|
|||
|
||||
private readonly dirty = new Map<Session, DirtyState>()
|
||||
|
||||
/** In-flight durable writes, drained on disposal so a late flush can never land after teardown. */
|
||||
private readonly inFlight = new Set<Promise<unknown>>()
|
||||
|
||||
constructor(ctx: Context, public config: Config) {
|
||||
super(ctx, 'sessionProjectionCache')
|
||||
}
|
||||
|
||||
/** Install the write-behind listeners. */
|
||||
protected async [Service.init](): Promise<void> {
|
||||
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<void> {
|
||||
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). */
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue