DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-topics-memory

f

dsh-topics-memory

Manifest valid★ 4

Topic memory for LLM agents — edited, not accumulated: a topic keeps the starting question, conclusion, impact and dependencies; process is not memory. OKF bundle for dsh, local-first, git-traceable,

hasBundlePatch

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.7-rc.1 — this plugin targets the dsh RC/stable line only (CI and releases resolve the newest of the latest/next dist-tags at runtime). The alpha line is no longer supported.

Demo

The full flow in about four minutes: topics captured, distilled, and injected in a real session.

https://github.com/user-attachments/assets/8c06cc98-b1ed-402b-9110-4f9a93eb15bc

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".

Why this plugin exists: memory is edited, not accumulated

The short version: more memory is not better memory.

Most memory tools assume accumulation — record everything, retrieve broadly. That may work for humans; for LLMs it backfires twice over. Model attention is a finite resource, so a giant memory bank means every turn is spent digging for signal in noise. Worse, process memories hoard intermediate judgments that were right once and wrong later — and they will confidently steer the model into bad decisions.

So this plugin takes a hard editorial line on what deserves to be remembered: a topic records exactly four things — the question that started it, the conclusion it reached, what it impacts, and what it depends on. Everything in between — the discussion, the dead ends, the wrong turns — is deliberately not memory. Process belongs to the session; when the session ends, it goes. Only conclusions that survive distillation make it into the bundle.

Short-term memory is the session's own job — the conversation context already is one, and a plugin that feeds it back is noise. Long-term memory belongs to topics: small, structured, git-traceable, injected in budgeted slices with zero hits meaning zero injection. Every turn hands the model the minimum high-value context, not the biggest warehouse.

This plugin is not trying to be the model's notebook. It is trying to be the model's editor: deciding what is worth keeping — and, more importantly, what should be forgotten.

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.

Jev decision layer (System One, experimental, default off)

An optional decision layer on the asynchronous lanes, powered by a System One typed-decision model (jev): it batch-scores slow-lane rerank candidates (replacing the LLM rerank), pre-filters consolidation merge pairs before the LLM gardener sees them, and shadow-audits the fast-lane lexical gate from a turn-end lane. Every verdict is probability-gated, and any failure — timeout, network error, bad answer — falls back to today's pure-local behavior (hardcoded fail-open, no switch key). The injection hot path never makes a remote call (ADR 0016–0019; design doc: docs/design/2026-09-25-system-one-integration.md). Off by default: jevEnabled: false means zero behavior change.

Keys

