Tyan66666/billion-context-dsh
Tyan66666/billion-context-dsh模型驱动的上下文压缩(Active Context Pruning):由模型决定何时压缩、压缩什么。
为 DeepSeek Harness提供持久化的项目级记忆。专门针对项目开发,原生融合 dsh 任务系统,会话内任务清单与读过的文件自动沉淀为跨会话任务记录,任务↔文件自动关联——开发工作流可切换、可续接,无需重复梳理整个项目。文档与代码符号在读取时索引,无需全量预扫;针对不同语言的项目,支持可选功能增强。经验笔记自动去重,避免重复记录。所有数据召回附带出处可溯源。纯 JS、单依赖、静默运行、零操作成本。
If this plugin saves you an hour of debugging, a star would be appreciated.
English | 简体中文
A persistent project development memory for DeepSeek Harness (dsh) agents. Built specifically for project development, natively integrated with dsh's task system: task lists and files read during a session are automatically persisted as cross-session task records, with tasks ↔ files linked — workflows can be switched and resumed, no need to re-scope the whole project, solving context loss. Documents (PDF/Markdown/txt) and code symbols are stored separately per workspace; documents are automatically cross-linked to the code symbols they mention. Experience notes (problem → solution) are automatically deduplicated, preventing repeated mistakes. All data is stored per project on disk, survives session compaction and handover; recalls include path:line citations for source verification. No native builds, no vector database. A 79 MB local embedding table (leanvec) installs and runs by default; without it, recall falls back to pure lexical.
The plugin keeps a compact project memory on disk, with every entry pointing to a concrete file and line — the agent can reorient quickly instead of re-reading the whole project.
list_tasks → select_task. Work delegated to subagents creates no task. On hosts without session events the task tools still work as a plain record list.typescript is installed, an optional second layer adds inferred return types, generics and interface shapes; it runs in the background and never blocks indexing.leanvec is present — installed by default, absent ⇒ pure lexical. Tuned for CJK: phrase boost, a synonym table, and CJK-aware boundaries for doc↔symbol linking.when triggers, guard narrows it, and prevents records what breaks without the entry. Corpus text can never trigger an injection; the statistical channel is gated separately and silent by default.The plugin relies only on stable public APIs declared through peerDependencies.
cd dsh-project-memory && dsh plugin --profile web add . -wThe -w (workspace-root) flag is required: the profile directory is a pnpm workspace root, and pnpm rejects add there without it. From elsewhere, use the same command with an absolute path.
The plugin is also published on npm as a scoped package:
dsh plugin --profile web add @yolk_vat-y/dsh-project-memory -wEach indexed project has its own store at /.dsh-project-memory/. It keeps itself out of git by default — there is nothing to add to your .gitignore.
The tools below are invoked by the agent, not typed by the user. In the chat, just ask naturally — e.g. "index this project" or "what does the auth module do?" — or simply keep working, and the agent calls the matching tool automatically. By default files are indexed the moment the model reads them, so memory fills in while you work. watch_repo keeps explicitly-watched roots fresh in the background; index_repo forces a full backfill (unchanged files are skipped).
In a git work tree the corpus is whatever git ls-files --cached --others --exclude-standard lists — tracked files plus untracked files that are not ignored — so .gitignore, nested rules included, decides what gets remembered. A directory that is not a git work tree falls back to a plain walk (no ignore rules). Lazy indexing applies the same check per file; an explicit index_doc always indexes what you point it at.
Tool · Purpose index_doc file_path · Index one document (PDF/MD/txt): chunk, summarize deterministically, and store with a path:line citation. Unchanged files are skipped. index_repo root · Index a whole project: documents get deterministic summaries, code files get a token-free symbol table. Incremental and self-healing: the store follows the file listing, so deleted, ignored or moved files are dropped, and documents are cross-linked to symbols. A non-existent or excluded root is rejected before anything is written. watch_repo root · Enable automatic refresh: a background poll detects new and changed files and re-indexes only those. Watched roots persist across restarts; a non-existent or excluded root is refused, roots that disappear are dropped rather than re-created. memory_stats root · Show what the store contains: totals, last index time, and the per-file list sorted by recency. query_memory query · Ranked search over documents, symbols, experience and insights, optionally query-expanded by the LLM. type selects a layer (all / doc / symbol / experience / insight / task). Returns hits with relative scores, sources or insight ids, and doc→symbol references. list_tasks · List task records for the project (archived marked). Call first in a new session before continuing work. select_task · Bind the session to a task so its todo list and file reads sync into it. Exact taskId, or exact title (multiple matches return candidates; no match creates a new task). Pass title with taskId to rename. Auto-unarchives. archive_task · Archive a task (hide from default views, exclude from capacity, stop syncing). select_task restores it. show_task_panel · Show the task panel in the UI. Call when the user asks to see the task list or when you want to display the panel. /tasks (typed by the user, not the model) · The only user command: shows the task stack and drives the workflow card. Every other action is a sub-verb invoked by card buttons: switch / archive / unbind / rename / todos … / step / step-move / step-text for tasks, insight list [scope] [offset] [limit] / confirm / promote / demote / archive / restore / delete / save / edit for memory. remember problem solution · Save an experience note. It is a kind: 'experience' insight — same store, same dedup and same delete path as lessons / decisions / procedures. Similar problems merge or overwrite instead of duplicating. forget id_or_query · Delete memories — lessons / decisions / procedures and experience notes alike, across task / project / global in one call, without knowing which layer the entry lives in. An exact id deletes that one entry; keywords match by query-side coverage (matched query terms ÷ query terms). Real deletion, no undo; when keywords match more than the cap (default 10) it deletes nothing and only lists the candidates. The 5 most recently deleted entries are kept as full entries in that layer's deleted.json next to its store (/.dsh-project-memory/deleted.json for the project layer, beside the global file for global), and the deletion result prints that path — read it and save again to recover a mis-delete. save_lesson · Save a lesson / decision / procedure at task, project or global scope; passing id overwrites that entry in place (only the fields you pass, the previous values stay in prev — write them back with the same id). Before writing it compares against existing memory: cosine ≥ 0.9 auto-overwrites that entry, ≥ 0.7 reports the nearest id in the result; without leanvec it falls back to lexical overlap ≥ 0.7 merge / 0.65–0.7 reinforce. Two tasks hitting the same insight promote it task → project, three or more → global. Key params: id, title, kind, scope, the content (pattern/fix, choice/reason or steps), and trigger (when is the only trigger surface, guard narrows it, prevents records what breaks without it).
/tasks appears in the web / menu inside an icon-bearing Workflow group, with three view entries — Tasks / Project Memory / Global Memory (labelled in the UI language). It is the plugin's only registered command; every other action rides it as a sub-verb driven by card buttons.
The design follows four principles:
path:line citation where applicable, so the agent can confirm details against the source.Building the memory does not require an upfront scan: files are memorized as the model reads them, so the memory grows to cover exactly what has been worked with. Re-reading an unchanged file is a no-op, so the memory stays fresh with minimal ongoing overhead.
The store is per-project and follows the codebase: changed files are re-extracted by content hash, deleted files are removed. Experience notes are kind: 'experience' entries in insights.json — retrieval-only, so accumulation does not affect context.
.dsh-project-memory/
format.json layout marker (v2, sharded)
shards/ one self-describing JSON per indexed source file — writes touch only dirty shards
watch.json watched roots
tasks.json TaskBridge task entities (cross-session)
binding.json current session ↔ task binding
insights.json project-scope memory (lessons / decisions / procedures / experience notes) — one entity
vectors.json optional semantic index (only when the vector layer runs)
injection-audit.jsonl one line per real injection (what / why / dropped / budget)
admission-shadow.jsonl one line per step, all scored candidates + features (offline replay)references, so a doc indexed before its symbols still links correctly.query_memory asks the LLM to rewrite the query into several variants (synonyms, EN/CN, identifier guesses) and merges the scores; when off, queries never touch the LLM. Indexing itself is model-free.forget. Writes take a cross-process directory lock (.write.lock) around a short read-modify-write, so two dsh instances can share one project store: shards converge by indexedAt, and a save that cannot get the lock is deferred and retried.TaskPanel (Container)
├── task-data-store (server data, cross-tab sync via BroadcastChannel)
├── task-ui-store (local UI state, localStorage)
├── task-hooks (useTaskDrag, useTaskEdit)
└── TaskComponents (MiniBar, TaskCard — presentational only)The workflow panel is collapsible, adapts to dsh and theme plugin styles, and offers four card styles.
Key · Default · Meaning memoryDir · .dsh-project-memory · store directory inside each indexed root chunkChars / maxChunksPerFile · 3000 / 40 · max chars per document chunk, max chunks per document maxFileSizeMb · 10 · skip documents (incl. PDF) and code files larger than this (MB); see the per-file memory budget below maxPdfPages · 1000 · PDF page cap maxOutputChars · 8000 · cap for query_memory result text (chars) lazyIndexing · true · index files the moment the model reads them autoIndexOnFirstUse · false · full scan of the current working directory on plugin load (opt-in) watch / watchInterval · true / 30 · background refresh; base poll interval in seconds (polls stay quiet while a turn is running — a 5-minute fallback keeps external edits fresh — and one merged scan runs at turn end; idle polls back off up to 2 minutes and reset on any change) maxScanFiles / maxScanDepth · 20000 / 12 · per-scan caps on files and directory depth; a truncated pass is reported and never removes what it did not reach. Cannot be disabled: 0 or any invalid value falls back to the default allowUnsafeRoots · false · compatibility switch that releases the built-in excluded list for every root decision (including lazy indexing, session audit and TaskBridge). It only applies to the built-in list — roots.deny / roots.exclude still win; to release a single directory, prefer an exact roots.allow entry roots.allow / roots.deny / roots.exclude · [] / [] / [] · custom root lists (machine/profile level, CLI and web alike). allow / deny match the directory itself exactly; exclude is a path-boundary prefix and covers the whole subtree (no file under it enters the index). Priority: exclude > deny > allow > built-in list; the filesystem root can never be a root. ~ / ~/... expansion, non-absolute entries are ignored with a one-time stderr warning, Windows is case-insensitive llmQueryExpansion / expansionCount · false / 6 · expand queries via the LLM before search (off by default to save tokens); max variants tsPath / enableTypeScript · (auto) / true · optional absolute path to a specific typescript install; set enableTypeScript: false to disable the type-aware layer entirely tasklist.enabled / tasklist.syncHostOnAdopt · true / true · TaskBridge auto-sync (task entities from the session todo list and file reads); push a bound task's steps to the host task list so dsh mirrors it insight.* · dedupOverlap 0.7 · reinforceBand 0.65 · autoMergeCos 0.9 · autoMergeMargin 0.05 · hintCos 0.7 · maxForgetMatches 10 · maxProject 100 · maxGlobalProcedures 200 · promoteConfidence 0.7 · globalPromoteTasks 3 · decayDays 90 · globalFile (auto) · insight dedupe / auto-overwrite / near-neighbour hint / delete cap / reinforce / promotion / capacity / archive settings. autoMergeCos / hintCos = 0 turns that band off reflection.enabled · false · LLM reflection, draft-only at task level (fires on task switch-away / archive). cooldownMs 1800000, maxLessonsPerReflect 3, maxDecisionsPerReflect 2 autoContext.enabled · true · silent injection wrapper (resident task card + gated items); inert until the host exposes a resolvable session cwd. maxTokens 400 (per step), editedMax 3 (recently written files shown on the task card), signalMinRatio 0.5 (a hint must reach half of its layer's top score), skipEchoSelfTodo true, budgetLog off, reinjectItemsAfter 0 (steps before the same insight may be injected again), rootNotice true autoContext.gateCooldownSteps · 2 · minimum number of pre-steps between two *item* injections (the resident task card is exempt — it is a state snapshot). This is the main "don't inject often" dial autoContext.maxItemsPerSession / autoContext.maxItemCharsPerSession · 60 / 24000 · runaway fuse, not a throttle — a hard per-session ceiling by count and characters; once reached, the item channel stays silent for the rest of the session autoContext.hintMinCoverage · 0.45 · absolute floor for the statistical channel: the IDF-weighted share of the query's information mass the entry covers. A ratio-only threshold cannot tell signal from "best of a bad lot" autoContext.hintMinMatched / autoContext.hintMinSupport · 2 / 0.15 · a hint must share at least this many terms with the query, and at least this share of the query's terms must exist in the corpus at all — otherwise the channel stays silent this round autoContext.vectorDir · unset · optional semantic layer over the experience/insight layers, on by default via the leanvec optionalDependency (79 MB, ≈+130 MB RSS). off forces pure lexical; a path or DSH_LEANVEC_DIR overrides it. Re-orders only, returned counts unchanged; while it is active only entries that got no cosine are still filtered by lexical match count autoContext.vectorAlpha · 0.5 · weight of (1-α)·lexical + α·cosine in those layers; 0 disables re-ranking only (write-side dedup stays on), 1 (pure vector) measured worse autoContext.vectorDedup · true · write-side near-duplicate detection (cosine ≥ 0.9 auto-overwrite, ≥ 0.7 near-neighbour hint). Independent of vectorAlpha; false keeps recall ranking untouched autoContext.vectorItemMin / autoContext.vectorSpeakMin · unset · experimental absolute-cosine gates for the hint channel (per-entry floor / channel-level semantic rescue). Uncalibrated — unset means off; see the evidence in src/auto-inject.js before enabling autoContext.legacyScope · ignore · how a legacy trigger.scope is treated: ignore (default) does not use it as a filter; filter applies it as a filter. npm run selfcheck:triggers lists entries that still carry a scope autoContext.auditLog · true · append one JSONL line per actual injection to injection-audit.jsonl (what / why / dropped / budget); rotates to .1 past auditMaxBytes (262144). Silent on I/O errors autoContext.shadowLog / autoContext.shadowMaxBytes · true / 524288 · append one JSONL line per step (including steps that injected nothing) to admission-shadow.jsonl: scored candidates with their features and the gate that rejected each one — what makes a threshold change answerable offline. Disk only, never in the prompt; rotates keeping one .1 generation
The two most relevant switches are lazyIndexing (index a file the moment the model reads it; default on) and autoIndexOnFirstUse (full scan of the current working directory on plugin load; default off). Lazily indexed project roots are automatically registered with the watcher, so changed files stay fresh without an explicit watch_repo.
Root resolution. In order: an explicit root argument; a registered root (watch_repo); the nearest ancestor with a VCS marker (.git/.hg/.svn) or a build/manifest marker (package.json, go.mod, Cargo.toml, pyproject.toml, …); the session working directory, unless it is on the excluded list. A file that matches none of these is not indexed.
When the root comes from the working directory, the model gets one notice per session naming it and how to change it; mute with autoContext.rootNotice: false. Starting dsh in a container directory such as ~/workspace therefore makes that directory the root, and memory spans everything beneath it up to the scan limit — start dsh inside the project for one store per project.
Excluded directories. By default, these are not used as a root, matched exactly (subdirectories are unaffected): the filesystem root, the home directory, temp directories — os.tmpdir() and the shared ones (/tmp, /var/tmp, %TEMP%, %SystemRoot%\Temp) — and system / package-manager prefixes (/opt/homebrew on POSIX; %SystemRoot%, %ProgramFiles%, %ProgramData% on Windows). A session in one of these runs without memory, with one line on stderr.
Custom root lists. The roots block overrides that decision. All three accept ~ / ~/...; non-absolute entries are ignored with a one-time stderr warning (relative paths would resolve against the host cwd and are unpredictable); Windows is case-insensitive:
roots.allow — allow a directory even when it is on the built-in list, matched exactly. The filesystem root is not eligible.roots.deny — this directory cannot be a root, matched exactly; its subdirectories can still be roots of their own.roots.exclude — no file in this subtree enters the index, matched by path-boundary prefix (subdirectories included); no new root is inferred inside it either. A complete scan of a parent ro