ddtcorex/dsh-maestro-memory1

@ddtcorex/dsh-maestro-memory

持久化分层记忆系统(全局/用户/项目/分支/每日),支持确认后写入、待办事项与技能管理,保留本地 memories 文件并支持 SHA-256 备份与回滚。

AI 分析

核心用途是为 DSH 提供安全、可控的多层级长期记忆与待办管理。适合需要跨项目、跨分支保持上下文,且要求记忆数据本地化、可备份的用户。

包名
@ddtcorex/dsh-maestro-memory
版本
1.0.0
许可证
MIT
最近更新
2026年8月24日

安装

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:ddtcorex/dsh-maestro-memory

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.yml id maestro-memory)
  • Changelog: CHANGELOG.md
  • Version: 1.0.0

Requirements

  • Node.js 22+, pnpm 11+
  • DSH deepseek-harness master (for cordis, dsh-client-* peers)
  • Existing ~/.dsh/memories directory (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:

  • dependencies value must be link:, not a semver. CI and assertSingleOwner reject non-link owners.
  • bundles must list exactly one owner for each compat tool (see below). Do not keep dsh-memory-evolve and dsh-maestro-memory in the same profile — they compete for memory/dtodo and for file ownership. The loader crashes on duplicate id: maestro-memory if you copy the patch row into the profile manually.
  • memoryDir: null resolves 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.

ToolPurposeWhen visible
memoryCRUD + query for five tracks (memory/user/project/key/daily) + archive/expand. See src/host/memory/store.ts.Always
dtodoFour-track todos (life/work/project/daily) with stable 8-hex ids, status/due/quadrant, smart view (max 8), historical daily lookup.Always
memory_suggestGated — 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_statusRead-only queue depth / write-block status (used by prompt hint / UI badge).Always
skill_manageBrowse / 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: key entries are stored with an optional [summary] line; list without expand returns summaries; expand with id returns full text.
  • Branch scope: key entries may carry [branch:main] tags; list with branch filters 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 !== true and no filter, list returns at most 8 items ordered overdue -> due today -> current project -> q1/q2 -> rest. Uses local date, not UTC.
  • History: past=true alone shows only completed history; past=true AND expired=true includes 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+project via memory entries (daily+project in one call) 2. Check dtodo 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.view slot { 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.handle host, ctx.connection.rpc.call client). Endpoints: queue.list, queue.decide (approve/reject/archive with optional edits/targets + cwd), memory.list, todo.list, todo.mutate, migration.inspect/dryRun/run/verify, status ({ queue, blocked }). migration.run via RPC requires payload.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):

  1. Preflight on a copy, not live home (see Migration). Keep the live profile untouched until verification passes.
  2. Backup the live ~/.dsh/memories via node scripts/migrate.mjs --root ~/.dsh/memories --apply — this is the only write; it creates manifest.json + byte-identical files/ under .maestro-memory/backups// + schema.json + journal.
  3. Verify (--verify) — must be ok=true, mismatches=[]. If not, writes are blocked (write-block.json) — resolve before continuing.
  4. Profile swap: remove dsh-memory-evolve from bundles/dependencies, add @ddtcorex/dsh-maestro-memory as link: (or pinned git SHA). Ensure exactly one owner per compat tool (memory, dtodo).
  5. Reload profile: restart dsh web at a user-approved window (ask first — do not kill the live dsh web process mid-session; it holds both :3000 and :3080). After restart, live-read every track (memory list for each target, dtodo list) before first mutation.
  6. One write against live data, then verify again.

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.

CommandEffectSide effects
--inspect (default)Inventory, parse, byte count, SHA-256, warnings for malformed JSONL / locks / non-canonical filesNone
--dry-runSame as inspect, explicitly read-onlyNone
--applyBackup + 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
--verifyReopen 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 in SUGGESTIONS.jsonl that 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

  1. After inspect/dryRun, confirm ok=true, expected memoryEntries/todoIdsCount/queueValid, and review warnings.
  2. After --apply, confirm manifest.json exists, each files/ copy is byte-identical (sha256 matches), and ~/.dsh/memories files are unchanged (no reformatting).
  3. After --verify, confirm ok=true, mismatches=[]. If ok=false, check mismatches (digest mismatch, byte count mismatch, todo ID set mismatch, inventory mismatch) and .maestro-memory/write-block.json. No mutation should proceed while blocked.
  4. After profile reload, live-read via tools/RPC (memory list for memory/user/key/daily/project, dtodo list for life/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 verify or a bad write: restore from the backup that verify reports.

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[].relative from backups//files/ to its original path, verifying sha256 after copy.
  • If a file was absent at backup time (exists:false in manifest) but appeared later, it is removed.
  • Clears write-block.json on completion and appends a rollback entry to migration-journal.jsonl.
  • Returns { ok, runId, manifestPath, restored, errors } (restored = count of files restored/removed).

After rollback:

  • verify must 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