Skip to content

fix(guidance): carry the runtime memory snapshot as this plugin's own message - #107

Merged
Grivn merged 1 commit into
omdsh-dev:mainfrom
daveycodez:fix/runtime-memory-own-message
Aug 30, 2026
Merged

fix(guidance): carry the runtime memory snapshot as this plugin's own message#107
Grivn merged 1 commit into
omdsh-dev:mainfrom
daveycodez:fix/runtime-memory-own-message

Conversation

@daveycodez

@daveycodez daveycodez commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

摘要 / Summary

mnemon:runtime-memory was contributed through systemPrompt.context(), so the host merged it into the shared @deepseek-ai/dsh-system-prompt runtime-context projection. Two consequences followed, and this changes only the carrier to fix both.

Attribution. The snapshot rendered as Context injection · @deepseek-ai/dsh-system-prompt, as though the harness produced it. Mnemon's own block carried only the 275-character [MNEMON] routing line. The snapshot is now a dsh-mnemon message and is attributed correctly.

Cache. DSH joins every .context() contribution into one string and diffs the whole (RuntimeContext.project() over joinContextSections(sections)), so a memory write re-emitted sandbox:policy and approval:policy along with it. At session start that projection is also composed ahead of skill-catalog, so across sessions a memory write invalidated roughly a thousand tokens of byte-stable catalog that had not changed.

The block remains a complete state superseding its predecessor — only the carrier changed. Supersede is keyed on the rendered text, which already carries the revision digest.

关联 Issue 或背景 / Related Issue or Context

Closes #111.

#111 is a verbatim refile of #106, which the template enforcer auto-closed nine seconds after submission: the body used level-2 (##) headings while .github/workflows/issue-template-enforcer.yml matches sections only at level 3 (###), so every required section read as empty. The content is unchanged.

Adjacent and complementary, not overlapping:

Why the ordering half is included

dsh-tool-skill ships inside @deepseek-ai/dsh-base and registers its agent/pre-step listener before dsh-mnemon does. Neither used prepend, so both are pushed onto the listener list in load order. Cordis runs waterfall listeners outermost-first, so the innermost returns first and appends first — meaning the later-loading plugin lands earlier in the batch. Mnemon therefore sat ahead of skill-catalog, and moving the snapshot to an own message without changing that would leave the catalog still behind it.

The existing lifecycle listener is now registered with prepend: true rather than adding a second listener. A second listener looked cleaner but the test harness keys listeners by event name (agentListeners.set(name, listener)), so a second agent/pre-step registration silently replaces the first; a one-listener change avoids a harness rewrite. Running outermost also means recordTurnMessages observes the complete batch rather than a partial one.

Why this matters disproportionately for local inference

Prefix caching is strictly positional: one changed token invalidates everything downstream, so a single memory write forfeits every byte behind it in the next session. On a hosted API that is a discounted line item. On local inference it is wall-clock time to first token, on hardware the user is also running the model on.

Measured on this setup, DSH against a local oMLX server (DeepSeek-V4-Flash-8bit, Apple silicon):

  • Two fresh sessions differing only in the first prompt: cache hit 42% → 94%, TTFT 10s → 3s, purely from moving stable context ahead of the variable part. ~2,000 tokens of misplaced context was worth 7 seconds at every session start.
  • oMLX quantizes its KV cache to 2048-token blocks. skill-catalog alone is ~1,073 tokens, so it straddles roughly a whole block that is reusable in principle and lost in practice whenever memory changed since the previous session.
  • The local cache is single-tenant and disk-backed, so cross-session reuse is near-guaranteed on a prefix match — no TTL, no multi-tenant eviction. Local is exactly where correct placement pays the most.
  • Local users also run many short sessions, which concentrates a per-session-start tax.

涉及区域 / Affected Areas

  • 子 Agent 或 Agent 工作流 / Subagent or Agent workflow
  • 测试、构建或文档 / Tests, build, or documentation

PR 类型 / PR Type

  • Bug 修复 / Bug fix

最新代码确认 / Latest Codebase Confirmation

  • 我已基于最新 main 分支开发,或在提交前已 rebase 或合并最新 main。 / I developed from the latest main, or rebased or merged the latest main before submitting.

