refactor: apply repository naming contract
Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
@@ -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 packages/session-query/README.md
|
||||
README.md: 90b105d44a2affd8d487db20dc51357425446f8b
|
||||
README.zh.md: 305223e77385f7ee640b54e1d03e8659967da43e
|
||||
README.md: 75f447f667261fe3005d1a95e285120bebe6338d
|
||||
README.zh.md: b89ca7441ed825c7d76e9fec4dd6c400c6c6811e
|
||||
|
||||
@@ -8,7 +8,7 @@ This family provides authorized retrieval over live and durable session logs, in
|
||||
|---|---|---|
|
||||
| [`session-query/`](session-query/README.md) | Defines trusted reads, relationship queries, and search operations | `ctx.sessionQuery` |
|
||||
| [`session-query-sqlite/`](session-query-sqlite/README.md) | Implements session queries with SQLite full-text search | `ctx.sessionQuery` |
|
||||
| [`session-export/`](session-export/README.md) | Adds the Web `/export` command, shared browser download state, and result modal over the Host ZIP endpoint | `ctx.sessionExport` |
|
||||
| [`session-log-download/`](session-log-download/README.md) | Adds the Web `/export` command, shared browser download state, and result modal over the Host ZIP endpoint | `ctx.sessionLogDownload` |
|
||||
| [`tool-session-query/`](tool-session-query/README.md) | Exposes workspace-authorized session queries to the model | registers on `ctx.tools` |
|
||||
|
||||
The subsystem reference — logical records, bounded reads, traces, filters, result pages — is [docs/subsystems/session-query.md](../../docs/subsystems/session-query.md).
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|---|---|---|
|
||||
| [`session-query/`](session-query/README.md) | 定义可信读取、关系查询和搜索操作 | `ctx.sessionQuery` |
|
||||
| [`session-query-sqlite/`](session-query-sqlite/README.md) | 使用 SQLite 全文搜索实现会话查询 | `ctx.sessionQuery` |
|
||||
| [`session-export/`](session-export/README.md) | 在 Host ZIP 端点之上增加 Web `/export` 命令、共享浏览器下载状态和结果弹窗 | `ctx.sessionExport` |
|
||||
| [`session-log-download/`](session-log-download/README.md) | 在 Host ZIP 端点之上增加 Web `/export` 命令、共享浏览器下载状态和结果弹窗 | `ctx.sessionLogDownload` |
|
||||
| [`tool-session-query/`](tool-session-query/README.md) | 向模型公开经过工作区授权的会话查询 | 注册到 `ctx.tools` |
|
||||
|
||||
子系统参考——逻辑记录、有界读取、追踪、筛选器、结果页——见 [docs/subsystems/session-query.md](../../docs/subsystems/session-query.md)。
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { clientBundle } from '../../client/tsdown.client.ts'
|
||||
|
||||
export default clientBundle('@deepseek-ai/dsh-session-export', ['lib/types/index.js', 'lib/types/invariant.js'])
|
||||
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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 packages/session-query/session-export/README.md
|
||||
README.md: 4abee799c51d50342421ff594e88b03a11e785fa
|
||||
README.zh.md: 9b3ab129c132f797bbede8d8676bc8af954c1f90
|
||||
# pnpm run verify-translation-pairing --write packages/session-query/session-log-download/README.md
|
||||
README.md: eb8f26e4c44abfea14a555555ceec4ad1da48c18
|
||||
README.zh.md: a1185315469518eb29acaa1a9c9010aa6b4660b4
|
||||
@@ -1,4 +1,4 @@
|
||||
# @deepseek-ai/dsh-session-export
|
||||
# @deepseek-ai/dsh-session-log-download
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
@@ -20,11 +20,11 @@ The modal reports preparation, download start, or failure. Closing it does not c
|
||||
## Composition
|
||||
|
||||
```yaml
|
||||
- id: session-export
|
||||
name: '@deepseek-ai/dsh-session-export'
|
||||
- id: session-log-download
|
||||
name: '@deepseek-ai/dsh-session-log-download'
|
||||
```
|
||||
|
||||
The Web bundle mounts the package beside `dsh-host-apiproxy`, `dsh-commands`, `dsh-client-ui-command`, and `dsh-client-ui-conversation`. The package contributes its button and modal to the right-aligned `conversation.session.header.utilities` list, independently of the title-adjacent mode, Subagent, and Task entries in `conversation.session.header.actions`; Trajectory carries no export control.
|
||||
The Web bundle mounts the package beside `dsh-host-apiproxy`, `dsh-commands`, `dsh-client-ui-commands`, and `dsh-client-ui-conversation`. The package contributes its button and modal to the right-aligned `conversation.session.header.utilities` list, independently of the title-adjacent mode, Subagent, and Task entries in `conversation.session.header.actions`; Trajectory carries no export control.
|
||||
|
||||
## Model Experience
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# @deepseek-ai/dsh-session-export
|
||||
# @deepseek-ai/dsh-session-log-download
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
@@ -20,11 +20,11 @@ Host 下载端点会在 `readRaw` 前 flush 活动的根 Session,因此斜杠
|
||||
## 组合
|
||||
|
||||
```yaml
|
||||
- id: session-export
|
||||
name: '@deepseek-ai/dsh-session-export'
|
||||
- id: session-log-download
|
||||
name: '@deepseek-ai/dsh-session-log-download'
|
||||
```
|
||||
|
||||
Web bundle 将本包与 `dsh-host-apiproxy`、`dsh-commands`、`dsh-client-ui-command` 和 `dsh-client-ui-conversation` 一起挂载。本包把按钮和弹窗贡献到最右侧的 `conversation.session.header.utilities` 列表,与标题旁 `conversation.session.header.actions` 中的模式、Subagent 和 Task 配置项相互独立;Trajectory 不包含导出入口。
|
||||
Web bundle 将本包与 `dsh-host-apiproxy`、`dsh-commands`、`dsh-client-ui-commands` 和 `dsh-client-ui-conversation` 一起挂载。本包把按钮和弹窗贡献到最右侧的 `conversation.session.header.utilities` 列表,与标题旁 `conversation.session.header.actions` 中的模式、Subagent 和 Task 配置项相互独立;Trajectory 不包含导出入口。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-session-export",
|
||||
"name": "@deepseek-ai/dsh-session-log-download",
|
||||
"description": "Web Session-log export command and shared download dialog",
|
||||
"version": "0.0.1-rc.2",
|
||||
"publishConfig": { "access": "restricted" },
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/session-query/session-export"
|
||||
"directory": "packages/session-query/session-log-download"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
@@ -25,7 +25,7 @@
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-command": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
@@ -39,7 +39,7 @@
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-command": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
@@ -54,7 +54,7 @@
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-client-locale",
|
||||
"@deepseek-ai/dsh-client-runtime",
|
||||
"@deepseek-ai/dsh-client-ui-command",
|
||||
"@deepseek-ai/dsh-client-ui-commands",
|
||||
"@deepseek-ai/dsh-client-ui-conversation"
|
||||
],
|
||||
"platform": "web"
|
||||
@@ -1,30 +1,30 @@
|
||||
import type { ObservableSnapshot, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { Button, Modal } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { SessionExportDownloadState } from './controller.ts'
|
||||
import type { SessionLogDownloadState } from './controller.ts'
|
||||
import { NS } from './locales.ts'
|
||||
|
||||
/** Browser operations and state injected into the Session Header contribution. */
|
||||
export interface SessionExportDialogInjected {
|
||||
hooks: { sessionExport: ObservableSnapshot<SessionExportDownloadState> }
|
||||
export interface SessionLogDownloadDialogInjected {
|
||||
hooks: { sessionLogDownload: ObservableSnapshot<SessionLogDownloadState> }
|
||||
request: (sessionId: SessionId) => Promise<void>
|
||||
dismiss: (sessionId: SessionId) => void
|
||||
}
|
||||
|
||||
export type SessionExportDialogProps =
|
||||
export type SessionLogDownloadDialogProps =
|
||||
PropsRuntime<'conversation.session.header.utilities'>
|
||||
& PropsLocale<typeof NS>
|
||||
& InjectFace<SessionExportDialogInjected>
|
||||
& InjectFace<SessionLogDownloadDialogInjected>
|
||||
|
||||
/**
|
||||
* Modal shared by the Session Header button and this browser's `/export` command.
|
||||
* @param props - Session runtime, bound controller state, actions, and localized copy.
|
||||
* @returns the modal portal contribution.
|
||||
*/
|
||||
export function SessionExportDialog({
|
||||
sessionId, useSessionExport, dismiss, t,
|
||||
}: SessionExportDialogProps) {
|
||||
const entry = useSessionExport(state => state.bySession[String(sessionId)])
|
||||
export function SessionLogDownloadDialog({
|
||||
sessionId, useSessionLogDownload, dismiss, t,
|
||||
}: SessionLogDownloadDialogProps) {
|
||||
const entry = useSessionLogDownload(state => state.bySession[String(sessionId)])
|
||||
|
||||
const status = entry?.status
|
||||
const open = entry?.open === true
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { IconDownloadOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { SessionExportDialog, type SessionExportDialogProps } from './Dialog.tsx'
|
||||
import { SessionLogDownloadDialog, type SessionLogDownloadDialogProps } from './Dialog.tsx'
|
||||
import css from './HeaderAction.module.css'
|
||||
|
||||
/**
|
||||
@@ -8,9 +8,9 @@ import css from './HeaderAction.module.css'
|
||||
* @param props - Session runtime, download controller, and localized dialog copy.
|
||||
* @returns the persistent Header action and Session-scoped dialog.
|
||||
*/
|
||||
export function SessionExportHeader(props: SessionExportDialogProps): ReactNode {
|
||||
const { sessionId, useSessionExport, request } = props
|
||||
const entry = useSessionExport(state => state.bySession[String(sessionId)])
|
||||
export function SessionLogDownloadHeaderAction(props: SessionLogDownloadDialogProps): ReactNode {
|
||||
const { sessionId, useSessionLogDownload, request } = props
|
||||
const entry = useSessionLogDownload(state => state.bySession[String(sessionId)])
|
||||
const busy = entry?.status === 'downloading'
|
||||
|
||||
return (
|
||||
@@ -25,7 +25,7 @@ export function SessionExportHeader(props: SessionExportDialogProps): ReactNode
|
||||
<span>Session log</span>
|
||||
<IconDownloadOutline16 size={12} />
|
||||
</button>
|
||||
<SessionExportDialog {...props} />
|
||||
<SessionLogDownloadDialog {...props} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -3,24 +3,24 @@
|
||||
import { createSnapshotStore, type SessionId, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
/** Download phases presented by the shared modal. */
|
||||
export type SessionExportDownloadStatus = 'downloading' | 'success' | 'error'
|
||||
export type SessionLogDownloadStatus = 'downloading' | 'success' | 'error'
|
||||
|
||||
/** One Session's current download-dialog state. */
|
||||
export interface SessionExportDownloadEntry {
|
||||
export interface SessionLogDownloadEntry {
|
||||
readonly open: boolean
|
||||
readonly status: SessionExportDownloadStatus
|
||||
readonly status: SessionLogDownloadStatus
|
||||
readonly error: string | null
|
||||
}
|
||||
|
||||
/** Download states keyed by the Session whose Header owns the dialog. */
|
||||
export interface SessionExportDownloadState {
|
||||
bySession: Record<string, SessionExportDownloadEntry | undefined>
|
||||
export interface SessionLogDownloadState {
|
||||
bySession: Record<string, SessionLogDownloadEntry | undefined>
|
||||
}
|
||||
|
||||
type Fetch = (input: string | URL, init?: RequestInit) => Promise<Response>
|
||||
type Save = (url: string, filename: string) => void
|
||||
|
||||
const INITIAL: SessionExportDownloadState = { bySession: {} }
|
||||
const INITIAL: SessionLogDownloadState = { bySession: {} }
|
||||
|
||||
/**
|
||||
* Collapse an untrusted Session id into the filename convention owned by the host endpoint.
|
||||
@@ -54,9 +54,9 @@ function messageOf(error: unknown): string {
|
||||
}
|
||||
|
||||
/** Owns one in-flight browser download per Session and publishes modal state. */
|
||||
export class SessionExportDownloadController {
|
||||
export class SessionLogDownloadController {
|
||||
/** uSES-safe state source shared by every Session-scoped modal contribution. */
|
||||
readonly store: SnapshotStore<SessionExportDownloadState> = createSnapshotStore(INITIAL)
|
||||
readonly store: SnapshotStore<SessionLogDownloadState> = createSnapshotStore(INITIAL)
|
||||
|
||||
private readonly active = new Map<SessionId, { readonly abort: AbortController; readonly done: Promise<void> }>()
|
||||
private disposed = false
|
||||
@@ -129,7 +129,7 @@ export class SessionExportDownloadController {
|
||||
}
|
||||
}
|
||||
|
||||
private publish(sessionId: SessionId, entry: SessionExportDownloadEntry): void {
|
||||
private publish(sessionId: SessionId, entry: SessionLogDownloadEntry): void {
|
||||
this.store.update((state) => {
|
||||
state.bySession = { ...state.bySession, [String(sessionId)]: entry }
|
||||
})
|
||||
@@ -2,26 +2,26 @@
|
||||
|
||||
import type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-command/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-commands/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { SessionExportDownloadController } from './controller.ts'
|
||||
import type { SessionExportDialogInjected } from './Dialog.tsx'
|
||||
import { SessionExportHeader } from './HeaderAction.tsx'
|
||||
import { en, NS, zh, type SessionExportKey } from './locales.ts'
|
||||
import { SessionLogDownloadController } from './controller.ts'
|
||||
import type { SessionLogDownloadDialogInjected } from './Dialog.tsx'
|
||||
import { SessionLogDownloadHeaderAction } from './HeaderAction.tsx'
|
||||
import { en, NS, zh, type SessionLogDownloadKey } from './locales.ts'
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
sessionExport: SessionExportDownloadController
|
||||
sessionLogDownload: SessionLogDownloadController
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
interface LocaleNamespaceMap {
|
||||
'session-export': SessionExportKey
|
||||
'session-log-download': SessionLogDownloadKey
|
||||
}
|
||||
}
|
||||
|
||||
export type { SessionExportDownloadEntry, SessionExportDownloadState } from './controller.ts'
|
||||
export type { SessionLogDownloadEntry, SessionLogDownloadState } from './controller.ts'
|
||||
|
||||
export const inject = ['slots', 'locale']
|
||||
|
||||
@@ -30,23 +30,23 @@ export const inject = ['slots', 'locale']
|
||||
* @param ctx - browser context carrying slots and locale services.
|
||||
*/
|
||||
export function apply(ctx: ClientContext): void {
|
||||
const controller = new SessionExportDownloadController()
|
||||
ctx.provide('sessionExport', controller)
|
||||
ctx.effect(() => async () => { await controller.dispose() }, 'session-export: browser download lifecycle')
|
||||
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'session-export: browser dictionaries')
|
||||
const controller = new SessionLogDownloadController()
|
||||
ctx.provide('sessionLogDownload', controller)
|
||||
ctx.effect(() => async () => { await controller.dispose() }, 'session-log-download: browser download lifecycle')
|
||||
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'session-log-download: browser dictionaries')
|
||||
ctx.on('command/executed', (sessionId, commandName, result) => {
|
||||
if (commandName === 'export' && result.kind === 'success') void controller.download(sessionId)
|
||||
})
|
||||
ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({
|
||||
name: 'conversation.session.header.utilities',
|
||||
id: 'session-export',
|
||||
id: 'session-log-download',
|
||||
locale: NS,
|
||||
inject: (): SessionExportDialogInjected => ({
|
||||
hooks: { sessionExport: controller.store },
|
||||
inject: (): SessionLogDownloadDialogInjected => ({
|
||||
hooks: { sessionLogDownload: controller.store },
|
||||
request: (sessionId: SessionId) => controller.download(sessionId),
|
||||
dismiss: (sessionId: SessionId) => { controller.dismiss(sessionId) },
|
||||
}),
|
||||
}, SessionExportHeader))
|
||||
}, SessionLogDownloadHeaderAction))
|
||||
}
|
||||
|
||||
export type { SessionExportDialogInjected, SessionExportDialogProps } from './Dialog.tsx'
|
||||
export type { SessionLogDownloadDialogInjected, SessionLogDownloadDialogProps } from './Dialog.tsx'
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Locale namespace owned by Session export browser feedback. */
|
||||
export const NS = 'session-export'
|
||||
export const NS = 'session-log-download'
|
||||
|
||||
/** Simplified-Chinese Session export strings. */
|
||||
export const zh = {
|
||||
@@ -24,4 +24,4 @@ export const en: Record<keyof typeof zh, string> = {
|
||||
}
|
||||
|
||||
/** Stable locale keys consumed by the shared modal. */
|
||||
export type SessionExportKey = keyof typeof zh
|
||||
export type SessionLogDownloadKey = keyof typeof zh
|
||||
@@ -3,7 +3,7 @@
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { CommandResult } from '@deepseek-ai/dsh-commands'
|
||||
|
||||
export const name = 'session-export'
|
||||
export const name = 'session-log-download'
|
||||
export const inject = ['commands']
|
||||
|
||||
const REQUESTED: CommandResult = {
|
||||
@@ -22,5 +22,5 @@ export function apply(ctx: Context): void {
|
||||
handler: invocation => Promise.resolve(invocation.rawInput.trim() === ''
|
||||
? REQUESTED
|
||||
: { kind: 'error', text: 'The Web /export command does not accept a path.' }),
|
||||
}), 'session-export: command')
|
||||
}), 'session-log-download: command')
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
/** Package invariant companion for `@deepseek-ai/dsh-session-export`. */
|
||||
/** Package invariant companion for `@deepseek-ai/dsh-session-log-download`. */
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-session-export'
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-session-log-download'
|
||||
|
||||
export const name = 'session-export-invariant'
|
||||
export const inject = ['invariants']
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { SessionExportHeader } from '../src/client/HeaderAction.tsx'
|
||||
import { SessionLogDownloadHeaderAction } from '../src/client/HeaderAction.tsx'
|
||||
import { apply, inject } from '../src/client/index.ts'
|
||||
|
||||
const SID = 'session-export-apply' as SessionId
|
||||
|
||||
afterEach(() => { vi.unstubAllGlobals() })
|
||||
|
||||
function declare(slots: SlotsService): () => void {
|
||||
function declare(slots: SlotRegistry): () => void {
|
||||
return slots.register({
|
||||
name: 'root',
|
||||
children: {
|
||||
@@ -23,30 +23,30 @@ function declare(slots: SlotsService): () => void {
|
||||
|
||||
async function bench() {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SlotsService).await()
|
||||
const slots = ctx.get('slots') as SlotsService
|
||||
await ctx.plugin(SlotRegistry).await()
|
||||
const slots = ctx.get('slots') as SlotRegistry
|
||||
const declaration = declare(slots)
|
||||
ctx.provide('locale', new LocaleService(ctx))
|
||||
ctx.provide('locale', new LocaleRuntime(ctx))
|
||||
const fiber = ctx.plugin({ inject: [...inject], apply })
|
||||
await fiber.await()
|
||||
return { ctx, slots, declaration, fiber }
|
||||
}
|
||||
|
||||
describe('session-export browser plugin', () => {
|
||||
describe('session-log-download browser plugin', () => {
|
||||
it('provides one controller and removes its Header contribution on disposal', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn(async () => new Response('', { status: 500 })))
|
||||
const b = await bench()
|
||||
expect(inject).toEqual(['slots', 'locale'])
|
||||
expect(b.ctx.sessionExport).toBeDefined()
|
||||
expect(b.ctx.sessionLogDownload).toBeDefined()
|
||||
expect(b.slots.entries('conversation.session.header.actions')).toHaveLength(0)
|
||||
const entry = b.slots.entries('conversation.session.header.utilities')[0]
|
||||
expect(entry?.component).toBe(SessionExportHeader)
|
||||
expect(entry?.options).toMatchObject({ id: 'session-export' })
|
||||
const injected = (entry?.inject as unknown as () => import('../src/client/Dialog.tsx').SessionExportDialogInjected)()
|
||||
expect(entry?.component).toBe(SessionLogDownloadHeaderAction)
|
||||
expect(entry?.options).toMatchObject({ id: 'session-log-download' })
|
||||
const injected = (entry?.inject as unknown as () => import('../src/client/Dialog.tsx').SessionLogDownloadDialogInjected)()
|
||||
await injected.request(SID)
|
||||
expect(b.ctx.sessionExport.store.getSnapshot().bySession[SID]?.status).toBe('error')
|
||||
expect(b.ctx.sessionLogDownload.store.getSnapshot().bySession[SID]?.status).toBe('error')
|
||||
injected.dismiss(SID)
|
||||
expect(b.ctx.sessionExport.store.getSnapshot().bySession[SID]?.open).toBe(false)
|
||||
expect(b.ctx.sessionLogDownload.store.getSnapshot().bySession[SID]?.open).toBe(false)
|
||||
|
||||
await b.fiber.dispose()
|
||||
expect(b.slots.entries('conversation.session.header.utilities')).toHaveLength(0)
|
||||
@@ -65,9 +65,9 @@ describe('session-export browser plugin', () => {
|
||||
first.ctx.emit('command/executed', SID, 'export', { kind: 'success' })
|
||||
await vi.waitFor(() => {
|
||||
expect(fetcher).toHaveBeenCalledOnce()
|
||||
expect(first.ctx.sessionExport.store.getSnapshot().bySession[SID]?.status).toBe('error')
|
||||
expect(first.ctx.sessionLogDownload.store.getSnapshot().bySession[SID]?.status).toBe('error')
|
||||
})
|
||||
expect(second.ctx.sessionExport.store.getSnapshot().bySession[SID]).toBeUndefined()
|
||||
expect(second.ctx.sessionLogDownload.store.getSnapshot().bySession[SID]).toBeUndefined()
|
||||
|
||||
await first.fiber.dispose()
|
||||
await second.fiber.dispose()
|
||||
@@ -79,7 +79,7 @@ describe('session-export browser plugin', () => {
|
||||
expect(b.slots.entries('conversation.session.header.utilities')).toHaveLength(0)
|
||||
const redeclare = declare(b.slots)
|
||||
await Promise.resolve()
|
||||
expect(b.slots.entries('conversation.session.header.utilities')[0]?.component).toBe(SessionExportHeader)
|
||||
expect(b.slots.entries('conversation.session.header.utilities')[0]?.component).toBe(SessionLogDownloadHeaderAction)
|
||||
redeclare()
|
||||
await b.fiber.dispose()
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import type { CommandDefinition, CommandInvocation } from '@deepseek-ai/dsh-commands'
|
||||
import * as SessionExport from '../src/index.ts'
|
||||
import * as SessionLogDownload from '../src/index.ts'
|
||||
|
||||
describe('/export Web download command', () => {
|
||||
it('registers one pathless command and removes it with the plugin fiber', async () => {
|
||||
@@ -13,7 +13,7 @@ describe('/export Web download command', () => {
|
||||
return () => { descriptor = undefined }
|
||||
},
|
||||
} as never)
|
||||
const fiber = await ctx.plugin(SessionExport)
|
||||
const fiber = await ctx.plugin(SessionLogDownload)
|
||||
|
||||
expect(descriptor).toMatchObject({
|
||||
name: 'export',
|
||||
@@ -2,7 +2,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import {
|
||||
downloadUrl, SessionExportDownloadController, sessionLogZipFilename,
|
||||
downloadUrl, SessionLogDownloadController, sessionLogZipFilename,
|
||||
} from '../src/client/controller.ts'
|
||||
|
||||
const SID = 'session-export-controller' as SessionId
|
||||
@@ -12,11 +12,11 @@ afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('SessionExportDownloadController', () => {
|
||||
describe('SessionLogDownloadController', () => {
|
||||
it('downloads the host ZIP and publishes one shared success state', async () => {
|
||||
const fetcher = vi.fn(async () => new Response('zip', { status: 200 }))
|
||||
const save = vi.fn()
|
||||
const controller = new SessionExportDownloadController(fetcher, save)
|
||||
const controller = new SessionLogDownloadController(fetcher, save)
|
||||
|
||||
await controller.download(SID)
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('SessionExportDownloadController', () => {
|
||||
it('collapses concurrent gestures and preserves a dismissed dialog', async () => {
|
||||
const response = Promise.withResolvers<Response>()
|
||||
const fetcher = vi.fn(() => response.promise)
|
||||
const controller = new SessionExportDownloadController(fetcher, vi.fn())
|
||||
const controller = new SessionLogDownloadController(fetcher, vi.fn())
|
||||
|
||||
const first = controller.download(SID)
|
||||
const second = controller.download(SID)
|
||||
@@ -54,7 +54,7 @@ describe('SessionExportDownloadController', () => {
|
||||
})
|
||||
|
||||
it('publishes HTTP and transport failures without leaking rejections', async () => {
|
||||
const http = new SessionExportDownloadController(
|
||||
const http = new SessionLogDownloadController(
|
||||
async () => new Response('backend unavailable', { status: 500 }), vi.fn(),
|
||||
)
|
||||
await http.download(SID)
|
||||
@@ -64,13 +64,13 @@ describe('SessionExportDownloadController', () => {
|
||||
error: 'Export failed: HTTP 500 backend unavailable',
|
||||
})
|
||||
|
||||
const transport = new SessionExportDownloadController(async () => { throw 'offline' }, vi.fn())
|
||||
const transport = new SessionLogDownloadController(async () => { throw 'offline' }, vi.fn())
|
||||
await transport.download(SID)
|
||||
expect(transport.store.getSnapshot().bySession[SID]?.error).toBe('offline')
|
||||
|
||||
transport.dismiss('absent' as SessionId)
|
||||
|
||||
const emptyDetail = new SessionExportDownloadController(
|
||||
const emptyDetail = new SessionLogDownloadController(
|
||||
async () => ({
|
||||
ok: false, status: 503, text: async () => { throw new Error('body unavailable') },
|
||||
}) as unknown as Response,
|
||||
@@ -88,7 +88,7 @@ describe('SessionExportDownloadController', () => {
|
||||
reject(signal?.reason instanceof Error ? signal.reason : new Error('aborted'))
|
||||
}, { once: true })
|
||||
}))
|
||||
const controller = new SessionExportDownloadController(fetcher, vi.fn())
|
||||
const controller = new SessionLogDownloadController(fetcher, vi.fn())
|
||||
const pending = controller.download(SID)
|
||||
|
||||
await controller.dispose()
|
||||
@@ -104,7 +104,7 @@ describe('SessionExportDownloadController', () => {
|
||||
const fetcher = vi.fn(async (_input: string | URL, _init?: RequestInit) => new Response('zip'))
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
const click = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {})
|
||||
const controller = new SessionExportDownloadController()
|
||||
const controller = new SessionLogDownloadController()
|
||||
|
||||
await controller.download(SID)
|
||||
|
||||
@@ -115,7 +115,7 @@ describe('SessionExportDownloadController', () => {
|
||||
|
||||
it('defaults dialog openness when state is externally cleared before settlement', async () => {
|
||||
const success = Promise.withResolvers<Response>()
|
||||
const successful = new SessionExportDownloadController(() => success.promise, vi.fn())
|
||||
const successful = new SessionLogDownloadController(() => success.promise, vi.fn())
|
||||
const successRun = successful.download(SID)
|
||||
successful.store.set({ bySession: {} })
|
||||
success.resolve(new Response('zip'))
|
||||
@@ -123,7 +123,7 @@ describe('SessionExportDownloadController', () => {
|
||||
expect(successful.store.getSnapshot().bySession[SID]?.open).toBe(true)
|
||||
|
||||
const failure = Promise.withResolvers<Response>()
|
||||
const failing = new SessionExportDownloadController(() => failure.promise, vi.fn())
|
||||
const failing = new SessionLogDownloadController(() => failure.promise, vi.fn())
|
||||
const failureRun = failing.download(SID)
|
||||
failing.store.set({ bySession: {} })
|
||||
failure.reject(new Error('failed after clear'))
|
||||
@@ -3,34 +3,34 @@ import { act, cleanup, fireEvent, render, waitFor } from '@testing-library/react
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { SessionExportDownloadController } from '../src/client/controller.ts'
|
||||
import { SessionExportDialog } from '../src/client/Dialog.tsx'
|
||||
import type { SessionExportDialogProps } from '../src/client/Dialog.tsx'
|
||||
import { SessionLogDownloadController } from '../src/client/controller.ts'
|
||||
import { SessionLogDownloadDialog } from '../src/client/Dialog.tsx'
|
||||
import type { SessionLogDownloadDialogProps } from '../src/client/Dialog.tsx'
|
||||
import { en } from '../src/client/locales.ts'
|
||||
|
||||
const SID = 'session-export-dialog' as SessionId
|
||||
|
||||
function bench(
|
||||
controller = new SessionExportDownloadController(
|
||||
controller = new SessionLogDownloadController(
|
||||
async () => new Response('zip', { status: 200 }), vi.fn(),
|
||||
),
|
||||
) {
|
||||
const dismiss = vi.fn((sessionId: SessionId) => { controller.dismiss(sessionId) })
|
||||
function useSessionExport<T>(selector: (state: ReturnType<typeof controller.store.getSnapshot>) => T): T {
|
||||
function useSessionLogDownload<T>(selector: (state: ReturnType<typeof controller.store.getSnapshot>) => T): T {
|
||||
return useSyncExternalStore(
|
||||
listener => controller.store.subscribe(listener),
|
||||
() => selector(controller.store.getSnapshot()),
|
||||
)
|
||||
}
|
||||
const t = (key: keyof typeof en): string => en[key]
|
||||
const props = { sessionId: SID, useSessionExport, dismiss, t } as unknown as SessionExportDialogProps
|
||||
const view = render(<SessionExportDialog {...props} />)
|
||||
const props = { sessionId: SID, useSessionLogDownload, dismiss, t } as unknown as SessionLogDownloadDialogProps
|
||||
const view = render(<SessionLogDownloadDialog {...props} />)
|
||||
return { controller, dismiss, view }
|
||||
}
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('SessionExportDialog', () => {
|
||||
describe('SessionLogDownloadDialog', () => {
|
||||
it('shows a controller failure and closes it without reading Session history', async () => {
|
||||
const b = bench()
|
||||
act(() => {
|
||||
@@ -49,7 +49,7 @@ describe('SessionExportDialog', () => {
|
||||
it('renders the in-flight state and the settled browser download state', async () => {
|
||||
let release!: (response: Response) => void
|
||||
const pending = new Promise<Response>((resolve) => { release = resolve })
|
||||
const controller = new SessionExportDownloadController(() => pending, vi.fn())
|
||||
const controller = new SessionLogDownloadController(() => pending, vi.fn())
|
||||
const b = bench(controller)
|
||||
|
||||
const download = controller.download(SID)
|
||||
@@ -3,15 +3,15 @@ import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { SessionExportDownloadController } from '../src/client/controller.ts'
|
||||
import { SessionExportHeader } from '../src/client/HeaderAction.tsx'
|
||||
import type { SessionExportDialogProps } from '../src/client/Dialog.tsx'
|
||||
import { SessionLogDownloadController } from '../src/client/controller.ts'
|
||||
import { SessionLogDownloadHeaderAction } from '../src/client/HeaderAction.tsx'
|
||||
import type { SessionLogDownloadDialogProps } from '../src/client/Dialog.tsx'
|
||||
import { en } from '../src/client/locales.ts'
|
||||
|
||||
const SID = 'session-export-header' as SessionId
|
||||
|
||||
function bindSessionExport(controller: SessionExportDownloadController) {
|
||||
return function useSessionExport<T>(selector: (state: ReturnType<typeof controller.store.getSnapshot>) => T): T {
|
||||
function bindSessionExport(controller: SessionLogDownloadController) {
|
||||
return function useSessionLogDownload<T>(selector: (state: ReturnType<typeof controller.store.getSnapshot>) => T): T {
|
||||
return useSyncExternalStore(
|
||||
listener => controller.store.subscribe(listener),
|
||||
() => selector(controller.store.getSnapshot()),
|
||||
@@ -20,18 +20,18 @@ function bindSessionExport(controller: SessionExportDownloadController) {
|
||||
}
|
||||
|
||||
function bench() {
|
||||
const controller = new SessionExportDownloadController(async () => new Response('zip'), vi.fn())
|
||||
const controller = new SessionLogDownloadController(async () => new Response('zip'), vi.fn())
|
||||
const request = vi.fn((sessionId: SessionId) => controller.download(sessionId))
|
||||
const dismiss = vi.fn((sessionId: SessionId) => { controller.dismiss(sessionId) })
|
||||
const useSessionExport = bindSessionExport(controller)
|
||||
const useSessionLogDownload = bindSessionExport(controller)
|
||||
const props = {
|
||||
sessionId: SID,
|
||||
useSessionExport,
|
||||
useSessionLogDownload,
|
||||
request,
|
||||
dismiss,
|
||||
t: (key: keyof typeof en): string => en[key],
|
||||
} as unknown as SessionExportDialogProps
|
||||
const view = render(<SessionExportHeader {...props} />)
|
||||
} as unknown as SessionLogDownloadDialogProps
|
||||
const view = render(<SessionLogDownloadHeaderAction {...props} />)
|
||||
return { controller, request, view }
|
||||
}
|
||||
|
||||
@@ -51,15 +51,15 @@ describe('Session export Header action', () => {
|
||||
const b = bench()
|
||||
let release!: (response: Response) => void
|
||||
const pending = new Promise<Response>((resolve) => { release = resolve })
|
||||
const controller = new SessionExportDownloadController(() => pending, vi.fn())
|
||||
const useSessionExport = bindSessionExport(controller)
|
||||
b.view.rerender(<SessionExportHeader {...({
|
||||
const controller = new SessionLogDownloadController(() => pending, vi.fn())
|
||||
const useSessionLogDownload = bindSessionExport(controller)
|
||||
b.view.rerender(<SessionLogDownloadHeaderAction {...({
|
||||
sessionId: SID,
|
||||
useSessionExport,
|
||||
useSessionLogDownload,
|
||||
request: (sessionId: SessionId) => controller.download(sessionId),
|
||||
dismiss: (sessionId: SessionId) => { controller.dismiss(sessionId) },
|
||||
t: (key: keyof typeof en): string => en[key],
|
||||
} as unknown as SessionExportDialogProps)} />)
|
||||
} as unknown as SessionLogDownloadDialogProps)} />)
|
||||
|
||||
const download = controller.download(SID)
|
||||
const button = b.view.getByRole('button', { name: 'Session log' })
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { apply, inject, name } from '../src/invariant.ts'
|
||||
|
||||
describe('@deepseek-ai/dsh-session-export/invariant', () => {
|
||||
describe('@deepseek-ai/dsh-session-log-download/invariant', () => {
|
||||
it('registers the package-owned empty companion', async () => {
|
||||
const register = vi.fn(() => vi.fn())
|
||||
const ctx = new Context()
|
||||
@@ -10,7 +10,7 @@ describe('@deepseek-ai/dsh-session-export/invariant', () => {
|
||||
const dispose = await apply(ctx)
|
||||
expect(name).toBe('session-export-invariant')
|
||||
expect(inject).toEqual(['invariants'])
|
||||
expect(register).toHaveBeenCalledWith('@deepseek-ai/dsh-session-export', expect.any(Function))
|
||||
expect(register).toHaveBeenCalledWith('@deepseek-ai/dsh-session-log-download', expect.any(Function))
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
@@ -7,9 +7,9 @@ import { Context } from '@deepseek-ai/cordis'
|
||||
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
||||
import Include from '@deepseek-ai/cordis-plugin-include'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import CommandService from '@deepseek-ai/dsh-commands'
|
||||
import CommandRuntime from '@deepseek-ai/dsh-commands'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import * as SessionExport from '@deepseek-ai/dsh-session-export'
|
||||
import * as SessionLogDownload from '@deepseek-ai/dsh-session-log-download'
|
||||
|
||||
let root: string | undefined
|
||||
let context: Context | undefined
|
||||
@@ -21,14 +21,14 @@ afterEach(async () => {
|
||||
root = undefined
|
||||
})
|
||||
|
||||
describe('session-export real Loader composition', () => {
|
||||
describe('session-log-download real Loader composition', () => {
|
||||
it('discovers and executes /export through the assembled command plane', async () => {
|
||||
root = await mkdtemp(join(tmpdir(), 'dsh-session-export-loader-'))
|
||||
const configPath = join(root, 'cordis.yml')
|
||||
await writeFile(configPath, [
|
||||
"- name: '@deepseek-ai/dsh-session'",
|
||||
"- name: '@deepseek-ai/dsh-commands'",
|
||||
"- name: '@deepseek-ai/dsh-session-export'",
|
||||
"- name: '@deepseek-ai/dsh-session-log-download'",
|
||||
'',
|
||||
].join('\n'))
|
||||
|
||||
@@ -38,8 +38,8 @@ describe('session-export real Loader composition', () => {
|
||||
context.loader.builtins.include = Include
|
||||
const modules = new Map<string, unknown>([
|
||||
['@deepseek-ai/dsh-session', SessionStore],
|
||||
['@deepseek-ai/dsh-commands', CommandService],
|
||||
['@deepseek-ai/dsh-session-export', SessionExport],
|
||||
['@deepseek-ai/dsh-commands', CommandRuntime],
|
||||
['@deepseek-ai/dsh-session-log-download', SessionLogDownload],
|
||||
])
|
||||
context.loader.internal = {
|
||||
version: 'v2',
|
||||
@@ -12,10 +12,10 @@
|
||||
{ "path": "../../interaction/commands" },
|
||||
{ "path": "../../client/locale" },
|
||||
{ "path": "../../client/runtime" },
|
||||
{ "path": "../../client/ui-command" },
|
||||
{ "path": "../../client/ui-commands" },
|
||||
{ "path": "../../client/ui-conversation" },
|
||||
{ "path": "../../client/ui-primitives" },
|
||||
{ "path": "../../client/ui-slots" },
|
||||
{ "path": "../../support/invariants" }
|
||||
{ "path": "../../runtime-diagnostics/invariants" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { clientBundle } from '../../client/tsdown.client.ts'
|
||||
|
||||
export default clientBundle('@deepseek-ai/dsh-session-log-download', ['lib/types/index.js', 'lib/types/invariant.js'])
|
||||
@@ -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 packages/session-query/session-query-sqlite/README.md
|
||||
README.md: 8784f1b2c2a3c9291b761c0a6e68201286b9e306
|
||||
README.zh.md: 14c31f3d700ccc0ac7fd6e2837afa0bce50eb20e
|
||||
README.md: 846f79bd8383a8114a6d1329e7dbe931a67d98d0
|
||||
README.zh.md: e84e521dd5f392dbcede1e1dd957e4ce0a9bc50f
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Concrete `ctx.sessionQuery` provider. `SessionQuerySqlite` inherits exact reads, traces, and provider-independent filters from the Service Definition package and implements its two full-text methods with SQLite FTS5. Search uses the live-preferred logical session corpus and groups cross-session results by their strongest event.
|
||||
Concrete `ctx.sessionQuery` provider. `SqliteSessionQueryEngine` inherits exact reads, traces, and provider-independent filters from the Service Definition package and implements its two full-text methods with SQLite FTS5. Search uses the live-preferred logical session corpus and groups cross-session results by their strongest event.
|
||||
|
||||
## Search contract
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
具体 `ctx.sessionQuery` 提供方。`SessionQuerySqlite` 从 Service Definition 包继承精确读取、跟踪和提供方无关的过滤,并使用 SQLite FTS5 实现其两个全文方法。搜索使用实时优先的逻辑会话语料库,并按每个会话中匹配度最高的事件对跨会话结果分组。
|
||||
具体 `ctx.sessionQuery` 提供方。`SqliteSessionQueryEngine` 从 Service Definition 包继承精确读取、跟踪和提供方无关的过滤,并使用 SQLite FTS5 实现其两个全文方法。搜索使用实时优先的逻辑会话语料库,并按每个会话中匹配度最高的事件对跨会话结果分组。
|
||||
|
||||
## 搜索约定
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
SessionPersistenceRevision,
|
||||
SessionPersistenceSnapshot,
|
||||
} from '@deepseek-ai/dsh-session-persistence'
|
||||
import SessionQueryService, {
|
||||
import SessionQueryEngine, {
|
||||
SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY,
|
||||
SESSION_QUERY_READ_WINDOW_MAX,
|
||||
SessionQueryError,
|
||||
@@ -187,7 +187,7 @@ interface CursorPayload {
|
||||
}
|
||||
|
||||
/** Concrete SQLite owner of the combined `ctx.sessionQuery` service. */
|
||||
export class SessionQuerySqlite extends SessionQueryService {
|
||||
export class SqliteSessionQueryEngine extends SessionQueryEngine {
|
||||
static override inject = ['sessions']
|
||||
|
||||
static Config: z<Config> = z.object({
|
||||
@@ -1079,4 +1079,4 @@ function isRuntimeArray(value: unknown): boolean {
|
||||
return Array.isArray(value)
|
||||
}
|
||||
|
||||
export default SessionQuerySqlite
|
||||
export default SqliteSessionQueryEngine
|
||||
|
||||
@@ -10,8 +10,8 @@ import { Context } from '@deepseek-ai/cordis'
|
||||
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
||||
import { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SessionStore from '@deepseek-ai/dsh-session'
|
||||
import SessionPersistenceSqlite from '@deepseek-ai/dsh-session-persistence-sqlite'
|
||||
import SessionQuerySqlite, * as queryModule from '@deepseek-ai/dsh-session-query-sqlite'
|
||||
import SqliteSessionPersistence from '@deepseek-ai/dsh-session-persistence-sqlite'
|
||||
import SqliteSessionQueryEngine, * as queryModule from '@deepseek-ai/dsh-session-query-sqlite'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
@@ -36,11 +36,11 @@ describe('dsh-session-query-sqlite real Loader path', () => {
|
||||
const searchPath = await temporaryPath('derived.db')
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const persistence = await ctx.plugin(SessionPersistenceSqlite, { path: persistencePath })
|
||||
const persistence = await ctx.plugin(SqliteSessionPersistence, { path: persistencePath })
|
||||
|
||||
const loader = Object.create(Loader.prototype) as Loader
|
||||
const unwrapped = loader.unwrapExports(queryModule) as Parameters<Context['plugin']>[0]
|
||||
expect(unwrapped).toBe(SessionQuerySqlite)
|
||||
expect(unwrapped).toBe(SqliteSessionQueryEngine)
|
||||
const query = await ctx.plugin(unwrapped, { path: searchPath })
|
||||
|
||||
const id = SessionId('loader-path')
|
||||
|
||||
@@ -9,8 +9,8 @@ import SessionStore, { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/ds
|
||||
import type { SessionEvent, SessionHeader, SessionId as SessionIdType } from '@deepseek-ai/dsh-session'
|
||||
import SessionPersistence, { SessionPersistenceRevision } from '@deepseek-ai/dsh-session-persistence'
|
||||
import type { SessionPersistenceSnapshot } from '@deepseek-ai/dsh-session-persistence'
|
||||
import SessionPersistenceSqlite from '@deepseek-ai/dsh-session-persistence-sqlite'
|
||||
import SessionQuerySqlite, {
|
||||
import SqliteSessionPersistence from '@deepseek-ai/dsh-session-persistence-sqlite'
|
||||
import SqliteSessionQueryEngine, {
|
||||
SESSION_QUERY_SQLITE_SCHEMA_VERSION,
|
||||
} from '@deepseek-ai/dsh-session-query-sqlite'
|
||||
import {
|
||||
@@ -179,22 +179,22 @@ class TestPersistence extends SessionPersistence {
|
||||
}
|
||||
}
|
||||
|
||||
async function liveContext(config: ConstructorParameters<typeof SessionQuerySqlite>[1] = { path: ':memory:' }): Promise<Context> {
|
||||
async function liveContext(config: ConstructorParameters<typeof SqliteSessionQueryEngine>[1] = { path: ':memory:' }): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionQuerySqlite, config)
|
||||
await ctx.plugin(SqliteSessionQueryEngine, config)
|
||||
return ctx
|
||||
}
|
||||
|
||||
describe('SQLite session search', () => {
|
||||
it('defaults and validates opening policy and persisted inspection concurrency through its Cordis config', async () => {
|
||||
const defaultCtx = await liveContext()
|
||||
expect((defaultCtx.sessionQuery as SessionQuerySqlite).config.openAt).toBe('startup')
|
||||
expect((defaultCtx.sessionQuery as SessionQuerySqlite).config.persistedInspectConcurrency)
|
||||
expect((defaultCtx.sessionQuery as SqliteSessionQueryEngine).config.openAt).toBe('startup')
|
||||
expect((defaultCtx.sessionQuery as SqliteSessionQueryEngine).config.persistedInspectConcurrency)
|
||||
.toBe(SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY)
|
||||
|
||||
const configuredValue = 2
|
||||
const configured = new SessionQuerySqlite.Config({
|
||||
const configured = new SqliteSessionQueryEngine.Config({
|
||||
path: ':memory:',
|
||||
openAt: 'first-search',
|
||||
persistedInspectConcurrency: configuredValue,
|
||||
@@ -202,16 +202,16 @@ describe('SQLite session search', () => {
|
||||
expect(configured.openAt).toBe('first-search')
|
||||
expect(configured.persistedInspectConcurrency).toBe(configuredValue)
|
||||
const configuredCtx = await liveContext(configured)
|
||||
expect((configuredCtx.sessionQuery as SessionQuerySqlite).config.persistedInspectConcurrency)
|
||||
expect((configuredCtx.sessionQuery as SqliteSessionQueryEngine).config.persistedInspectConcurrency)
|
||||
.toBe(configuredValue)
|
||||
|
||||
for (const persistedInspectConcurrency of [0, Number.MAX_SAFE_INTEGER + 1]) {
|
||||
expect(() => new SessionQuerySqlite.Config({
|
||||
expect(() => new SqliteSessionQueryEngine.Config({
|
||||
path: ':memory:',
|
||||
persistedInspectConcurrency,
|
||||
})).toThrow()
|
||||
}
|
||||
expect(() => new SessionQuerySqlite.Config({
|
||||
expect(() => new SqliteSessionQueryEngine.Config({
|
||||
path: ':memory:',
|
||||
openAt: 'later' as never,
|
||||
})).toThrow()
|
||||
@@ -221,7 +221,7 @@ describe('SQLite session search', () => {
|
||||
const path = await temporaryPath('unopened.db')
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const search = await ctx.plugin(SessionQuerySqlite, {
|
||||
const search = await ctx.plugin(SqliteSessionQueryEngine, {
|
||||
path,
|
||||
openAt: 'first-search',
|
||||
})
|
||||
@@ -234,11 +234,11 @@ describe('SQLite session search', () => {
|
||||
it('opens once on the first search and reuses readiness for later searches', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionQuerySqlite, {
|
||||
await ctx.plugin(SqliteSessionQueryEngine, {
|
||||
path: ':memory:',
|
||||
openAt: 'first-search',
|
||||
})
|
||||
const service = ctx.sessionQuery as SessionQuerySqlite
|
||||
const service = ctx.sessionQuery as SqliteSessionQueryEngine
|
||||
const internals = service as unknown as { _open(): Promise<void> }
|
||||
const open = vi.spyOn(internals, '_open')
|
||||
|
||||
@@ -251,11 +251,11 @@ describe('SQLite session search', () => {
|
||||
it('shares one readiness promise across concurrent first searches', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionQuerySqlite, {
|
||||
await ctx.plugin(SqliteSessionQueryEngine, {
|
||||
path: ':memory:',
|
||||
openAt: 'first-search',
|
||||
})
|
||||
const service = ctx.sessionQuery as SessionQuerySqlite
|
||||
const service = ctx.sessionQuery as SqliteSessionQueryEngine
|
||||
const internals = service as unknown as { _open(): Promise<void> }
|
||||
const originalOpen = internals._open.bind(internals)
|
||||
const release = Promise.withResolvers<undefined>()
|
||||
@@ -647,7 +647,7 @@ describe('SQLite session search', () => {
|
||||
]) {
|
||||
const direct = new Context()
|
||||
await direct.plugin(SessionStore)
|
||||
expect(() => new SessionQuerySqlite(direct, config as never))
|
||||
expect(() => new SqliteSessionQueryEngine(direct, config as never))
|
||||
.toThrow(expectCode('SESSION_QUERY_INVALID_CONFIG'))
|
||||
expect(direct.sessionQuery).toBeUndefined()
|
||||
}
|
||||
@@ -1068,7 +1068,7 @@ describe('SQLite reconciliation and source lifecycle', () => {
|
||||
const first = new Context()
|
||||
await first.plugin(SessionStore)
|
||||
const firstPersistence = await first.plugin(TestPersistence)
|
||||
const firstSearch = await first.plugin(SessionQuerySqlite, { path })
|
||||
const firstSearch = await first.plugin(SqliteSessionQueryEngine, { path })
|
||||
await first.sessionQuery.searchSessions({ query: 'needle' })
|
||||
expect(Object.fromEntries(TestPersistence.inspections)).toEqual({ unchanged: 1, changed: 1, deleted: 1 })
|
||||
await first.sessionQuery.searchSessions({ query: 'needle' })
|
||||
@@ -1088,7 +1088,7 @@ describe('SQLite reconciliation and source lifecycle', () => {
|
||||
const second = new Context()
|
||||
await second.plugin(SessionStore)
|
||||
const secondPersistence = await second.plugin(TestPersistence)
|
||||
const secondSearch = await second.plugin(SessionQuerySqlite, { path })
|
||||
const secondSearch = await second.plugin(SqliteSessionQueryEngine, { path })
|
||||
const result = await second.sessionQuery.searchSessions({ query: 'needle' })
|
||||
expect(result.items.map(item => item.header.id).sort()).toEqual([added.id, changed.id, unchanged.id].sort())
|
||||
expect(Object.fromEntries(TestPersistence.inspections)).toEqual({
|
||||
@@ -1118,7 +1118,7 @@ describe('SQLite reconciliation and source lifecycle', () => {
|
||||
await first.plugin(SessionStore)
|
||||
const persistence = await first.plugin(TestPersistence)
|
||||
const live = first.sessions.create(shared.id, { seed: messageEvents('live needle'), meta: { createdAt: 10 } })
|
||||
const search = await first.plugin(SessionQuerySqlite, { path })
|
||||
const search = await first.plugin(SqliteSessionQueryEngine, { path })
|
||||
await expect(first.sessionQuery.searchEvents({ sessionId: live.id, query: 'live' })).resolves.toMatchObject({ items: [{}] })
|
||||
await search.dispose()
|
||||
await persistence.dispose()
|
||||
@@ -1126,7 +1126,7 @@ describe('SQLite reconciliation and source lifecycle', () => {
|
||||
const second = new Context()
|
||||
await second.plugin(SessionStore)
|
||||
const persistenceAgain = await second.plugin(TestPersistence)
|
||||
const searchAgain = await second.plugin(SessionQuerySqlite, { path })
|
||||
const searchAgain = await second.plugin(SqliteSessionQueryEngine, { path })
|
||||
await expect(second.sessionQuery.searchSessions({ query: 'live' })).resolves.toEqual({ items: [] })
|
||||
await expect(second.sessionQuery.searchSessions({ query: 'persisted' }))
|
||||
.resolves.toMatchObject({ items: [{ header: shared, live: false, persisted: true }] })
|
||||
@@ -1202,7 +1202,7 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
expect((await stat(path)).mode & 0o777).toBe(0o600)
|
||||
expect((await stat(`${path}-wal`)).mode & 0o777).toBe(0o600)
|
||||
expect((await stat(`${path}-shm`)).mode & 0o777).toBe(0o600)
|
||||
await (ctx.sessionQuery as SessionQuerySqlite).close()
|
||||
await (ctx.sessionQuery as SqliteSessionQueryEngine).close()
|
||||
})
|
||||
|
||||
it('creates a persistent rollback journal owner-only', async () => {
|
||||
@@ -1213,7 +1213,7 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
|
||||
expect((await stat(path)).mode & 0o777).toBe(0o600)
|
||||
expect((await stat(`${path}-journal`)).mode & 0o777).toBe(0o600)
|
||||
await (ctx.sessionQuery as SessionQuerySqlite).close()
|
||||
await (ctx.sessionQuery as SqliteSessionQueryEngine).close()
|
||||
})
|
||||
|
||||
it('preserves the mode of an existing database file', async () => {
|
||||
@@ -1226,7 +1226,7 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
await ctx.sessionQuery.searchSessions({ query: 'needle' })
|
||||
|
||||
expect((await stat(path)).mode & 0o777).toBe(0o644)
|
||||
await (ctx.sessionQuery as SessionQuerySqlite).close()
|
||||
await (ctx.sessionQuery as SqliteSessionQueryEngine).close()
|
||||
})
|
||||
|
||||
it('surfaces filesystem failures while pre-creating the database', async () => {
|
||||
@@ -1234,7 +1234,7 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
|
||||
await expect(ctx.plugin(SessionQuerySqlite, { path })).rejects.toMatchObject({
|
||||
await expect(ctx.plugin(SqliteSessionQueryEngine, { path })).rejects.toMatchObject({
|
||||
code: 'SESSION_QUERY_INDEX_FAILED',
|
||||
cause: { code: 'ERR_INVALID_ARG_VALUE' },
|
||||
})
|
||||
@@ -1244,14 +1244,14 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
it('resets a recognized incompatible schema but refuses unknown or foreign tables', { timeout: 20_000 }, async () => {
|
||||
const stalePath = await temporaryPath('stale.db')
|
||||
const staleOwner = await liveContext({ path: stalePath })
|
||||
await (staleOwner.sessionQuery as SessionQuerySqlite).close()
|
||||
await (staleOwner.sessionQuery as SqliteSessionQueryEngine).close()
|
||||
const stale = new DatabaseSync(stalePath)
|
||||
stale.exec(`PRAGMA user_version = ${SESSION_QUERY_SQLITE_SCHEMA_VERSION - 1}`)
|
||||
stale.close()
|
||||
const staleCtx = await liveContext({ path: stalePath })
|
||||
staleCtx.sessions.create(SessionId('live'), { seed: messageEvents('needle') })
|
||||
await staleCtx.sessionQuery.searchSessions({ query: 'needle' })
|
||||
await (staleCtx.sessionQuery as SessionQuerySqlite).close()
|
||||
await (staleCtx.sessionQuery as SqliteSessionQueryEngine).close()
|
||||
const rebuilt = new DatabaseSync(stalePath)
|
||||
expect((rebuilt.prepare('PRAGMA user_version').get() as { user_version: number }).user_version)
|
||||
.toBe(SESSION_QUERY_SQLITE_SCHEMA_VERSION)
|
||||
@@ -1259,7 +1259,7 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
|
||||
const augmentedPath = await temporaryPath('augmented.db')
|
||||
const augmentedOwner = await liveContext({ path: augmentedPath })
|
||||
await (augmentedOwner.sessionQuery as SessionQuerySqlite).close()
|
||||
await (augmentedOwner.sessionQuery as SqliteSessionQueryEngine).close()
|
||||
const augmented = new DatabaseSync(augmentedPath)
|
||||
augmented.exec('CREATE TABLE unrelated(value TEXT)')
|
||||
augmented.exec("INSERT INTO unrelated VALUES ('safe')")
|
||||
@@ -1267,7 +1267,7 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
augmented.close()
|
||||
const augmentedCtx = new Context()
|
||||
await augmentedCtx.plugin(SessionStore)
|
||||
await expect(augmentedCtx.plugin(SessionQuerySqlite, { path: augmentedPath }))
|
||||
await expect(augmentedCtx.plugin(SqliteSessionQueryEngine, { path: augmentedPath }))
|
||||
.rejects.toThrow(expectCode('SESSION_QUERY_INDEX_FAILED'))
|
||||
expect(augmentedCtx.sessionQuery).toBeUndefined()
|
||||
const stillAugmented = new DatabaseSync(augmentedPath)
|
||||
@@ -1277,14 +1277,14 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
|
||||
const currentAugmentedPath = await temporaryPath('current-augmented.db')
|
||||
const currentAugmentedOwner = await liveContext({ path: currentAugmentedPath })
|
||||
await (currentAugmentedOwner.sessionQuery as SessionQuerySqlite).close()
|
||||
await (currentAugmentedOwner.sessionQuery as SqliteSessionQueryEngine).close()
|
||||
const currentAugmented = new DatabaseSync(currentAugmentedPath)
|
||||
currentAugmented.exec('CREATE TABLE unrelated(value TEXT)')
|
||||
currentAugmented.exec("INSERT INTO unrelated VALUES ('safe')")
|
||||
currentAugmented.close()
|
||||
const currentAugmentedCtx = new Context()
|
||||
await currentAugmentedCtx.plugin(SessionStore)
|
||||
await expect(currentAugmentedCtx.plugin(SessionQuerySqlite, {
|
||||
await expect(currentAugmentedCtx.plugin(SqliteSessionQueryEngine, {
|
||||
path: currentAugmentedPath,
|
||||
journalMode: 'delete',
|
||||
})).rejects.toThrow(expectCode('SESSION_QUERY_INDEX_FAILED'))
|
||||
@@ -1304,7 +1304,7 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
foreign.close()
|
||||
const foreignCtx = new Context()
|
||||
await foreignCtx.plugin(SessionStore)
|
||||
await expect(foreignCtx.plugin(SessionQuerySqlite, { path: foreignPath, journalMode: 'delete' }))
|
||||
await expect(foreignCtx.plugin(SqliteSessionQueryEngine, { path: foreignPath, journalMode: 'delete' }))
|
||||
.rejects.toThrow(expectCode('SESSION_QUERY_INDEX_FAILED'))
|
||||
expect(foreignCtx.sessionQuery).toBeUndefined()
|
||||
const stillForeign = new DatabaseSync(foreignPath)
|
||||
@@ -1320,7 +1320,7 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
wildcard.close()
|
||||
const wildcardCtx = new Context()
|
||||
await wildcardCtx.plugin(SessionStore)
|
||||
await expect(wildcardCtx.plugin(SessionQuerySqlite, {
|
||||
await expect(wildcardCtx.plugin(SqliteSessionQueryEngine, {
|
||||
path: wildcardPath,
|
||||
journalMode: 'delete',
|
||||
})).rejects.toThrow(expectCode('SESSION_QUERY_INDEX_FAILED'))
|
||||
@@ -1338,7 +1338,7 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
otherApp.close()
|
||||
const otherAppCtx = new Context()
|
||||
await otherAppCtx.plugin(SessionStore)
|
||||
await expect(otherAppCtx.plugin(SessionQuerySqlite, { path: otherAppPath }))
|
||||
await expect(otherAppCtx.plugin(SqliteSessionQueryEngine, { path: otherAppPath }))
|
||||
.rejects.toThrow(expectCode('SESSION_QUERY_INDEX_FAILED'))
|
||||
expect(otherAppCtx.sessionQuery).toBeUndefined()
|
||||
})
|
||||
@@ -1354,7 +1354,7 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
try {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await expect(ctx.plugin(SessionQuerySqlite, { path }))
|
||||
await expect(ctx.plugin(SqliteSessionQueryEngine, { path }))
|
||||
.rejects.toThrow(expectCode('SESSION_QUERY_INDEX_FAILED'))
|
||||
await new Promise<void>((resolve) => { setImmediate(resolve) })
|
||||
expect(unhandled).toEqual([])
|
||||
@@ -1372,18 +1372,18 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
|
||||
const lazyCtx = new Context()
|
||||
await lazyCtx.plugin(SessionStore)
|
||||
const lazy = await lazyCtx.plugin(SessionQuerySqlite, {
|
||||
const lazy = await lazyCtx.plugin(SqliteSessionQueryEngine, {
|
||||
path,
|
||||
openAt: 'first-search',
|
||||
})
|
||||
expect(lazyCtx.sessionQuery).toBeInstanceOf(SessionQuerySqlite)
|
||||
expect(lazyCtx.sessionQuery).toBeInstanceOf(SqliteSessionQueryEngine)
|
||||
await expect(lazyCtx.sessionQuery.searchSessions({ query: 'needle' }))
|
||||
.rejects.toThrow(expectCode('SESSION_QUERY_INDEX_FAILED'))
|
||||
await lazy.dispose()
|
||||
|
||||
const eagerCtx = new Context()
|
||||
await eagerCtx.plugin(SessionStore)
|
||||
await expect(eagerCtx.plugin(SessionQuerySqlite, { path }))
|
||||
await expect(eagerCtx.plugin(SqliteSessionQueryEngine, { path }))
|
||||
.rejects.toThrow(expectCode('SESSION_QUERY_INDEX_FAILED'))
|
||||
expect(eagerCtx.sessionQuery).toBeUndefined()
|
||||
})
|
||||
@@ -1681,7 +1681,7 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
}
|
||||
const ctx = await liveContext()
|
||||
await ctx.plugin(TestPersistence)
|
||||
const search = ctx.sessionQuery as SessionQuerySqlite
|
||||
const search = ctx.sessionQuery as SqliteSessionQueryEngine
|
||||
const accepted = search.searchSessions({ query: 'needle' })
|
||||
await started
|
||||
const queued = search.searchSessions({ query: 'needle' })
|
||||
@@ -1702,7 +1702,7 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
TestPersistence.reset()
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const search = await ctx.plugin(SessionQuerySqlite, { path: ':memory:' })
|
||||
const search = await ctx.plugin(SqliteSessionQueryEngine, { path: ':memory:' })
|
||||
const persistence = await ctx.plugin(TestPersistence)
|
||||
const optional = (ctx.sessionQuery as unknown as {
|
||||
_optionalPersistenceFiber: Fiber
|
||||
@@ -1725,8 +1725,8 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
const searchPath = await temporaryPath('derived.db')
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const persistence = await ctx.plugin(SessionPersistenceSqlite, { path: persistencePath })
|
||||
const search = await ctx.plugin(SessionQuerySqlite, { path: searchPath })
|
||||
const persistence = await ctx.plugin(SqliteSessionPersistence, { path: persistencePath })
|
||||
const search = await ctx.plugin(SqliteSessionQueryEngine, { path: searchPath })
|
||||
const meta = header('real', 10, { cwd: '/work' })
|
||||
await ctx.sessionPersistence.create(meta)
|
||||
await ctx.sessionPersistence.append(meta.id, messageEvents('real SQLite needle'))
|
||||
@@ -1750,11 +1750,11 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
|
||||
const first = new Context()
|
||||
await first.plugin(SessionStore)
|
||||
const persistenceA = await first.plugin(SessionPersistenceSqlite, { path: persistencePathA })
|
||||
const persistenceA = await first.plugin(SqliteSessionPersistence, { path: persistencePathA })
|
||||
await first.sessionPersistence.create(shared)
|
||||
await first.sessionPersistence.append(shared.id, messageEvents('alpha source'))
|
||||
const inspectA = vi.spyOn(first.sessionPersistence, 'inspect')
|
||||
const searchA = await first.plugin(SessionQuerySqlite, { path: searchPath })
|
||||
const searchA = await first.plugin(SqliteSessionQueryEngine, { path: searchPath })
|
||||
await expect(first.sessionQuery.searchSessions({ query: 'alpha' }))
|
||||
.resolves.toMatchObject({ items: [{ header: shared }] })
|
||||
expect(inspectA).toHaveBeenCalledTimes(1)
|
||||
@@ -1763,9 +1763,9 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
|
||||
const reopened = new Context()
|
||||
await reopened.plugin(SessionStore)
|
||||
const persistenceAAgain = await reopened.plugin(SessionPersistenceSqlite, { path: persistencePathA })
|
||||
const persistenceAAgain = await reopened.plugin(SqliteSessionPersistence, { path: persistencePathA })
|
||||
const reopenedInspect = vi.spyOn(reopened.sessionPersistence, 'inspect')
|
||||
const searchAAgain = await reopened.plugin(SessionQuerySqlite, { path: searchPath })
|
||||
const searchAAgain = await reopened.plugin(SqliteSessionQueryEngine, { path: searchPath })
|
||||
await expect(reopened.sessionQuery.searchSessions({ query: 'alpha' }))
|
||||
.resolves.toMatchObject({ items: [{ header: shared }] })
|
||||
expect(reopenedInspect).not.toHaveBeenCalled()
|
||||
@@ -1774,11 +1774,11 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
||||
|
||||
const second = new Context()
|
||||
await second.plugin(SessionStore)
|
||||
const persistenceB = await second.plugin(SessionPersistenceSqlite, { path: persistencePathB })
|
||||
const persistenceB = await second.plugin(SqliteSessionPersistence, { path: persistencePathB })
|
||||
await second.sessionPersistence.create(shared)
|
||||
await second.sessionPersistence.append(shared.id, messageEvents('bravo source'))
|
||||
const inspectB = vi.spyOn(second.sessionPersistence, 'inspect')
|
||||
const searchB = await second.plugin(SessionQuerySqlite, { path: searchPath })
|
||||
const searchB = await second.plugin(SqliteSessionQueryEngine, { path: searchPath })
|
||||
await expect(second.sessionQuery.searchSessions({ query: 'bravo' }))
|
||||
.resolves.toMatchObject({ items: [{ header: shared }] })
|
||||
await expect(second.sessionQuery.searchSessions({ query: 'alpha' })).resolves.toEqual({ items: [] })
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"path": "../../session/session-persistence"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
},
|
||||
{
|
||||
"path": "../session-query"
|
||||
|
||||
@@ -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 packages/session-query/session-query/README.md
|
||||
README.md: 5d4d689477fca4fd6b01091f1304ff3065af7443
|
||||
README.zh.md: e37de1b43fb67379ff70693d9eb7dc139db7e7a9
|
||||
README.md: 02640bee4b3eb7f6877756a2ce84b26f698c9f58
|
||||
README.zh.md: 794c0e6e86606d92ed82ec286844647487df3295
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
`SessionQueryService` is the combined abstract `ctx.sessionQuery` contract. It implements exact session-history retrieval, relationship tracing, and provider-independent filtering over live `ctx.sessions` plus optional dynamically mounted `ctx.sessionPersistence`; concrete backends implement its two full-text methods. Matching ids produce one record: live events win, while `live` and `persisted` report both source availabilities. Conflicting immutable headers fail with `SESSION_QUERY_SOURCE_CONFLICT`.
|
||||
`SessionQueryEngine` is the combined abstract `ctx.sessionQuery` contract. It implements exact session-history retrieval, relationship tracing, and provider-independent filtering over live `ctx.sessions` plus optional dynamically mounted `ctx.sessionPersistence`; concrete backends implement its two full-text methods. Matching ids produce one record: live events win, while `live` and `persisted` report both source availabilities. Conflicting immutable headers fail with `SESSION_QUERY_SOURCE_CONFLICT`.
|
||||
|
||||
## Reads
|
||||
|
||||
@@ -27,7 +27,7 @@ The text clause is deliberately independent of FTS providers: caller text is esc
|
||||
|
||||
## Full-text methods
|
||||
|
||||
`SessionQueryService.searchSessions(request, exec?)` groups the logical corpus by strongest matching event; `searchEvents(request, exec?)` searches one logical session. These are the service's only abstract methods. Both return pages whose continuation is an owned branded `SessionSearchCursor`, accept optional cancellation, and expose snippets without provider-specific numeric scores. An event-search page also carries the cloned target header from the same indexed generation as its hits, allowing authorization consumers to bind policy to the payload observation. Search requests accept only metadata event filters, because literal-text filtering is the scan path described above.
|
||||
`SessionQueryEngine.searchSessions(request, exec?)` groups the logical corpus by strongest matching event; `searchEvents(request, exec?)` searches one logical session. These are the service's only abstract methods. Both return pages whose continuation is an owned branded `SessionSearchCursor`, accept optional cancellation, and expose snippets without provider-specific numeric scores. An event-search page also carries the cloned target header from the same indexed generation as its hits, allowing authorization consumers to bind policy to the payload observation. Search requests accept only metadata event filters, because literal-text filtering is the scan path described above.
|
||||
|
||||
The package has no provider coordinator, fallback implementation, or standalone concrete plugin. A concrete service backend inherits the implemented reads, filters, and traces while owning full-text observation, reconciliation, ranking, cursor generations, and query execution; the first implementation is [`@deepseek-ai/dsh-session-query-sqlite`](../session-query-sqlite/README.md).
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
`SessionQueryService` 是组合式抽象 `ctx.sessionQuery` 约定。它对实时 `ctx.sessions` 和可选的动态挂载 `ctx.sessionPersistence` 实现精确会话历史取回、关系跟踪和与提供方无关的过滤;具体后端实现它的两个全文方法。匹配 id 只产生一条记录:实时事件优先,而 `live` 和 `persisted` 会报告两种来源的可用性。如果不可变 header 存在冲突,则以 `SESSION_QUERY_SOURCE_CONFLICT` 失败。
|
||||
`SessionQueryEngine` 是组合式抽象 `ctx.sessionQuery` 约定。它对实时 `ctx.sessions` 和可选的动态挂载 `ctx.sessionPersistence` 实现精确会话历史取回、关系跟踪和与提供方无关的过滤;具体后端实现它的两个全文方法。匹配 id 只产生一条记录:实时事件优先,而 `live` 和 `persisted` 会报告两种来源的可用性。如果不可变 header 存在冲突,则以 `SESSION_QUERY_SOURCE_CONFLICT` 失败。
|
||||
|
||||
## 读取
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
## 全文方法
|
||||
|
||||
`SessionQueryService.searchSessions(request, exec?)` 按匹配最强的事件对逻辑语料库分组;`searchEvents(request, exec?)` 搜索一个逻辑会话。这两个是服务仅有的抽象方法。两者都返回分页结果,其延续信息是由服务持有的带品牌 `SessionSearchCursor`;接受可选取消,并在不使用提供方专用数值分数的情况下提供摘录。事件搜索分页结果还携带来自与命中相同索引世代的克隆目标 header,使授权消费方可将策略绑定到此次载荷观察。搜索请求只接受事件元数据过滤器,因为字面文本过滤使用上文所述扫描路径。
|
||||
`SessionQueryEngine.searchSessions(request, exec?)` 按匹配最强的事件对逻辑语料库分组;`searchEvents(request, exec?)` 搜索一个逻辑会话。这两个是服务仅有的抽象方法。两者都返回分页结果,其延续信息是由服务持有的带品牌 `SessionSearchCursor`;接受可选取消,并在不使用提供方专用数值分数的情况下提供摘录。事件搜索分页结果还携带来自与命中相同索引世代的克隆目标 header,使授权消费方可将策略绑定到此次载荷观察。搜索请求只接受事件元数据过滤器,因为字面文本过滤使用上文所述扫描路径。
|
||||
|
||||
该包没有提供方协调器、回退实现或独立具体插件。具体服务后端继承已实现的读取、过滤和跟踪,同时负责全文观察、对账、排名、游标世代和查询执行;第一个实现是 [`@deepseek-ai/dsh-session-query-sqlite`](../session-query-sqlite/README.md)。
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export { assertSessionHeadersCompatible } from './sources.ts'
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
sessionQuery: SessionQueryService
|
||||
sessionQuery: SessionQueryEngine
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ declare module '@deepseek-ai/cordis' {
|
||||
* A backend implements full-text observation, reconciliation, ranking, cursor
|
||||
* generations, and query execution on the same `ctx.sessionQuery` service.
|
||||
*/
|
||||
export abstract class SessionQueryService extends Service {
|
||||
export abstract class SessionQueryEngine extends Service {
|
||||
static inject = ['sessions']
|
||||
|
||||
private readonly _readWindowMax: number
|
||||
@@ -356,4 +356,4 @@ export abstract class SessionQueryService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
export default SessionQueryService
|
||||
export default SessionQueryEngine
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
materializeSessionResultFilters,
|
||||
type SessionQueryErrorCode,
|
||||
} from '@deepseek-ai/dsh-session-query'
|
||||
import { TestSessionQueryService } from './test-service.ts'
|
||||
import { TestSessionQueryEngine } from './test-service.ts'
|
||||
|
||||
const id = SessionId('session')
|
||||
|
||||
@@ -274,7 +274,7 @@ describe('session-query document and filter helpers', () => {
|
||||
it('exposes the scan path on the combined query service', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(TestSessionQueryService)
|
||||
await ctx.plugin(TestSessionQueryEngine)
|
||||
const session = ctx.sessions.create(id)
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'Alpha\n beta' }], source: { kind: 'user' },
|
||||
@@ -290,7 +290,7 @@ describe('session-query document and filter helpers', () => {
|
||||
it('registers exact and abstract search behavior under one ctx key', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const fiber = await ctx.plugin(TestSessionQueryService)
|
||||
const fiber = await ctx.plugin(TestSessionQueryEngine)
|
||||
const session = ctx.sessions.create(id)
|
||||
await expect(ctx.sessionQuery.searchSessions({ query: 'AI' })).resolves.toEqual({ items: [] })
|
||||
await expect(ctx.sessionQuery.searchEvents({ sessionId: id, query: 'AI' }))
|
||||
|
||||
@@ -4,13 +4,13 @@ import { Context, type Fiber } from '@deepseek-ai/cordis'
|
||||
import SessionStore, { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent, SessionHeader, SessionId as SessionIdType } from '@deepseek-ai/dsh-session'
|
||||
import SessionPersistence, { SessionPersistenceCorruptionError, SessionPersistenceRevision } from '@deepseek-ai/dsh-session-persistence'
|
||||
import SessionQueryService, {
|
||||
import SessionQueryEngine, {
|
||||
SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY,
|
||||
type SessionEventSurface,
|
||||
type SessionQueryErrorCode,
|
||||
} from '@deepseek-ai/dsh-session-query'
|
||||
import { SessionTitleProviderId } from '@deepseek-ai/dsh-session-title'
|
||||
import { TestSessionQueryService } from './test-service.ts'
|
||||
import { TestSessionQueryEngine } from './test-service.ts'
|
||||
|
||||
function header(id: string, createdAt = 1, extra: Partial<SessionHeader> = {}): SessionHeader {
|
||||
return { version: SESSION_FORMAT_VERSION, id: SessionId(id), createdAt, ...extra }
|
||||
@@ -122,10 +122,10 @@ class TestPersistence extends SessionPersistence {
|
||||
}
|
||||
}
|
||||
|
||||
async function liveContext(config: ConstructorParameters<typeof TestSessionQueryService>[1] = {}): Promise<Context> {
|
||||
async function liveContext(config: ConstructorParameters<typeof TestSessionQueryEngine>[1] = {}): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(TestSessionQueryService, config)
|
||||
await ctx.plugin(TestSessionQueryEngine, config)
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -1176,7 +1176,7 @@ describe('session-query exact reads', () => {
|
||||
|
||||
const direct = new Context()
|
||||
await direct.plugin(SessionStore)
|
||||
expect(new TestSessionQueryService(direct)).toBeInstanceOf(SessionQueryService)
|
||||
expect(new TestSessionQueryEngine(direct)).toBeInstanceOf(SessionQueryEngine)
|
||||
for (const config of [
|
||||
{ readWindowMax: -1 },
|
||||
{ persistedInspectConcurrency: 0 },
|
||||
@@ -1184,7 +1184,7 @@ describe('session-query exact reads', () => {
|
||||
]) {
|
||||
const invalid = new Context()
|
||||
await invalid.plugin(SessionStore)
|
||||
expect(() => new TestSessionQueryService(invalid, config))
|
||||
expect(() => new TestSessionQueryEngine(invalid, config))
|
||||
.toThrow(expectCode('SESSION_QUERY_INVALID_CONFIG'))
|
||||
}
|
||||
})
|
||||
@@ -1192,8 +1192,8 @@ describe('session-query exact reads', () => {
|
||||
it('leaves the optional persistence dependency optional', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const fiber = await ctx.plugin(TestSessionQueryService)
|
||||
expect(ctx.sessionQuery).toBeInstanceOf(TestSessionQueryService)
|
||||
const fiber = await ctx.plugin(TestSessionQueryEngine)
|
||||
expect(ctx.sessionQuery).toBeInstanceOf(TestSessionQueryEngine)
|
||||
await fiber.dispose()
|
||||
expect(ctx.sessionQuery).toBeUndefined()
|
||||
})
|
||||
@@ -1202,7 +1202,7 @@ describe('session-query exact reads', () => {
|
||||
TestPersistence.reset()
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const query = await ctx.plugin(TestSessionQueryService)
|
||||
const query = await ctx.plugin(TestSessionQueryEngine)
|
||||
const persistence = await ctx.plugin(TestPersistence)
|
||||
const optional = (ctx.sessionQuery as unknown as {
|
||||
_corpus: { _optionalPersistenceFiber: Fiber }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import SessionQueryService from '@deepseek-ai/dsh-session-query'
|
||||
import SessionQueryEngine from '@deepseek-ai/dsh-session-query'
|
||||
import type {
|
||||
SessionEventSearchPage,
|
||||
SessionEventSearchRequest,
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
} from '@deepseek-ai/dsh-session-query'
|
||||
|
||||
/** Test-only concrete query service for backend-independent behavior. */
|
||||
export class TestSessionQueryService extends SessionQueryService {
|
||||
export class TestSessionQueryEngine extends SessionQueryEngine {
|
||||
override searchSessions(
|
||||
_request: SessionSearchRequest,
|
||||
_exec?: SessionSearchExecContext,
|
||||
|
||||
@@ -5,7 +5,7 @@ import SessionStore, { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/ds
|
||||
import type { Session, SessionEvent, SessionHeader, SessionId as SessionIdType } from '@deepseek-ai/dsh-session'
|
||||
import SessionPersistence from '@deepseek-ai/dsh-session-persistence'
|
||||
import { type SessionQueryErrorCode } from '@deepseek-ai/dsh-session-query'
|
||||
import { TestSessionQueryService } from './test-service.ts'
|
||||
import { TestSessionQueryEngine } from './test-service.ts'
|
||||
|
||||
type MutableSessionHeader = { -readonly [K in keyof SessionHeader]: SessionHeader[K] }
|
||||
|
||||
@@ -99,7 +99,7 @@ class TracePersistence extends SessionPersistence {
|
||||
async function queryContext(): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(TestSessionQueryService)
|
||||
await ctx.plugin(TestSessionQueryEngine)
|
||||
return ctx
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"path": "../../session/session-persistence"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"@deepseek-ai/dsh-session-title": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-timeout": "workspace:^",
|
||||
"@deepseek-ai/dsh-timeout-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-call-timeout-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ import SessionStore, {
|
||||
SessionId,
|
||||
type Session,
|
||||
} from '@deepseek-ai/dsh-session'
|
||||
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import SessionQuerySqlite from '@deepseek-ai/dsh-session-query-sqlite'
|
||||
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import SqliteSessionQueryEngine from '@deepseek-ai/dsh-session-query-sqlite'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||
import ToolRuntime from '@deepseek-ai/dsh-tools'
|
||||
import * as ToolSessionQuery from '@deepseek-ai/dsh-tool-session-query'
|
||||
|
||||
const temporaryDirectories: string[] = []
|
||||
@@ -38,9 +38,9 @@ describe('tool-session-query with the real SQLite provider', () => {
|
||||
contexts.push(ctx)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(SessionPersistenceJsonl, { root, compression: 'none' })
|
||||
await ctx.plugin(SessionQuerySqlite, { path: join(root, 'session-query.db') })
|
||||
await ctx.plugin(ToolRuntime)
|
||||
await ctx.plugin(JsonlSessionPersistence, { root, compression: 'none' })
|
||||
await ctx.plugin(SqliteSessionQueryEngine, { path: join(root, 'session-query.db') })
|
||||
await ctx.plugin(ToolSessionQuery)
|
||||
|
||||
const persisted = SessionId('persisted')
|
||||
@@ -107,9 +107,9 @@ describe('tool-session-query with the real SQLite provider', () => {
|
||||
contexts.push(ctx)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(SessionPersistenceJsonl, { root, compression: 'none' })
|
||||
await ctx.plugin(SessionQuerySqlite, { path: join(root, 'session-query.db') })
|
||||
await ctx.plugin(ToolRuntime)
|
||||
await ctx.plugin(JsonlSessionPersistence, { root, compression: 'none' })
|
||||
await ctx.plugin(SqliteSessionQueryEngine, { path: join(root, 'session-query.db') })
|
||||
await ctx.plugin(ToolSessionQuery)
|
||||
|
||||
const base = Date.parse('2026-07-24T00:00:00.000Z')
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Context, type Fiber } from '@deepseek-ai/cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { createUserMessage, CallId, HarnessError , createMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { MAX_TIMER_DELAY_MS, TimeoutReason } from '@deepseek-ai/dsh-timeout'
|
||||
import * as TimeoutPolicy from '@deepseek-ai/dsh-timeout-policy'
|
||||
import * as TimeoutPolicy from '@deepseek-ai/dsh-tool-call-timeout-policy'
|
||||
import SessionStore, {
|
||||
SESSION_FORMAT_VERSION,
|
||||
SessionId,
|
||||
@@ -11,7 +11,7 @@ import SessionStore, {
|
||||
type SessionHeader,
|
||||
type SessionId as SessionIdValue,
|
||||
} from '@deepseek-ai/dsh-session'
|
||||
import SessionQueryService, {
|
||||
import SessionQueryEngine, {
|
||||
SessionQueryError,
|
||||
SessionSearchCursor,
|
||||
type SessionEventSearchHit,
|
||||
@@ -25,7 +25,7 @@ import SessionQueryService, {
|
||||
type SessionTitleObservationResult,
|
||||
} from '@deepseek-ai/dsh-session-query'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { type ToolExecutionResult } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRuntime, { type ToolExecutionResult } from '@deepseek-ai/dsh-tools'
|
||||
import * as ToolSessionQuery from '@deepseek-ai/dsh-tool-session-query'
|
||||
|
||||
const activeContexts: Context[] = []
|
||||
@@ -111,7 +111,7 @@ function eventHit(sessionId: SessionIdValue, seq: number, text = 'needle excerpt
|
||||
}
|
||||
}
|
||||
|
||||
class FakeQuery extends SessionQueryService {
|
||||
class FakeQuery extends SessionQueryEngine {
|
||||
static sessionSearch: (
|
||||
request: SessionSearchRequest,
|
||||
exec?: SessionSearchExecContext,
|
||||
@@ -204,7 +204,7 @@ async function mount(
|
||||
activeContexts.push(ctx)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(ToolRuntime)
|
||||
if (enforceTimeout) await ctx.plugin(TimeoutPolicy)
|
||||
await ctx.plugin(FakeQuery)
|
||||
const fiber = await ctx.plugin(ToolSessionQuery, config)
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"path": "../session-query"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
},
|
||||
{
|
||||
"path": "../../util/timeout"
|
||||
|
||||
Reference in New Issue
Block a user