Back to home@fan56

dsh-topics-memory

dsh-plugin: OKF v0.2 topic memory for dsh - local-first git-tracked bundle, hot-path LLM-free injection, two-stage observer with background distill

Stars
0
Language
TypeScript
Created
Aug 31, 2026
Updated
Sep 3, 2026
GitHub repo

Introduction

dsh-topics-memory

English | 中文

A dsh plugin: maintains "working topic memory" as an OKF (Open Knowledge Format v0.2) knowledge bundle, persisted in a local git repository (optionally synced to a private GitHub repo), with conclusions traceable through git history, sessions automatically observed and distilled into knowledge, and relevant topics injected to the model before every turn.

Requires dsh >= 0.1.2-alpha.4 (adapted to the dsh 0.1.2-alpha.4 settings namespace and LLM seam; the rc line is no longer supported).

The problem it solves

Long sessions forget. Cross-session, even more so. This plugin maintains structured topic memory: each Topic records a matter's name, dependencies, open questions, current conclusion, impact, and recommendations. When a conclusion changes, edit the file and commit — git log directly answers "when, by whom, and why did this conclusion change".

Core features

  • Strict OKF v0.2 compliance: each Topic is a markdown + YAML frontmatter concept document (type: Topic) that the whole OKF ecosystem (Obsidian, OKF validators) can consume directly; ships with the provenance (sources), trust (generated/verified), and lifecycle (status/stale_after) field families.
  • Git-traceable: one conclusion change = one commit (write-through); the topic_history tool and /topics history make change history first-class.
  • Local-first: local-only mode by default (~/.dsh/topics/), zero config, zero credentials; setting repo enables GitHub sync (single repo, single bundle, single main, write-through + debounced push; rebase conflicts are demoted and flagged for a human — no automatic smart-merge).
  • LLM-free hot-path injection: per-turn lexical matching (CJK bigrams + words + weighted tags + depends graph walk), millisecond-scale; zero matches = zero injection; per-topic digest ≤300 tokens, top-K ≤4, total budget ≤1.5k tokens — all configurable.
  • Observable, tunable injection: every turn writes an Injection Log (hits, scores, near-misses, budget usage); /topics stats reports hit rate, top-N, near-miss distribution, and tuning suggestions — tune from evidence, not vibes.
  • Knowledge as a graph: depends (machine-readable directed edges) plus body [[wikilinks]] and markdown links (human-written edges) form one graph; retrieval walks it in both directions (per-level decay, configurable depth) so a single hit pulls in a knowledge subgraph; every write rebuilds the meta/backlinks.json reverse index, and /topics show lists "who references me, and how" — check the blast radius before changing a conclusion.
  • Two-stage observer (M2): the main model jots atomic observations with topic_observe; a background distill lane (session end + every N turns, model configurable) distills them into formal Topics in batches; when the model itself deems something worth keeping, it topic_saves directly.

Quick start

  1. Install (command below), restart dsh;
  2. Run /topics onboard — native dsh ask-user panels walk you through the five decisions: mode / repo / distill model / injection tier / auto-observe — nothing is written until the final confirm;
  3. Work as usual: relevant conclusions are injected every turn; say "remember…" to have the model topic_save; /topics status for health, /topics stats for injection stats.

Tools & commands

Model toolsPurpose
topic_saveDistill/revise a Topic (name / dependencies / open questions / conclusion / impact / recommendations)
topic_observeJot an atomic observation (decision/finding/constraint/question), pending distill
topic_searchLLM-free keyword search over memory
topic_historyA topic's conclusion change history (git log as a tool)
CommandPurpose
/topics onboardInteractive setup wizard on dsh-native ask-user panels (mode / repo / distill model / injection tier / auto-observe); typed fallback where no ask-user UI exists
/topics statusBundle health: topic count, observation backlog, conflicts, last distill outcome, sync status
/topics distillManually trigger one distill run over the current observation pool (same lane, same in-flight guard; summary mirrors the distill-state fields)
/topics statsInjection stats: hit rate, top-N, near-miss distribution, tuning advice
/topics list / show / historyBrowse topics, backlinks, and change history
/topics graphGenerate a relationship-graph web page (force-directed, draggable/zoomable, hover for conclusions) and open it in the browser
/topics sync [pull|push]GitHub mode: manual pull/push (automatic by default)
/topics config / set <key> <value>View and edit config (thresholds, budgets, distill model, …)

Install

dsh plugin --profile <your profile> add @aiwayds/dsh-topics-memory

First thing after installing: run /topics onboard. The bundle lives at ~/.dsh/topics/ by default ($DSH_TOPICS_HOME overrides). GitHub sync: /topics set repo <owner/name> (suggested repo name dsh-topics-data, to keep it distinct from the plugin's own source repo); credentials come from $GITHUB_TOKEN or a logged-in gh CLI (login is not this plugin's job).

Upgrading from 0.5.x (rename)

0.6.0 renames the plugin: @aiwayds/dsh-llmwiki-memory@aiwayds/dsh-topics-memory, the /wiki command family → /topics, and the settings namespace llmwikitopics. Install the new package (and remove the old one from your profile) — on first start the plugin migrates everything automatically: the data directory ~/.dsh/llmwiki is renamed to ~/.dsh/topics, and user-tuned values in the old llmwiki settings namespace are copied into topics. No manual steps; if a migration step fails the plugin falls back to the old locations and keeps working.

Configuration

