Merge branch 'master' into docs/readme-human-polish-2

This commit is contained in:
j-xiang
2026-08-13 12:01:22 +08:00
committed by GitHub
23 changed files with 207 additions and 41 deletions

View File

@@ -106,13 +106,19 @@
- id: attachment-local
name: '@deepseek-ai/dsh-attachment-local'
# Raw configs can supply a process-local path or disable this shared session
# capability. The neutral default is process-local and opens only when used.
# Full-text session search is opt-in. `openAt: never` keeps
# ctx.sessionQuery mounted — exact reads, titles, and lineage traces
# (session export, subagent-fork Workspace inheritance) stay available —
# while search calls fail with SESSION_QUERY_SEARCH_DISABLED and SQLite is
# never opened; the Web sidebar search matches titles and workspace names
# only. Deployments enabling content search override `openAt` to
# `first-search` or `startup` in a later patch layer (profile
# cordis.patch.yml or a --patch overlay), typically with a durable `path`.
- id: session-query-sqlite
name: '@deepseek-ai/dsh-session-query-sqlite'
config:
path: ':memory:'
openAt: first-search
openAt: never
# Shared projection registry: subagent catalog identity (mode/label) folds
# through its registered units, so the `list_agents` surface below fails

View File

@@ -22,14 +22,15 @@
- id: hmr
disabled: true
# Web content search runs on an ephemeral in-memory index. The service
# activates at boot, while first-search defers the node:sqlite import and
# in-memory handle so Node 22 startup stays quiet until content search
# actually uses SQLite. That search then reconciles this boot's sources.
# Full-text session search is opt-in (the base row's `openAt: never`). This
# restatement keeps the Web values on one ephemeral in-memory index; a
# deployment enabling content search overrides `openAt` to `first-search` in a
# later patch layer, which defers the node:sqlite import and in-memory handle
# to the first search so Node 22 startup stays quiet.
- id: session-query-sqlite
config:
path: ':memory:'
openAt: first-search
openAt: never
- id: tools
config:

View File

@@ -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: 846f79bd8383a8114a6d1329e7dbe931a67d98d0
README.zh.md: e84e521dd5f392dbcede1e1dd957e4ce0a9bc50f
README.md: 25257ddbe672d47faf39e1472a1bd882205d8c4e
README.zh.md: 584e466a5042334e22797a6c2794cce910844d5a

View File

@@ -16,7 +16,7 @@ All three surfaces (`current`, `shadowed`, and `log-only`) are searchable by def
The service requires `ctx.sessions` and observes optional `ctx.sessionPersistence` dynamically. One serialized state machine compares source-qualified lightweight durable snapshot revisions, non-mutatingly inspects only new or changed logs, extracts shared semantic documents, reconciles changes transactionally, and runs the query. Session queries never invoke the persistence backend's crash-repairing `load()`; an owner attaching during inspection cannot mutate its log, and the stable-observation retry makes the result live-preferred. The TEMP live row still records persisted availability, and the durable base refreshes after that live owner detaches. Repeated queries and an unchanged same-store reopen perform no full durable-log inspection; switching stores, or observing new, changed, deleted, or externally load-repaired sources, reconciles on the next stable observation. Source or transaction failure commits nothing, and the next search retries.
`openAt: startup` is the default: service activation imports `node:sqlite`, opens the handle, and fails before publication when the index is invalid. `openAt: first-search` publishes the service as ACTIVE without importing the SQLite module or opening a handle; the first concurrent searches share one readiness promise, and disposal before any search opens nothing. This mode supports compositions that need clean Node 22 startup output by deferring SQLite's experimental warning until the first actual search; it does not suppress a warning at that point. An invalid database likewise fails the first search instead of service activation.
`openAt: startup` is the default: service activation imports `node:sqlite`, opens the handle, and fails before publication when the index is invalid. `openAt: first-search` publishes the service as ACTIVE without importing the SQLite module or opening a handle; the first concurrent searches share one readiness promise, and disposal before any search opens nothing. This mode supports compositions that need clean Node 22 startup output by deferring SQLite's experimental warning until the first actual search; it does not suppress a warning at that point. An invalid database likewise fails the first search instead of service activation. `openAt: never` turns full-text search off for the deployment: `searchSessions` and `searchEvents` fail with `SESSION_QUERY_SEARCH_DISABLED` before any request normalization, node:sqlite is never imported or opened, and no source observation or reconciliation runs, while every inherited exact read, filter, and trace on `ctx.sessionQuery` keeps working.
Persisted FTS rows live in a dedicated derived database. Connection-local TEMP tables hold live rows, which shadow the durable base for the same session and reveal it when the live owner disappears. Unmounting persistence hides durable rows without discarding the cache; remounting reconciles it. Closing or reopening the database drops every live overlay while retaining persisted rows.
@@ -27,7 +27,7 @@ The database is disposable but reset is guarded: every recognized schema version
| Key | Default | Contract |
|---|---:|---|
| `path` | required | Dedicated derived-index SQLite path; `:memory:` is supported. Missing filesystem paths are created owner-only on POSIX filesystems. |
| `openAt` | `startup` | `startup` opens before service activation completes; `first-search` defers the SQLite module and handle until search. |
| `openAt` | `startup` | `startup` opens before service activation completes; `first-search` defers the SQLite module and handle until search; `never` disables full-text search (typed `SESSION_QUERY_SEARCH_DISABLED` failures) while inherited reads stay available. |
| `journalMode` | `wal` | `wal`, `delete`, `truncate`, or `persist`. |
| `defaultLimit` | `20` | Page size when a request omits `limit`; at most `Number.MAX_SAFE_INTEGER - 1`. |
| `maxLimit` | `100` | Largest accepted request page size; at most `Number.MAX_SAFE_INTEGER - 1`. |