KeyDefaultMeaning
jevEnabledfalseMaster switch (rollout: default off → shadow → opt-in per profile → default-on observation); false stops the stats too
jevBackendzenzen (free) / native / openrouter
jevModelempty sentinelResolved per backend at call time: jev-1.13-free (zen) / jev-1.13.0 (native) / typesafe/jev-1.13 (openrouter) — pinned, never an alias
jevTimeoutMs3000Per-request timeout; single AbortSignal.timeout, no retry (the lane's next cadence retries naturally)
jevSecretFilenonePath to an external secret list; loaded once at boot, re-read when the path is hot-changed

Keys provisioning

BackendKey source
zen (default, free)JEV_ZEN_API_KEY env, or the macOS keychain service opencode-zen-inference (default fallback — zero config)
nativeTYPESAFE_API_KEY
openrouterOPENROUTER_API_KEY; the keychain service openrouter-inference requires an explicit JEV_KEYCHAIN

dsh scrubs ambient KEY|PASSWORD|SECRET|TOKEN variables from plugin environments, so a key exported in your shell never reaches the plugin — pass it explicitly through the profile patch env: block, or keep it out of files entirely via the keychain (JEV_KEYCHAIN / zen's default service; neither matches the scrub rules):

# ~/.dsh/cordis.patch.yml — merge into your profile patch. The !!js
# expression keeps the key out of the file (same convention as dsh-jev-mcp).
- insert:
    - id: dsh-topics-memory
      name: '@aiwayds/dsh-topics-memory'
      env:
        JEV_ZEN_API_KEY: !!js process.env.JEV_ZEN_API_KEY ?? ''
        # native:     TYPESAFE_API_KEY: !!js process.env.TYPESAFE_API_KEY ?? ''
        # openrouter: OPENROUTER_API_KEY: !!js process.env.OPENROUTER_API_KEY ?? ''
        #   plus JEV_KEYCHAIN: 'openrouter-inference'
        #   (zen needs no JEV_KEYCHAIN — with no env key it reads the
        #   default keychain service 'opencode-zen-inference')

Thresholds

Adopt / record / fallback bands (calibrated offline on a 383-case gold corpus; the numbers are bound to the batch protocol — design doc §5). Record-band verdicts only land in the decision log without affecting behavior. The cluster-level "worth consolidating?" question records without gating in v1:

SeamAdoptRecord-onlyFallback line
Slow-lane rerank (per candidate)noul ≥ 0.60 → enters picks0.10 – 0.60< 0.10 → strong veto, lexical-gate behavior
Consolidation merge (per pair)noul ≥ 0.50 → pair sent to the LLM0.15 – 0.50 (pair still evaluated by the LLM)< 0.15 → pair cut before the LLM

Fallback behavior

SeamOn failure / timeout / bad answerLegacy path
Slow-lane rerankThe round falls back to the old LLM rerank; picks still producedOld RERANK_PROMPT path kept until rollout step 4 (default-on observation), then removed
Consolidation prefilterThe cluster goes to the LLM in full, exactly as todaynone
Fast-lane shadowRecords the outcome only; zero behavior impactnone

Setting up laya: the bundled skill dsh-topics-memory-laya walks through install (venv, mirror acceleration for CN networks, checkpoint download), starting laya-serve, wiring jevLayaFallback, and verifying via decisions.jsonl. Ask the agent about laya, or read skills/dsh-topics-memory-laya/SKILL.md directly.

Local laya pace-maker (experimental, off by default). With jevLayaFallback: true and a local laya serve running, every jev call fires a parallel local laya request: the primary answer drives the decision, and the laya answer is logged alongside it — a permanently running laya-vs-jev comparison on identical questions. laya is TELEMETRY ONLY: on real queries its within-batch ranking agreed with the primary 0/14 and its negative scores sit at 0.63-0.76 (absolute scores unusable), so a primary failure falls open to the legacy path exactly as without the pace-maker — the comparison rows are the value, not a degraded takeover. The consolidation prefilter deliberately does not ride the pace-maker. laya not running costs a refused connection in milliseconds.

Every call and verdict lands in ~/.dsh/topics/meta/decisions.jsonl — local-only and redacted: slugs, pair hashes, question types, probabilities, latency and token counts; never conversation text or conclusion bodies. It has no config key (it stops together with jevEnabled: false); /topics status shows a 30-day summary line.

Latency benchmarks (measured 2026-09-26, Apple M5, typesafe native backend, jev-1.13.0)

Real-payload benchmarks and in-sandbox runs — use them to pick jevTimeoutMs. Sources vary in shape and load; all are the same 8-candidate shadow batch unless noted:

SourceShapep50p90maxn
Idle benchmark (sequential)8-question batch742 ms1290 ms1461 ms10
Idle benchmark (sequential)1-question367 ms—925 ms5
Threshold-sweep runs (Sept 25)20-question batch1760 ms avg—8470 ms16
Live headless turns8-question batch, concurrent with the main model streaming327–5698 ms—10619 ms3

Readings that matter: the idle path sits comfortably under the 3000 ms default (≈2× headroom at p90), but a live turn's shadow call shares the network with the main model's streaming response — the 10.6 s outlier above was observed exactly there. A timeout is fail-open: the batch is dropped (slow-lane rerank falls back to the old LLM path), so a tight timeout costs shadow data, never correctness. Keep the 3000 ms default unless decisions.jsonl shows a persistent timeout share above ~5% (the 30-day summary in /topics status surfaces it); weak-network users can raise jevTimeoutMs freely. zen and openrouter are unmeasured here — after enabling, your own decisions.jsonl latency column is the ground truth for your network.

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_saveSave/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 consolidateManually trigger one consolidation run: the LLM gardener merges duplicates, promotes settled drafts, deprecates superseded entries, refreshes metadata — every change is its own git commit, revert to roll back
/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 llmwiki → topics. 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.

Uninstall

Remove the plugin from a profile:

dsh plugin --profile <name> remove @aiwayds/dsh-topics-memory

The host reconciles the profile automatically: the dsh.profile.bundles entry is spliced and the patch layer is dropped.

What stays on disk (kept on purpose — this is your memory):

  • ~/.dsh/topics/ — the whole topic bundle: topic markdown, meta/, and the embedded .git repo (the full history; it may carry an origin remote — GitHub sync stops with the plugin). To archive the bundle elsewhere, copy or clone this directory as-is.
  • The settings-page dsh-topics-memory entry (profile patch) — user overrides. Reinstalling silently reactivates sync including any configured repo; clear the entry first if you want a clean start.
  • A legacy llmwiki: section left inside the old settings.yaml (renamed settings.yaml.imported after the host's one-shot 0.1.7 import) is never touched again; mine it by hand if something still needs it.

Purge everything: back up ~/.dsh/topics first, then rm -rf ~/.dsh/topics.

Configuration

First-time setup belongs to /topics onboard; day-to-day tuning is /topics set <key> <value> — on dsh 0.1.7+ it writes the settings-page dsh-topics-memory entry (the profile patch) and every key is volatile: edits take effect immediately, no restart. Upgrading from a pre-0.1.7 install: a legacy top-level topics: section in the old settings.yaml is imported once into the new entry automatically at the next plugin boot (the audit record lands in ~/.dsh/storages/dsh-topics-memory/legacy-import.json). 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
suppressEchotrueDistill-echo suppression: topics distilled from the CURRENT session's own turns are not injected back into it (provenance rides the observations log sessionId → distilledInto)
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 (total cap across hits equals this value)
injectModepointerInjection shape: pointer (light pointers, ≤80 tok each, topic_open pulls the full text; total budget capped at 600) / digest (full digest rendering, per-topic 300 / total 1500)
qualityLanesampledSlow quality lane: off / sampled (1/3 of turns) / always; produced at turn/end, consumed by the next injection (consume-once), never for subagent sessions
graphDepth2depends graph walk depth (0 disables)
recencyWindowDays7Recency bonus window (+0.2)
autoObservetrueCapture atomic observations every turn
includeSubagentsfalseWhether injection and observation also engage subagent sessions (ADR 0011; off by default since 0.7.0); 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
consolidateCadencedailyConsolidation-lane cadence: daily/3d/7d/off. At session start the plugin checks the last consolidation time (meta/consolidate-state.json) and, when due, runs the LLM gardener in the background (reusing the distill model route): local lexical clustering only feeds near-look-alike candidate clusters; four op kinds merge/promote/deprecate/refresh, out-of-scope ops are dropped; a failed call never advances the stamp, so the next start retries
deprecatedTtlDays15Deprecated topics older than N days are dropped at session start (local rule, no model; each drop is its own git commit — history stays recoverable); 0 disables the sweep
usageBoost0.15Usage boost (ADR 0015): topics injected/opened in the last 30 days score higher at retrieval (gate-scoped, capped at 0.2, never granted to zero-lexical candidates, structural gate not waived); 0 disables
pushDebounceSeconds45GitHub-mode debounced push interval
jevEnabledfalseSystem One decision layer master switch (experimental): false = zero behavior change — see the Jev decision layer section above
jevBackendzenDecision endpoint: zen (free) / native / openrouter
jevModelempty (per backend: jev-1.13-free / jev-1.13.0 / typesafe/jev-1.13)Version-pinned decision model; upgrading is an explicit action
jevTimeoutMs3000Per-request decision timeout; single attempt, no retry
jevSecretFilenoneExternal secret list for the outbound secret gate; re-read when hot-changed
jevLayaFallbackfalseLocal laya pace-maker: fire a parallel laya call on every jev request; its answer takes over (degraded, relative-ranking only) when the primary fails
jevLayaUrlhttp://127.0.0.1:8000/v1/systemonelaya-serve endpoint for the pace-maker

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 are out of memory by default — one switch to opt in: by default (include-subagents off since 0.7.0) delegated sessions are skipped entirely — no injection, no observation, no distill triggers; /topics set include-subagents on applies injection and observation to them too. 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.
  • Exit is local-only (0.10.0): the plugin's disposer makes one local git commit of the meta sidecars (observations / injections / distill state) and never waits on the network — no pull, no push, no model call, so host exit no longer pays a git round-trip or a bounded distill wait (the old 90s cap is gone; only a 10s guard against a pathological git stall remains). The exit distill trigger is fired fire-and-forget and is skipped entirely while a session-end run is still in flight (the same pool head would otherwise be fed to the model twice). Nothing is lost by skipping: observations are write-through on disk, the deferred push is replayed by the next boot's pull, and the skipped distill is replayed there too (boot-replay). 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 profile-patch edits take effect most reliably from the next session start (dsh 0.1.7: the settings document is the profile patch; the old settings.yaml is imported once and renamed).
  • 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–0019: 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, dual-channel injection, the usage boost, direct-HTTP decisions, the fast-lane zero-remote-calls line, hardcoded fail-open, the embedded secret gate

License

MIT

Versions

Latest versionPublishedSize
0.6.0——
0.7.0——
0.8.0——
0.8.1——
0.9.0——
0.10.0——
0.11.0——
0.12.0——
0.13.0——
0.13.1——
0.14.0——
0.15.0——
0.16.0——
0.16.1-next.1——
0.17.1——
0.18.0——
0.18.1——
0.18.2——
0.19.0——
0.20.0——

Comments

Loading…

Similar plugins

dsh-memory

by yangyongzhen

Long-term memory injected at session start: preferences/facts/summaries/knowledge in global and per-project scopes, budgeted recall via `agent/pre-step`, durable JSON store.

Memory & ContextSessions & MessagesManifest valid

★ 1

↓ 669/wk

JavaScript

Aug 15, 2026

dsh plugin --profile web add dsh-memory

by ZHI-QI

dsh-okf-memory: 会话记忆 → OKF 知识沉淀插件(神经自我学习驱动)。Session-to-OKF memory plugin: predictive recall, uncertainty-driven capture, reinforcement feedback, consolidation & forgetting.

Manifest valid

★ 3

↓ 33/wk

MIT

JavaScript

Sep 13, 2026

dsh plugin --profile web add dsh-okf-memory

Fully local vector memory for DSH: local embeddings, SQLite storage, automatic recall injection, dedup with conflict detection, soft-delete recycle bin, online backup, and optional LLM extraction.

Memory & ContextManifest valid

★ 0

↓ 46/wk

dsh plugin --profile web add dsh-local-vector-memory

by jonah791

Agent-driven long-term memory for DeepSeek Harness: scoped memory (global + per-workspace), layered entries (fact/knowle

Manifest valid

★ 3

↓ 97/wk

MIT

TypeScript

Sep 7, 2026

dsh plugin --profile web add dsh-agent-memory

by menotbobbybrown

Persistent long-term memory for DeepSeek Harness: a JSON store of typed entries (fact, preference, entity, rule, episodic) exposed as the memory_remember and memory_recall agent tools, with recall ran

Memory & ContextManifest valid

★ 0

MIT

TypeScript

Sep 8, 2026

dsh plugin --profile web add @modelnorth/dsh-plugin-memory

by LittleBlackTong

Long-term cross-session markdown memory with an LLM-Wiki structure and a SOUL.md persona, injected at session start (by default only after the first user message and only in the active session), plus

Memory & ContextManifest valid

★ 4

↓ 348/wk

MIT

JavaScript

Sep 29, 2026

dsh plugin --profile web add dsh-plugin-memory