feat(web): add workspace-aware session flow

This commit is contained in:
imccyu
2026-07-25 16:04:48 +08:00
parent 755e2a8c51
commit 9eb9c70a8a
170 changed files with 7573 additions and 3006 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# @deepseek-ai/dsh-storage-domain
Domain data form for the DeepSeek Harness storage hub: mounts `ctx.storage.domain`, opening schema-validated KV domains over configured storage backends. A domain is declared once with `defineDomain` (zod record schemas, `z.infer`-derived types), opened through `DomainFacility.open`, and served from authoritative in-memory state — reads are synchronous, writes serialize on one per-domain chain, reach durability on the routed backend first, then update memory and emit `domain/changed`. The opening consumer owns the handle's lifecycle and releases it with `Domain.close()` (idempotent; typically its own `ctx.effect` disposer); domains still open when the plugin unmounts are closed by the facility.
Domain data form for the DeepSeek Harness storage hub: exposes the injectable `ctx.storageDomain` service and the matching `ctx.storage.domain` projection after every configured backend is registered. A domain is declared once with `defineDomain` (zod record schemas, `z.infer`-derived types), opened through `DomainFacility.open`, and served from authoritative in-memory state — reads are synchronous, writes serialize on one per-domain chain, reach durability on the routed backend first, then update memory and emit `domain/changed`. The opening consumer owns the handle's lifecycle and releases it with `Domain.close()` (idempotent; typically its own `ctx.effect` disposer); domains still open when the plugin unmounts are closed by the facility.
Design rationale, open semantics, and the storage/domain layer split live in the [Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md).
@@ -17,7 +17,7 @@ Design rationale, open semantics, and the storage/domain layer split live in the
#### What the model sees
Nothing. The package registers no tools, injects no prompts, and appends no session events; it stores non-session data (workspace records, future session sidecars) behind `ctx.storage.domain` and emits only the in-process `domain/changed` event, which reaches a model only if a consumer package renders it through its own documented surface.
Nothing. The package registers no tools, injects no prompts, and appends no session events; it stores non-session data (workspace records, future session sidecars) behind `ctx.storageDomain` and emits only the in-process `domain/changed` event, which reaches a model only if a consumer package renders it through its own documented surface.
#### Token effect
+27 -10
View File
@@ -9,6 +9,7 @@
import type { Context } from 'cordis'
import z from 'schemastery'
import { storageBackendServiceKey } from '@deepseek-ai/dsh-storage'
import { DomainError } from './error.ts'
import { descriptorOf } from './spec.ts'
import type { DomainSpec } from './spec.ts'
@@ -31,6 +32,12 @@ declare module '@deepseek-ai/dsh-storage' {
}
}
declare module 'cordis' {
interface Context {
storageDomain: DomainFacility
}
}
/** Cordis plugin name. */
export const name = 'storage-domain'
/** The storage hub must be present before the form can mount. */
@@ -188,16 +195,26 @@ function parseRecord<T>(domain: string, table: string, key: string, parse: () =>
* Mount the domain data form on the storage hub.
* @param ctx - Plugin context.
* @param config - Validated plugin config.
* @returns resolution after an already-available backend set activates the form.
*/
export function apply(ctx: Context, config: Config) {
const facility = new DomainFacility(ctx, config)
ctx.effect(() => {
const unmount = ctx.storage.mount('domain', facility)
return async () => {
// Close leftovers before unmounting: draining writes still emit
// domain/changed, whose invariant resolves the facility through the hub.
await facility.closeAll()
unmount()
}
export function apply(ctx: Context, config: Config): Promise<void> {
const backendServices = [...new Set([
config.backend,
...Object.values(config.routes ?? {}),
])].map(storageBackendServiceKey)
const fiber = ctx.inject(backendServices, (domainCtx) => {
const facility = new DomainFacility(domainCtx, config)
domainCtx.effect(() => {
const unmount = domainCtx.storage.mount('domain', facility)
return async () => {
// Close leftovers before unmounting: draining writes still emit
// domain/changed, whose invariant resolves the facility through the hub.
await facility.closeAll()
unmount()
}
})
domainCtx.provide('storageDomain', facility)
})
return Promise.resolve(fiber).then(() => {})
}
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { z } from 'zod'
import Storage from '@deepseek-ai/dsh-storage'
import Storage, { storageBackendServiceKey } from '@deepseek-ai/dsh-storage'
import { DomainFacility, defineDomain, domainTable } from '../src/index.ts'
import type { Config } from '../src/index.ts'
import type { DomainChanged } from '../src/events.ts'
@@ -151,15 +151,26 @@ describe('DomainFacility.open', () => {
})
describe('plugin apply', () => {
it('mounts the facility as ctx.storage.domain through the plugin effect', async () => {
it('waits for routed backends, then mounts one lifecycle-bound service and form', async () => {
const ctx = new Context()
await ctx.plugin(Storage)
ctx.storage.backend.register('memory', new MemoryStorageBackend())
const DomainPlugin = await import('../src/index.ts')
const fiber = await ctx.plugin(DomainPlugin, { backend: 'memory' })
expect(ctx.storage.domain).toBeInstanceOf(DomainFacility)
await fiber.dispose()
expect(ctx.get('storageDomain')).toBeUndefined()
expect(() => ctx.storage.form('domain')).toThrow(/not mounted/)
const backend = new MemoryStorageBackend()
ctx.storage.backend.register('memory', backend)
const disposeBackend = ctx.provide(storageBackendServiceKey('memory'), backend)
await vi.waitFor(() => { expect(ctx.storageDomain).toBeInstanceOf(DomainFacility) })
expect(ctx.storage.domain).toBe(ctx.storageDomain)
disposeBackend()
await vi.waitFor(() => {
expect(ctx.get('storageDomain')).toBeUndefined()
expect(() => ctx.storage.form('domain')).toThrow(/not mounted/)
})
await fiber.dispose()
})
})
@@ -295,10 +306,12 @@ describe('close and lifecycle', () => {
it('facility unmount closes domains the consumer never closed', async () => {
const ctx = new Context()
await ctx.plugin(Storage)
ctx.storage.backend.register('memory', new MemoryStorageBackend())
const backend = new MemoryStorageBackend()
ctx.storage.backend.register('memory', backend)
ctx.provide(storageBackendServiceKey('memory'), backend)
const DomainPlugin = await import('../src/index.ts')
const fiber = await ctx.plugin(DomainPlugin, { backend: 'memory' })
const domain = await ctx.storage.domain.open(bareSpec)
const domain = await ctx.storageDomain.open(bareSpec)
const table = domain.table('rows')
await table.put('a', { label: 'x', count: 1 })
await fiber.dispose()