跳过主要内容
K

dsh-session-recall

kittimzhe/dsh-session-recall

Deterministic cross-session transcript retrieval for DeepSeek Harness: the model-facing `recall` tool searches past session logs with explicit scope control

安装

dsh plugin --profile web add github:kittimzhe/dsh-session-recall

README

dsh-session-recall

English | 中文

CI npm version npm downloads License: MIT

Deterministic cross-session full-text retrieval for DeepSeek Harness: the model-facing recall tool lets the agent search its own past session transcripts — "that bug we fixed last week", "the font we chose for my resume" — through the trusted ctx.sessionQuery seam.

Quick Start

Requirements: Node.js 20 or 22 · a DeepSeek Harness profile that mounts the commands and sessionQuery services (the shipped web / agent profiles qualify).

dsh plugin --profile web add dsh-session-recall

The first search builds a persistent FTS5 index automatically — no extra setup. In a session, just ask naturally:

recall: which font did we pick for the resume last week?

Full details — GitHub install route, cordis.patch.yml snippet, configuration — in Install below.

Positioning

dsh-session-recall is a transcript retrieval layer focused on correctness and control.

  • It returns evidence from original session logs, not synthesized summaries.
  • It enforces explicit retrieval scope (cwd by default, opt-in widening).
  • It favors deterministic behavior over "smart" but lossy memory extraction.

If you need agent memory orchestration, use a memory framework; if you need bounded, auditable lookup over historical transcripts, use this plugin.

Competitive context

Capability focusMemory frameworksGeneric transcript searchdsh-session-recall
Retrieval targetDerived memory objectsVaries by implementationOriginal session transcript events
Scope controlFramework-specificOften coarsecwd-scoped default + explicit all_projects, since_days, tools, errors_only gate
CJK behaviorFramework-specificOften tokenizer-limitedFTS + CJK zero-hit substring fallback
Output contractUsually framework-nativeVariesTyped recall result with stable fields/hints

Name & scope notes (2026-09):

  • This plugin is unrelated to dsh-recall-plugin — that plugin is message undo/rewind (restoring workspace and conversation to before a message was sent).
  • It succeeds dsh-recall — an earlier transcript-search plugin (last release 2026-08-21) with a similar goal; this plugin continues the line with persistent FTS5 indexing, CJK fallback, approval gates, and lineage-scoped authorization.
  • It complements memory frameworks such as dsh-mnemon (write-side memory orchestration): this plugin stays a read-only retrieval layer over original session logs and makes no writes to any memory store.

Roadmap

  • P2: evidence handoff — one-click bridge to session export for matched sessions.

Why

The official @deepseek-ai/dsh-session-query README lists exactly what is missing:

No registries or model-facing tool — … a model-facing tool is absent. No caller authorization — … a model tool or UI must constrain which sessions its caller may inspect.

And the shipped web profile mounts its SQLite FTS5 backend with openAt: never and an in-memory database — so cross-session full-text search is off by default, and even when enabled the index dies with the process.

shipped web profile+ this plugin
Model-facing search toolabsentrecall
FTS indexopenAt: never (off)on, lazy (first-search)
Index storage:memory: (lost on restart)persistent <DSH_HOME>/session-recall/index.db
Caller authorizationcaller's responsibilitycwd-scoped by default, explicit opt-out

Memory plugins extract structured notes with an LLM (lossy, costs tokens); recall searches the original transcripts — zero extraction, zero loss, works retroactively on day one.

What the model gets

recall({ query })                        → best-matching event per session, current project only
recall({ query, all_projects: true })    → search every session on the machine
recall({ query, session_id })            → search the events of one session
recall({ query, limit, cursor })         → page through results

Each hit carries the session id, title (best-effort), date, and a match snippet; the result renders as a native search card in the Web UI (SearchMatchesResultView). Because the FTS unicode61 tokenizer indexes an uninterrupted CJK run as a single token, a short Chinese phrase inside a longer sentence would otherwise never match the index — so a zero-hit CJK query automatically falls back to a substring scan over session text (the sessionQuery.filterEvents literal text clause). Every whitespace-separated term must match, so 简历 模板 still recovers 简历模板; the hint reports when that path matched.

Scoping (the authorization gap)