First-time setup belongs to /topics onboard; day-to-day tuning is /topics set <key> <value> (writes the topics namespace in settings.yaml, effective from the next session). All keys and defaults:

KeyDefaultMeaning
repoempty (local-only)GitHub sync repo owner/name; suggested dsh-topics-data; empty = back to local-only
autoInjecttruePer-turn injection master switch
injectDeduptrueSession-level injection dedup: topics already injected in this session are not re-injected (registry cleared at session end; budget-dropped topics stay injectable; deduped topK slots are NOT backfilled) — ADR 0012
topK4Max topics injected per turn
perTopicBudget300Per-topic digest token budget
totalBudget1500Total injection budget per turn
matchThreshold0.3Hit threshold; tune from /topics stats near-miss evidence
tagBoost0.15Additive boost per tag hit
graphDepth2depends graph walk depth (0 disables)
recencyWindowDays7Recency bonus window (+0.2)
autoObservetrueCapture atomic observations every turn
includeSubagentstrueWhether injection and observation also engage subagent sessions (ADR 0011); off skips them entirely
observationMaxChars2000Per-side per-turn observation truncation
distillProvider / distillModelempty (distill off)Distill lane model route; both must be set to enable. With a UI, /topics set distill-provider / distill-model without a value opens a picker panel (provider list → that provider's model catalog); a mixed provider model / provider/model value for distill-model splits into both keys
distillEveryTurns5Distill every N turns of a long session
distillOnSessionEndtrueDistill once when a session ends
distillBatchSize40Observations per distill model call. On an output-limit (max-tokens) failure the batch halves automatically (floor 5) and retries — a failing batch can no longer livelock the backlog; the shrink persists until reload or a config change. Note: /topics set distillBatchSize back to the same value does not reset the shrink — set a different value or reload the plugin
distillMaxModelCalls8Max model calls per distill run, including the one corrective retry for ops echoing no valid observed_ids (the run stalls when the budget can't fit it). Batches already distilled keep their marks when the budget stops the run (partial progress), recorded as partial: … in the distill state
pushDebounceSeconds45GitHub-mode debounced push interval

Acknowledgements

This project's shape is directly inspired and supported by:

  • zosmaai/pi-llm-wiki — a native OKF v0.2 knowledge extension for pi and this project's direct inspiration; its two-stage observation (cheap atomic observations + background distill), cache-safe injection (volatile content never enters the system prompt), and layered vault & ownership model are all absorbed here.
  • GoogleCloudPlatform/open-knowledge-format — the Open Knowledge Format (OKF) v0.2 spec this bundle format strictly follows.
  • Karpathy's LLM Wiki pattern — the starting point of the whole "an LLM maintains a personal knowledge base" methodology.
  • fan56/pi-topic-memory — the same author's predecessor: a working topic ledger with silent injection for pi; its LLM-free hot-path matching and injection-timing experience is this project's direct technical ancestor.
  • chancelu/dsh-llmwiki — a fellow dsh-ecosystem precedent; this project's same-turn injection seam (agent/inbox/spliced + systemPrompt.context()) follows the mechanism it validated on real dsh.

Known boundaries

  • Subagents engage memory by default — one switch to opt out: by default (include-subagents on), injection and observation apply to subagent sessions too. /topics set include-subagents off skips delegated sessions entirely — no injection, no observation, no distill triggers; the topic tools stay on the global layer, so an explicit topic_save from a child still lands. Out-of-process subagents (claude-code/codex providers) never load this plugin anyway.
  • Session-end distill in headless one-shot sessions: the plugin's disposer now waits (bounded, up to 90s) for the exit-triggered distill to land before unload completes, so a headless process no longer loses that race by default; a hanging model call gives up at the cap and exit proceeds without that run. Within that 90s window the exit run may complete only part of its call budget — the remaining backlog waits for the next trigger. The exit trigger is skipped entirely while a session-end run is still in flight (the same pool head would otherwise be fed to the model twice). Manual /topics distill runs inside a live session bypass the race entirely; meta/distill-state.json records each lane's outcome, checkable via /topics status.
  • Observation GC (three strikes): an observation the model actually evaluated (parseable answer, however useless) but no op consumed accrues one failed attempt; the third failed attempt physically deletes it — explicitly authorized cleanup of raw data the lane demonstrably cannot process. Runs that never evaluated the batch never count: infrastructure failures (network errors, unconfigured distill route → readable no-model short-circuit) and unparseable output (invalid-output) are exempt, and a batch still mid-shrink on output-limit retries is only counted once a verdict is reached (success, floor stop, stall, or an explicit skip). Deletions are committed immediately (data destruction stays git-traceable); pure attempt counters follow the usual flush cadence.
  • Config read timing: /topics set and settings.yaml edits take effect most reliably from the next session start.
  • Picking a distill model: /topics onboard splits the distill decision into two dependent questions (provider first, then that provider's model catalog), pre-validated with resolveModelInfo — a provider with no live route blocks and re-asks, an off-catalog model (a non-NO_ADAPTER failure: outside the advisory catalog, possibly still usable) warns but is allowed; hosts without an ask UI or a usable model route fall back to typed input. The same validation backs the /topics set picker panels.

Design docs

  • CONTEXT.md — domain glossary
  • docs/adr/ — 0001–0013: OKF compliance, remote shape, sync strategy, two-stage observer, bundle layout, injection defaults, observability & tunables, dual-mode persistence, onboarding wizard, subagent isolation, the include-subagents switch, injection dedup default-on, the rename & migration

License

MIT