ddtcorex/dsh-maestro-memory ↗★ 1
@ddtcorex/dsh-maestro-memory
持久化分层记忆系统(全局/用户/项目/分支/每日),支持确认后写入、待办事项与技能管理,保留本地 memories 文件并支持 SHA-256 备份与回滚。
AI 分析
核心用途是为 DSH 提供安全、可控的多层级长期记忆与待办管理。适合需要跨项目、跨分支保持上下文,且要求记忆数据本地化、可备份的用户。
安装
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:ddtcorex/dsh-maestro-memory说明文档
阅读完整 README ↗dsh-maestro-memory
Purpose
Durable, user-governed memory and todos for DeepSeek Harness (DSH) that preserves your existing ~/.dsh/memories files in place.
One sentence: Give the AI in DSH cross-session durable memory and todos — the more you use it, the more it understands you, and switching sessions never loses context.
- Package:
@ddtcorex/dsh-maestro-memory(cordis.patch.ymlidmaestro-memory) - Changelog:
CHANGELOG.md - Version:
1.0.0
Requirements
- Node.js 22+, pnpm 11+
- DSH
deepseek-harnessmaster (forcordis,dsh-client-*peers) - Existing
~/.dsh/memoriesdirectory (created lazily if absent)
Install
From the checked-out repo:
pnpm install # install deps (frozen lockfile in CI)
pnpm run build # tsc host + tsc client + build-client.mjs -> lib/
pnpm run verify # tsc --noEmit host + client (typecheck)
pnpm test # full Vitest suite (13 files, 199 tests)
Manual verification of the client bundle:
test -f lib/client.js && head -n 2 lib/client.js | grep -q "ModuleLoader" && echo "bundle ok"
ls -lh lib/client.js lib/index.js
From a DSH profile (operator)
The package is consumed as a DSH plugin via cordis.patch.yml. Two install paths:
Local link (development / recommended for cutover rehearsal):
# inside the profile that will own the plugin:
dsh plugin --profile web add link:/home/kai/Work/htdocs/maestro-harness/dsh-maestro-memory
# or manually in ~/.dsh/profiles/web/package.json:
# "@ddtcorex/dsh-maestro-memory": "link:/home/kai/Work/htdocs/maestro-harness/dsh-maestro-memory"
Git / registry (production after release):
dsh plugin --profile web add github:ddtcorex/dsh-maestro-memory#
# pin to an exact commit SHA; branch names reuse stale tarballs (pnpm cache pitfall)
After install, rebuild is not needed inside the profile — the host loads lib/index.js and the client loads lib/client.js via the dsh.client manifest. If you edited src/client/, rebuild at the checkout first (pnpm run build).
Profile Patch
cordis.patch.yml is owned by the package and applied automatically by dsh plugin add. Do not duplicate it in the profile.
# dsh-maestro-memory/cordis.patch.yml (shipped with the package)
- insert:
- id: maestro-memory
name: '@ddtcorex/dsh-maestro-memory'
config:
memoryDir: null # null -> ~/.dsh/memories
snapshotOrder: 500 # systemPrompt.context order
Profile ~/.dsh/profiles/web/package.json after a correct install:
{
"dsh": { "profile": { "bundles": ["@ddtcorex/dsh-maestro-memory"] } },
"dependencies": {
"@ddtcorex/dsh-maestro-memory": "link:/home/kai/Work/htdocs/maestro-harness/dsh-maestro-memory"
}
}
Rules:
dependenciesvalue must belink:, not a semver. CI andassertSingleOwnerreject non-link owners.bundlesmust list exactly one owner for each compat tool (see below). Do not keepdsh-memory-evolveanddsh-maestro-memoryin the same profile — they compete formemory/dtodoand for file ownership. The loader crashes on duplicateid: maestro-memoryif you copy the patch row into the profile manually.memoryDir: nullresolves to~/.dsh/memories(resolveMemoryRoot(null)). Override only for tests / rehearsal (--root /tmp/...).
Verify the profile in a rehearsal (see src/host/migration/fixture.ts):
import { createFixtureProfile, assertSingleOwner } from '@ddtcorex/dsh-maestro-memory/migration/fixture'
await createFixtureProfile({ profileDir: '/tmp/profile', packageDir: '/path/to/dsh-maestro-memory' })
const res = await assertSingleOwner('/tmp/profile')
console.assert(res.ok && res.owners['memory'] === '@ddtcorex/dsh-maestro-memory')
Supported Tools
All tools are registered via ctx.tools.register inside ctx.effect(..., 'label') so they dispose cleanly on unload. No HTTP.
| Tool | Purpose | When visible |
|---|---|---|
memory | CRUD + query for five tracks (memory/user/project/key/daily) + archive/expand. See src/host/memory/store.ts. | Always |
dtodo | Four-track todos (life/work/project/daily) with stable 8-hex ids, status/due/quadrant, smart view (max 8), historical daily lookup. | Always |
memory_suggest | Gated — model proposes memory/user/key/todo-* into SUGGESTIONS.jsonl; never writes directly. Requires human approve/edit/reject via Review UI or queue.decide RPC. | Always |
memory_review_status | Read-only queue depth / write-block status (used by prompt hint / UI badge). | Always |
skill_manage | Browse / mutate ~/.agents/skills (optional module). Disabled by default; enable only if the optional skills module is explicitly configured. | Opt-in |
memory — actions and targets
memory({
action: 'add'|'list'|'replace'|'remove'|'archive'|'expand',
target: 'memory'|'user'|'project'|'key'|'daily', // memory=global, key=per-cwd long-term
content?: string, // add: entry body; replace: new body
match?: string, // replace/remove/archive: unique substring of existing entry
filter?: string, // list: content substring filter
since?: string, until?: string, // list: YYYY-MM-DD
limit?: number, recent?: boolean, branch?: string, archived?: boolean,
branches?: string, // add key: csv "main,dev" (empty=all), branch scope
summary?: string, // add key: one-line summary for progressive disclosure
id?: string, // expand: [mem-xxxx] id
cwd?: string, // project/key track working directory (defaults to session cwd)
date?: string, // daily track YYYY-MM-DD
})
- Progressive disclosure:
keyentries are stored with an optional[summary]line;listwithoutexpandreturns summaries;expandwithidreturns full text. - Branch scope:
keyentries may carry[branch:main]tags;listwithbranchfilters to that branch + entries with no branch tag.
dtodo — actions
dtodo({
action: 'add'|'list'|'done'|'update'|'remove',
target?: 'life'|'work'|'project'|'daily', // add/list filter; add defaults to cwd?project:work
content?: string,
id?: string, // done/update/remove
due?: string, // YYYY-MM-DD
quadrant?: 'q1'|'q2'|'q3'|'q4', // or important/urgent booleans -> quadrant
cat?: string, status?: 'pending'|'doing'|'done'|'blocked'|'cancelled',
all?: boolean, past?: boolean, expired?: boolean, // list: smart-view controls
cwd?: string, date?: string,
})
- Smart view (default): when
all !== trueand no filter,listreturns at most 8 items orderedoverdue -> due today -> current project -> q1/q2 -> rest. Uses local date, not UTC. - History:
past=truealone shows only completed history;past=true AND expired=trueincludes expired unfinished daily todos (daily todos expire same day).
memory_suggest (gated)
memory_suggest({ target: 'memory'|'user'|'key'|'todo-life'|'todo-work'|'todo-project'|'todo-daily', content: string, reason: string })
Dedupes by (target, content) within the queue (bumps hits), appends to SUGGESTIONS.jsonl. The model must never write key/user directly — queue + human click is the only activation path.
System Prompt Snapshot
Registered as ctx.systemPrompt.context({ name: 'memory:snapshot', order: 500, text: (ctx) => renderSnapshot(cwd, branch) }).
Injected text is bounded and deterministic: USER + global MEMORY + current-project KEY (branch-filtered if session.header.branch is present), plus a header with sessionId/sessionName and an end-of-turn discipline note:
End of every turn ... you must: 1. Write
daily+projectviamemoryentries (daily+project in one call) 2. Checkdtodo list(bounded, max 8)
daily and project log (projects//MEMORY.md) are queryable via memory but not injected, to keep prompt cost predictable. New prompt/snapshot.ts must reproduce this contract or agents silently stop writing logs.
UI & RPC
- UI: exactly one
conversation.viewslot{ name:'conversation.view', id:'maestro-memory', order:40, label:()=>'Memory' }with internal tabs Memory / Review queue / Todos. Uses package-private RPC, no HTTP, no DOM hacks. Client injects['slots','locale','conversation','sessions','connection']. - RPC channel:
/dsh-maestro-memory(ctx.connection.rpc.handlehost,ctx.connection.rpc.callclient). Endpoints:queue.list,queue.decide(approve/reject/archivewith optionaledits/targets+cwd),memory.list,todo.list,todo.mutate,migration.inspect/dryRun/run/verify,status({ queue, blocked }).migration.runvia RPC requirespayload.apply === true.
Cutover
Principle: staged single-owner replacement — never run dsh-memory-evolve and dsh-maestro-memory in the same profile. The new internals, services, RPC methods, and slot ids use a Maestro namespace; compatibility is limited to agent-facing tool names and legacy file grammar.
Operator steps (production):
- Preflight on a copy, not live home (see Migration). Keep the live profile untouched until verification passes.
- Backup the live
~/.dsh/memoriesvianode scripts/migrate.mjs --root ~/.dsh/memories --apply— this is the only write; it createsmanifest.json+ byte-identicalfiles/under.maestro-memory/backups//+schema.json+journal. - Verify (
--verify) — must beok=true,mismatches=[]. If not, writes are blocked (write-block.json) — resolve before continuing. - Profile swap: remove
dsh-memory-evolvefrombundles/dependencies, add@ddtcorex/dsh-maestro-memoryaslink:(or pinned git SHA). Ensure exactly one owner per compat tool (memory,dtodo). - Reload profile: restart
dsh webat a user-approved window (ask first — do not kill the livedsh webprocess mid-session; it holds both :3000 and :3080). After restart, live-read every track (memorylist for each target,dtodolist) before first mutation. - One write against live data, then
verifyagain.
Before any writes, rollback is just a profile change (remove Maestro, restore old bundle). After writes, restore files from the manifest.
For a disposable rehearsal, use src/host/migration/fixture.ts (createFixtureProfile, createCopiedMemoryRoot, assertSingleOwner) — see tests/m4-rehearsal.spec.ts and the Migration rehearsal CI job. Never touch ~/.dsh/memories in tests.
Migration
CLI: node scripts/migrate.mjs --root [--inspect|--dry-run|--verify|--apply] [--run-id ]
Default is read-only. The only write is --apply.
| Command | Effect | Side effects |
|---|---|---|
--inspect (default) | Inventory, parse, byte count, SHA-256, warnings for malformed JSONL / locks / non-canonical files | None |
--dry-run | Same as inspect, explicitly read-only | None |
--apply | Backup + adopt: byte-preserving copy of every file (excluding .maestro-memory) into backups//files/ + manifest.json (path, bytes, sha256, inventory) + schema.json + migration-journal.jsonl entry. Only after all required data parses; source content is never reformatted. | Writes manifest, files/, schema.json, journal |
--verify | Reopen with new stores, compare digest (bytes, sha256) + inventory (memoryEntries, todoIds, queueValid) against manifest. On mismatch, writes .maestro-memory/write-block.json and blocks mutations; on success clears the block. | Writes write-block.json on failure; clears on success |
Disk layout:
~/.dsh/memories/
MEMORY.md USER.md # may be absent until first global write
MEMORY-archive.md USER-archive.md
SUGGESTIONS.jsonl
TODOS-life.md TODOS-work.md
daily/YYYY-MM-DD.md daily/YYYY-MM-DD.todo.md
projects//
MEMORY.md KEY.md
KEY-archive.md TODOS.md
.maestro-memory/
schema.json
migration-journal.jsonl
write-block.json # present only when verify failed
backups//
manifest.json # { files:[{path,relative,bytes,sha256,kind,...}], inventory, runId, at }
files/... # byte-identical copies
Warnings (non-fatal, reported in inspect/dryRun/verify):
non-canonical— file does not round-trip through§parse/serialize (drift); mutation is refused until canonicalized.malformed todo— entry missing timestamp/id in a todo file.malformed queue— JSONL line inSUGGESTIONS.jsonlthat does not parse as{target, content}.
Write-block: migration/service.ts:isWriteBlocked(root) checks .maestro-memory/write-block.json. When blocked, memory/dtodo mutations return an error until verify passes or rollback clears it.
Examples:
node scripts/migrate.mjs --root ~/.dsh/memories # inspect (read-only)
node scripts/migrate.mjs --root /tmp/mem --dry-run # dry-run
node scripts/migrate.mjs --root /tmp/mem --apply # backup + adopt
node scripts/migrate.mjs --root /tmp/mem --verify # verify (latest manifest)
node scripts/migrate.mjs --root /tmp/mem --verify --run-id 20260824T151230.425Z
Verification
- After
inspect/dryRun, confirmok=true, expectedmemoryEntries/todoIdsCount/queueValid, and reviewwarnings. - After
--apply, confirmmanifest.jsonexists, eachfiles/copy is byte-identical (sha256matches), and~/.dsh/memoriesfiles are unchanged (no reformatting). - After
--verify, confirmok=true,mismatches=[]. Ifok=false, checkmismatches(digest mismatch,byte count mismatch,todo ID set mismatch,inventory mismatch) and.maestro-memory/write-block.json. No mutation should proceed while blocked. - After profile reload, live-read via tools/RPC (
memorylist formemory/user/key/daily/project,dtodolist forlife/work/project/daily) and compare to pre-cutover inventory.
The rehearsal suite (tests/m4-rehearsal.spec.ts) exercises the full sequence against a copied schema: fixture profile (link:) → one-owner proof → dry-run (no .maestro-memory) → backup (byte-preserving) → verify → profile reload (apply/ctx.effect) → live reads → one write → second verify (fails) → rollback (byte-identical) → verify (passes) → live home untouched.
Rollback
Rollback restores files byte-identical from a backup manifest. It is exercised and tested in tests/m4-rehearsal.spec.ts.
When to rollback:
- Before any writes: no rollback needed — just revert the profile change (remove Maestro bundle, restore old plugin).
- After a failed
verifyor a bad write: restore from the backup thatverifyreports.
How (CLI / service API):
import { rollback } from './src/host/migration/service.ts'
// restore latest (schema.json runId or newest backup)
await rollback('/tmp/memories')
// or specific run
await rollback('/tmp/memories', '20260824T151230.425Z')
Or via the migration RPC (host) if exposed. The service:
- Copies each
manifest.files[].relativefrombackups//files/to its originalpath, verifyingsha256after copy. - If a file was absent at backup time (
exists:falsein manifest) but appeared later, it is removed. - Clears
write-block.jsonon completion and appends arollbackentry tomigration-journal.jsonl. - Returns
{ ok, runId, manifestPath, restored, errors }(restored= count of files restored/removed).
After rollback:
verifymust pass (ok=true, no mismatches).- A new write must succeed (the write-block is cleared).
Retention: keep ~/.dsh/memories/.maestro-memory/backups/ for at least 90 days afte