sessionQuery is trusted infrastructure — it can read every session. This tool therefore constrains each call itself:

  • by default, sessionFilters: [{ kind: 'cwd', values: [<calling agent's cwd>] }] — only sessions started in the same project directory;
  • all_projects: true widens the scope, and only if the deployment allows it (allowAllProjects: false disables the argument).

Install (out-of-tree plugin)

From npm:

dsh plugin --profile web add dsh-session-recall

Or from GitHub:

dsh plugin --profile web add github:kittimzhe/dsh-session-recall

Then add to the profile's cordis.patch.yml:

- insert:
    - id: session-recall
      name: 'dsh-session-recall'

The bundle's own patch layer turns the persistent index on (session-query-sqliteopenAt: first-search, path: <DSH_HOME>/session-recall/index.db); if you maintain your own override of that row, keep those two values.

Configuration

Plugin row config (all optional):

- id: session-recall
  name: 'dsh-session-recall'
  config:
    allowAllProjects: true  # honor the tool's all_projects argument
    defaultLimit: 5         # page size when the model omits limit (1..10)
    maxLimit: 10            # largest accepted page size (1..25)
    cjkHint: true           # explain CJK zero-hit results
    cjkFallback: true       # CJK zero-hit → exact substring scan over session text
    cjkFallbackScanMax: 50  # max sessions scanned per cross-session fallback (1..500)

Failure behavior

Every failure returns a friendly hint instead of a raw exception: a disabled index explains the two config keys needed, a stale cursor tells the model to restart without one, an unknown session_id suggests discovering sessions first. Title enrichment is best-effort — a failed title batch degrades to untitled rows, never a failed search.

Scope policy & redaction (v0.4)

Deployment-level controls for what the model may read back:

OptionValuesDefaultEffect
redactionModeoff / mask / hashoffRedact secret-looking text (bearer headers, prefixed API keys, private-key blocks, emails) in snippets and titles. hash keeps secrets comparable (#xxxxxxxx, same secret → same marker) without being readable. Results carry a redacted count.
cwdAllowlistlist of paths(none)Only sessions started in these directories are searchable; the calling cwd itself must be listed.
cwdDenylistlist of paths(none)These directories are never searchable. Deny wins over allow.
recencyHalfLifeDaysdays (e.g. 30)(off)Re-rank cross-session hits: backend rank × exponential recency decay over the match time. Unset or <= 0 keeps backend order. Per result page.
pinnedCwdslist of paths(none)Sessions from these project directories rank first, as a group.
allProjectsPolicyallow / deny / confirmallowdeny ignores all_projects with a model-facing hint; confirm asks the user through the official @deepseek-ai/dsh-user-approval seam — fail-closed when no answerer is composed.

All three gates apply uniformly to cross-session hits, the CJK fallback scan, and session_id reads — no bypass route.

Every result also carries a diagnostics object (v0.5): which engine produced the matches (fts / cjk-fallback / session-scan), how many sessions a fallback scan visited against its budget, and whether re-ranking was applied — so callers can tell why they got what they got.

Known limitations

  • First search after startup walks the durable logs to build the index (the tool description warns the model); subsequent searches are incremental.
  • unicode61 matches whole tokens/phrases, not substrings — AI does not match BRAID. CJK queries that get zero full-text hits fall back to a substring scan (filterEvents) whose whitespace-separated terms are ANDed, so 简历 模板 also recovers 简历模板; the hint reports when that path matched.
  • One process must own the index file (single-writer SQLite, per the official backend).
  • By default matches return transcript text verbatim — redaction is opt-in since v0.4 (redactionMode: 'mask' | 'hash'; see "Scope policy & redaction"). With redactionMode: 'off' (the default), a token or sensitive path pasted into an earlier session can still be surfaced by a matching search; default cwd scoping and allowAllProjects: false remain the containment baseline.

Benchmark

Measured on a real headless profile (Node 25, Apple Silicon, warm filesystem cache).

Corpus
Sessions31
Events in the durable logs187,706 (~104 MB uncompressed, 49.7 MB zstd)
Indexed text events8,187
FTS index on disk15 MB
QueryHitsWarm latency (FTS5 MATCH)
EN font100 (capped)1.1 ms
EN resume template460.6 ms
CN 字体290.3 ms
CN 简历 模板100.1 ms

Warm searches run sub-millisecond to ~1.5 ms against the on-disk index. Cold start: scanning the 49.7 MB of session logs takes ~4.7 s (decompress + line scan) and inserting the 8,187 text events into a fresh FTS5 table takes ~190 ms; the first recall in a fresh profile completes within the tool's 10 s timeout. After that, restarts reuse the persisted index with incremental reconciliation.

Development

npm install
npm run typecheck   # tsc --noEmit
npm test            # vitest run
npm run bundle      # tsdown → lib/

License

MIT

Community

相关插件