View File

@@ -16,7 +16,7 @@
该服务需要 `ctx.sessions`,并动态观察可选的 `ctx.sessionPersistence`。一个串行化状态机比较来源限定的轻量持久化快照修订,仅以不修改日志的方式检查新日志或已更改日志,提取共享语义文档,以事务方式对账变更,然后运行查询。会话查询绝不会调用持久化后端会修复崩溃的 `load()`检查期间接入的活动所有者无法修改其日志稳定观察重试使结果优先使用实时来源。TEMP 实时行仍会记录持久化可用性,而持久基库会在该活动所有者脱离后刷新。重复查询以及同一存储未发生变化的重新打开操作不会执行完整持久化日志检查;切换存储,或观察到新增、已更改、已删除或经外部 load 修复的来源时,会在下次稳定观察时对账。来源或事务失败不会提交任何内容,下一次搜索会重试。
`openAt: startup` 是默认值:服务激活会导入 `node:sqlite` 并打开句柄;如果索引无效,则会在服务发布前失败。`openAt: first-search` 会将服务以 ACTIVE 状态发布,同时不导入 SQLite 模块也不打开句柄;首批并发搜索共享同一个就绪 promise在任何搜索前 dispose资源释放服务时也不会导入模块或打开句柄。此模式通过把 SQLite 的实验性警告推迟到首次实际搜索,支持需要干净 Node 22 启动输出的组合;它不会抑制届时的警告。无效数据库同样会使首次搜索失败,而不是服务激活失败。
`openAt: startup` 是默认值:服务激活会导入 `node:sqlite` 并打开句柄;如果索引无效,则会在服务发布前失败。`openAt: first-search` 会将服务以 ACTIVE 状态发布,同时不导入 SQLite 模块也不打开句柄;首批并发搜索共享同一个就绪 promise在任何搜索前 dispose资源释放服务时也不会导入模块或打开句柄。此模式通过把 SQLite 的实验性警告推迟到首次实际搜索,支持需要干净 Node 22 启动输出的组合;它不会抑制届时的警告。无效数据库同样会使首次搜索失败,而不是服务激活失败。`openAt: never` 为该部署关闭全文搜索:`searchSessions``searchEvents` 在任何请求规范化之前就以 `SESSION_QUERY_SEARCH_DISABLED` 失败node:sqlite 绝不会被导入或打开,也不运行任何来源观察或对账,而 `ctx.sessionQuery` 上继承的全部精确读取、过滤和跟踪保持可用。
持久化 FTS 行位于专用派生数据库中。连接本地 TEMP 表保存实时行,这些行会遮蔽同一会话的持久化基库,并在实时所有者消失后使其重新可见。卸载持久化会隐藏持久行,但不会丢弃缓存;重新挂载会对账缓存。关闭或重新打开数据库会删除全部实时覆盖层,但保留持久行。
@@ -27,7 +27,7 @@
| 键 | 默认值 | 约定 |
|---|---:|---|
| `path` | 必填 | 专用派生索引 SQLite 路径;支持 `:memory:`。在 POSIX 文件系统上,缺失的文件系统路径会以仅所有者可访问的方式创建。 |
| `openAt` | `startup` | `startup` 会在服务激活完成前打开;`first-search` 把 SQLite 模块与句柄推迟到搜索时再加载和打开。 |
| `openAt` | `startup` | `startup` 会在服务激活完成前打开;`first-search` 把 SQLite 模块与句柄推迟到搜索时再加载和打开`never` 关闭全文搜索(以类型化的 `SESSION_QUERY_SEARCH_DISABLED` 失败),继承的读取保持可用。 |
| `journalMode` | `wal` | `wal``delete``truncate``persist`。 |
| `defaultLimit` | `20` | 请求省略 `limit` 时的分页大小;最多为 `Number.MAX_SAFE_INTEGER - 1`。 |
| `maxLimit` | `100` | 接受的最大请求分页大小;最多为 `Number.MAX_SAFE_INTEGER - 1`。 |