Branched from e118094 (Merge pull request #104, v0.3.4).

同步命令 / Sync command:

git fetch origin && git rebase origin/main

AI 编码披露 / AI Coding Disclosure

  • 完全 AI 编码:全部编程改动由 AI 产出,并由贡献者接受和审查。 / Fully AI-coded: AI produced all programming changes, which the contributor accepted and reviewed.

使用的 AI 模型 / AI model used:

Claude Opus 5

使用的编码 Agent 工具 / Coding Agent tool used:

Claude Code

仓库规范检查 / Repository Rules

  • 未修改 DSH 官方源码,未让 tsconfig 指向 DSH 源码 checkout,仅使用正式的 @deepseek-ai/* NPM 契约。 / I did not modify DSH source or point tsconfig at a DSH source checkout, and used only published @deepseek-ai/* NPM contracts.
  • Client 与 Host 边界仍以 src/shared/contracts.ts 为准,没有在两侧重复定义 wire DTO。 / The Client and Host boundary still uses src/shared/contracts.ts as the single source for wire DTOs. This change touches only the Host-side src/contracts.ts and adds no Client or wire DTO.
  • 持久化格式、RPC 权限、路径或凭据处理的变更包含兼容或拒绝路径、安全分析和相应测试。 / Changes to persistence formats, RPC authority, paths, or credentials include compatibility or rejection paths, security analysis, and tests. None of these are touched.
  • 没有提交 token、密钥、私有记忆、未脱敏日志或生成的 lib/ 文件。 / I did not commit tokens, credentials, private memory, unredacted logs, or generated lib/ files.
  • 用户可见文案和长期文档已同步维护中文与英文版本,命令、配置键和路径保持一致。 / User-facing copy and long-lived documentation are synchronized. No user-facing copy, configuration key, or path changed; the snapshot text and the routing reminder are unmodified.
  • 新增和修改的代码、注释、文档、提交信息不含 emoji。 / New and modified code, comments, documentation, and commits contain no emoji.

兼容性与数据安全 / Compatibility and Data Safety

No persistence format, storage path, RPC authority, or Provider credential is touched, and no configuration key is added or renamed.

  • HostAgentContext['on'] and HostContextShape['on'] gain an optional third options parameter. Omitting it keeps the previous two-argument behaviour exactly, so this is source- and behaviour-compatible in both directions.
  • applyAgentMemoryViewWake now filters any inherited mnemon:runtime-memory contribution out of assembly.contexts, so a profile upgraded in place stops emitting it through the shared projection rather than emitting it twice.
  • memoryPromptText is exported so the same {{}} neutralization applies to the injected text; interpolation is still never parsed.
  • registerRuntimeMemoryContext and registerAgentRuntimeMemoryContext are left as they are. Neither is wired in production — only registerAgentMemoryViewContext is, at lifecycle.ts — and their tests still pass, so the diff stays minimal. Happy to migrate or remove them if you would rather they not diverge.
  • The static mnemon:runtime-memory-protocol section is unchanged and stays in the system prompt.

本地验证 / Local Validation

执行的命令 / Commands run:

pnpm install
./node_modules/.bin/tsc --noEmit -p tsconfig.json
TZ=UTC pnpm run verify

结果摘要 / Result summary:

All checks pass: typecheck clean; Vitest 511 passed, 1 skipped, 0 failed; deterministic double builds verified across 106 files; Headless profile activation verified with 35 total tools and 5 representative Mnemon tools; package contents verified (113 files); 10 Node-compatible public entries imported on v24.20.0; publint --strict and attw --pack . --profile esm-only both pass. Nothing skipped or failed.

TZ=UTC is needed only because tests/pack.spec.ts fails 11 tests on unmodified main in timezones behind UTC, unrelated to this change (filed separately as #112): src/pack.ts pins mtime: new Date('1980-01-01T00:00:00.000Z') for reproducible archives, and fflate validates against local date parts, so in any negative-UTC-offset timezone that reads as 1979 and trips date not in range 1980-2099. Reproduced on unmodified main at e118094; TZ=UTC passes 12/12. Details and a suggested one-line fix are in #112.

测试改动 / Test changes

Existing assertions were updated where they pinned the old carrier or the old message count, and the pinning test now observes the Wake through the injected message instead of the context callback — covering both directions of supersede: unchanged text on a later step is not re-injected, and a new turn's Wake is.

… message

The snapshot was contributed through systemPrompt.context(), so the host
merged it into the shared dsh-system-prompt runtime-context projection. It
therefore rendered as 'Context injection - @deepseek-ai/dsh-system-prompt'
rather than as dsh-mnemon, and because DSH joins every context contribution
into one string and diffs the whole, a memory write re-emitted sandbox:policy
and approval:policy with it.

At session start that projection is also composed ahead of skill-catalog, so
across sessions a memory write invalidated roughly a thousand tokens of
byte-stable catalog that had not changed.

The snapshot now travels as a dsh-mnemon message appended last in the batch,
and the pre-step listener is registered with prepend so it observes the fully
assembled batch. The block remains a complete state superseding its
predecessor; only the carrier changed. Supersede is keyed on the rendered
text, which already carries the revision digest.

The static mnemon:runtime-memory-protocol section is untouched.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Issue]: Runtime memory rides in the shared dsh-system-prompt projection: wrong attribution, and cross-session it invalidates skill-catalog

2 participants