View File

@@ -82,8 +82,8 @@ export const SESSION_QUERY_SQLITE_SNIPPET_CHARS = 240
// One transient source change gets a retry; repeated churn fails rather than monopolizing the queue.
const STABLE_OBSERVATION_ATTEMPTS = 2
/** SQLite module/handle opening phase. */
export type OpenAt = 'startup' | 'first-search'
/** SQLite module/handle opening phase; `never` disables full-text search entirely. */
export type OpenAt = 'startup' | 'first-search' | 'never'
/** Combined session-query configuration backed by SQLite full-text search. */
export interface Config extends SessionQueryConfig {
@@ -93,7 +93,13 @@ export interface Config extends SessionQueryConfig {
* POSIX filesystems; existing modes are preserved.
*/
path: string
/** Open the SQLite module and handle at service activation or the first search. Defaults to `startup`. */
/**
* Open the SQLite module and handle at service activation or the first
* search, or `never` to disable full-text search: the inherited exact
* reads, filters, and traces stay available, while `searchSessions` and
* `searchEvents` fail with `SESSION_QUERY_SEARCH_DISABLED` and SQLite is
* never imported or opened. Defaults to `startup`.
*/
openAt?: OpenAt
/** SQLite journal mode. Defaults to `wal`. */
journalMode?: JournalMode
@@ -192,7 +198,7 @@ export class SqliteSessionQueryEngine extends SessionQueryEngine {
static Config: z<Config> = z.object({
path: z.string().required(),
openAt: z.union(['startup', 'first-search'] as const).default('startup'),
openAt: z.union(['startup', 'first-search', 'never'] as const).default('startup'),
journalMode: z.union(['wal', 'delete', 'truncate', 'persist'] as const).default('wal'),
defaultLimit: z.number().step(1).min(1).max(SQLITE_MAX_PAGE_LIMIT).default(SESSION_QUERY_SQLITE_DEFAULT_LIMIT),
maxLimit: z.number().step(1).min(1).max(SQLITE_MAX_PAGE_LIMIT).default(SESSION_QUERY_SQLITE_MAX_LIMIT),
@@ -251,6 +257,7 @@ export class SqliteSessionQueryEngine extends SessionQueryEngine {
request: SessionSearchRequest,
exec?: SessionSearchExecContext,
): Promise<SessionSearchPage<SessionSearchHit>> {
this._assertSearchEnabled()
const normalized = normalizeSessionRequest(request, this.config)
const signal = exec?.signal
return this._serialized(signal, async () => {
@@ -278,6 +285,7 @@ export class SqliteSessionQueryEngine extends SessionQueryEngine {
request: SessionEventSearchRequest,
exec?: SessionSearchExecContext,
): Promise<SessionEventSearchPage> {
this._assertSearchEnabled()
const normalized = normalizeEventRequest(request, this.config)
const signal = exec?.signal
return this._serialized(signal, async () => {
@@ -310,6 +318,19 @@ export class SqliteSessionQueryEngine extends SessionQueryEngine {
return this._closePromise
}
/**
* Refuse full-text calls under `openAt: 'never'` before any request
* normalization or SQLite work, so a disabled deployment never imports
* node:sqlite, opens the index, or observes sources.
*/
private _assertSearchEnabled(): void {
if (this.config.openAt !== 'never') return
throw new SessionQueryError(
'session search is disabled: this deployment configures the session-query index with openAt "never"',
'SESSION_QUERY_SEARCH_DISABLED',
)
}
private async _close(): Promise<void> {
this._closed = true
await this._tail
@@ -991,7 +1012,7 @@ function resolveConfig(config: Config): ResolvedConfig {
if (typeof resolved.path !== 'string' || resolved.path.trim().length === 0) {
throw invalidConfig('path must not be blank')
}
const openPhases: readonly string[] = ['startup', 'first-search']
const openPhases: readonly string[] = ['startup', 'first-search', 'never']
if (!openPhases.includes(resolved.openAt)) throw invalidConfig('openAt is not supported')
assertPageLimit('defaultLimit', resolved.defaultLimit)
assertPageLimit('maxLimit', resolved.maxLimit)

View File

@@ -231,6 +231,36 @@ describe('SQLite session search', () => {
await expect(stat(path)).rejects.toMatchObject({ code: 'ENOENT' })
})
it('refuses search in never mode while inherited reads and traces keep working', async () => {
const path = await temporaryPath('never-mode.db')
const ctx = new Context()
await ctx.plugin(SessionStore)
const search = await ctx.plugin(SqliteSessionQueryEngine, { path, openAt: 'never' })
const service = ctx.sessionQuery as SqliteSessionQueryEngine
expect(service.config.openAt).toBe('never')
const parent = SessionId('never-parent')
const child = SessionId('never-child')
ctx.sessions.create(parent, { seed: messageEvents('never opened needle'), meta: { createdAt: 10 } })
ctx.sessions.create(child, { meta: { parentSession: parent, createdAt: 20 } })
await expect(service.searchSessions({ query: 'needle' }))
.rejects.toThrow(expectCode('SESSION_QUERY_SEARCH_DISABLED'))
await expect(service.searchEvents({ sessionId: parent, query: 'needle' }))
.rejects.toThrow(expectCode('SESSION_QUERY_SEARCH_DISABLED'))
expect((await service.listSessions()).map(record => record.header.id).sort())
.toEqual([child, parent])
const lineage = await service.traceSession(parent)
expect(lineage.complete).toBe(true)
expect(lineage.descendants.map(node => node.session.header.id)).toEqual([child])
// The disabled index never touches the filesystem, in mount, use, or disposal.
await expect(stat(path)).rejects.toMatchObject({ code: 'ENOENT' })
await search.dispose()
await expect(stat(path)).rejects.toMatchObject({ code: 'ENOENT' })
})
it('opens once on the first search and reuses readiness for later searches', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)

View File

@@ -31,6 +31,7 @@ export type SessionQueryErrorCode =
| 'SESSION_QUERY_INVALID_SURFACE'
| 'SESSION_QUERY_INVALID_WINDOW'
| 'SESSION_QUERY_PERSISTENCE_FAILED'
| 'SESSION_QUERY_SEARCH_DISABLED'
| 'SESSION_QUERY_SESSION_NOT_FOUND'
| 'SESSION_QUERY_STALE_CURSOR'
| 'SESSION_QUERY_SOURCE_CONFLICT'

View File

@@ -71,6 +71,10 @@ const SAFE_SESSION_QUERY_FAILURES = {
code: 'SESSION_QUERY_PERSISTENCE_FAILED',
message: 'session history storage is unavailable',
},
SESSION_QUERY_SEARCH_DISABLED: {
code: 'SESSION_QUERY_SEARCH_DISABLED',
message: 'session search is disabled in this deployment',
},
SESSION_QUERY_SESSION_NOT_FOUND: {
code: 'SESSION_QUERY_SESSION_NOT_FOUND',
message: 'session was not found',