# dsh-file-mount

> Provides incremental file mounting for DeepSeek Harness: automatically tracks which lines of read files enter the model context, re-reads only supply missing or changed portions, and presents a billable "mounted files" dashboard on the web.

## Metadata

- Author: [@acefun29](https://github.com/acefun29)
- Repo: <https://github.com/acefun29/dsh-file-mount.git>
- GitHub: [acefun29/dsh-file-mount](https://github.com/acefun29/dsh-file-mount)
- Stars: 11
- Language: TypeScript
- License: [MIT](https://spdx.org/licenses/MIT.html)
- Topics: `context`, `cordis`, `deepseek-harness`, `dsh-plugin`
- Forks: 0
- Open Issues: 0
- Last push: 2026-08-17T16:36:41.000Z
- Added: 2026-08-20T00:00:00.000Z

## Install

```bash
dsh plugin --profile web add github:acefun29/dsh-file-mount
```

## Wiki

## One-Line Description
dsh-file-mount recaptures the context window wasted when "AI repeatedly reads the same file" — it maintains a ledger for DSH's read/write/edit tools tracking which lines of each file have already entered the model context, so repeated reads only supplement missing lines or lines changed on disk, plus includes a browser-side "Mounted Files" dashboard that displays the ledger and savings in real time.

## Core Capabilities
- Intercept `read` tool results, replacing mounted line ranges with a single "mounted" marker instead of stuffing entire content blocks into context repeatedly
- When file disk content changes, use line-level diff to only supplement modified lines (append-only logs only add new tails; when middle sections are too large, split by unique line anchors using LCS)
- Files the model just `write`s are automatically marked as "known" — subsequent reads are fee-exempt; `edit` marks expire but retain line fingerprint drafts for incremental comparison
- Provides `file_mount_forget` tool, allowing the model to actively drop a file's mount and force the next read to resend the entire file
- Register a "Mounted Files" tab on the web side: list each file, expand into line segments, show coverage maps (position of mounted lines in the full file), mark freshness with color ribbons, and display net savings with rough RMB conversion
- Support compressed sensing: when DSH's standard compression checkpoint appears, old mount messages it shadows are no longer counted in the ledger, avoiding incorrect deduplication based on removed content

## Technical Implementation
- **Language**: TypeScript (build outputs `lib/index.js` + `lib/client.js`, dual-side release)
- **Key Dependencies**: `@deepseek-ai/cordis` (Service/Context container), `@deepseek-ai/schemastery` (Config schema), `@deepseek-ai/dsh-tools` (`defineTool` to register `file_mount_forget`, `tools/post-execute` interception hook), `@deepseek-ai/dsh-llm` (`createUserMessage` to inject mount hints)
- **Architecture Pattern**: Dual-sided Cordis plugin — `cordis.patch.yml` injects a plugin named `file-mount` into the host via bundle.patch (host side runs `tools/post-execute` hook for incremental deduplication), `package.json#dsh.client` registers `file-mount-ui` tab via `conversation.view` slot of `dsh-client-ui-conversation` (browser side renders dashboard); the ledger attaches structured `source` fields to standard `user/message` events, with host compaction/session recovery/browser folding sharing the same merge rules (`mount-source.ts`)
- **Entry Files**: `src/index.ts` (host side `FileMountService`, exports `name = 'file-mount'`), `src/client/index.ts` (browser side `apply(ctx)`, exports `name = 'file-mount-ui'`)

## Use Cases
During daily DSH and AI collaboration modifying a mid-scale project, the model frequently revisits `package.json`, config files, utility scripts, and UI component source files — most lines in these files have already entered context, and pasting them again just wastes the window. dsh-file-mount makes "reading the same book a second time" become just pasting bookmarks, only supplementing lines when the model truly needs new content; it's also suitable for having the AI confirm what it wrote after completing a file — the write action automatically makes the file "known," so reading it again incurs no token cost.

## Prerequisites & Compatibility
| Dependency | Minimum Version | Notes |
|---|---|---|
| DeepSeek Harness | 0.1.0-rc.5 (tested) | peerDependencies all declare `^0.1.0-rc.5`; plugin injects into host via `cordis.patch.yml`, into Web client via `dsh.client` |
| Node.js | ^22.19.0 or >=24.0.0 | enforced by `package.json#engines`; `dsh plugin` installation chain also calls pnpm |
| Platform | macOS / Windows / Linux | No native modules; `paths.ts` only decides whether to fold case and drive letters based on `process.platform === 'win32'` during path normalization |
| Native Modules | None | Only depends on Node built-ins `node:fs/promises`, `node:path`, and the npm dependencies above |

## Installation
```bash
dsh plugin --profile web add github:acefun29/dsh-file-mount
```

> This is the installation command users see on the landing page. The README notes that `github:` format currently only installs source (repo has no `lib/` and no `prepare` script), the actual production path recommends `pnpm dsh:install` (the repo's built-in installation script builds a tarball then installs via `file:E:/...tgz` into web profile) or using pre-built `dsh-file-mount.tgz` from GitHub Releases. After installation, you must restart harness — just refreshing the web page won't activate it.

## Configuration Options
| Config | Type | Description | Default |
|---|---|---|---|
| `enabled` | boolean | Master switch; when off, all reads pass through natively, ledger stops writing entirely | `true` |
| `capacity` | integer | Capacity limit for file identity cache (mtime+size+sha256); mounted files are pinned by reference count and not subject to eviction | `32` |
| `ttlMs` | integer | Cache safety valve: fallback re-read interval (ms) when mtime+size unchanged but content actually changed | `300000` (5 minutes) |
| `maxPinnedFiles` | integer | Maximum number of mounted files to pin in a single session (LRU evicts least recently used when exceeded) | `256` |
| `minSavedTokens` | integer | If net savings fall below this token count, skip this deduplication/increment; pass through natively, don't write to ledger, and don't count toward safety valve | `16` |
| `maxFingerprintBytes` | integer | Files exceeding this byte size don't retain line-level drafts; can only remount entire file on changes | `1000000` |
| `maxManagedBytes` | integer | Files exceeding this byte size are not managed at all, passed through as-is | `16777216` (16 MiB) |
| `excludeGlobs` | string[] | These paths always pass through as-is, no ledger or dashboard | `[]` (typical: `['**/node_modules/**']`) |
| `statsFile` | string | Optional: JSON file path for cross-session total ledger; host can read via `ctx.fileMount.stats()` | Not set |
| `freshnessEnabled` | boolean | Whether to enable "freshness" heuristic: when context nears window limit, mark earlier segments as expired and resend on next read | `true` |
| `freshnessThreshold` | number | Segment position below this threshold (0..1) is marked as expired | `0.6` |
| `safeRatio` | number | When current context / window is below this ratio, pressure is considered 0, no expiration | `0.95` |
| `safeTokens` | number | Optional: absolute safe token ceiling; if set, overrides `safeRatio` | Not set |
| `pinAfter` | integer | After a segment is marked expired this many times, pin it — only resend at most once | `1` |
| `contextWindow` | integer | Default context window token count used when session doesn't report window size | `128000` |
| `resendBudget` | number | Optional: segments estimated larger than this token count won't be removed from ledger even if expired, to avoid stuffing large segments back into context | Not set |
| `valveReads` | integer | After N consecutive full-coverage deduplications, the next read passes through native result and refreshes related segments (failsafe; 0=off) | `2` |

> Configuration is written under the `config` node at the same level as `cordis.patch.yml`; schema validation and defaults are at `src/index.ts:103-121`.

## FAQ

**Q: What's the relationship between this plugin and DSH's built-in session compaction/context truncation?**

A: They don't conflict and don't replace each other. Session compaction solves "how much original history fits in the current window," while dsh-file-mount solves "the same file content repeatedly stuffed into the window" — the former looks at context, the latter looks at file ledger; the two directions are orthogonal and can be enabled simultaneously. When compaction occurs, old mount messages shadowed by it are automatically removed from the ledger and re-anchored on next read.

**Q: Why did the Web UI's read card become a generic card when reading files, losing the original file highlight/code block?**

A: This is expected behavior. The plugin replaces the model-visible result text in `tools/post-execute` (deduplication marker or incremental body), and the UI's read card is rendered based on the result text, so it degrades to a generic version; the canonical value remains intact, unaffected downstream audit logs.

**Q: After uninstalling or disabling the plugin, is my mount ledger still there?**

A: The in-memory ledger is released when the session ends; no ledger files are stored on disk (unless `statsFile` is actively configured). When re-enabled, the ledger replays from the current session's injected message `source` fields — so if closed and reopened quickly, data is continuous; if you want to preserve cross-session cumulative stats before closing, you must first configure `statsFile`.

**Q: The "Line Range" list for a file in the dashboard is too granular, hard to view — what can I do?**

A: Click the collapse arrow on the left of the file lines to collapse all line segments for that file; the top bar provides search (fuzzy path matching) and sorting (by net savings or by path). Color ribbons only distinguish tiers and can't adjust thresholds — all freshness thresholds (`freshnessThreshold` / `safeRatio` / `safeTokens` / `pinAfter`) are adjusted in host configuration, not the dashboard.

**Q: What if line-level diff misses changes during incremental line supplementation?**

A: Silent misses don't happen. The plugin uses stat validation (mtime+size fast path + sha256) to confirm file identity; whenever file content changes, it branches on hash inconsistency; when line-level drafts are lost or changes are too large, it automatically falls back to "full remount" rather than abandoning the file. So misses can only happen in ① files exceeding `maxFingerprintBytes` without draft retention, or ② extreme cases where mtime+size unchanged but content actually changed — the latter is covered by the `ttlMs` safety valve.

**Q: Can case sensitivity, soft links, or relative paths cause misidentification?**

A: The ledger internally uses "absolute path + realpath + case folding (based on actual filesystem, Windows/default macOS fold, Linux doesn't)" as the unique key, which won't be circumvented by soft links or relative paths; the slip the model sees uses working-directory-relative paths (forward slashes), with working directory preferring session `header.cwd`, falling back to `dsh-fs-local`'s `cwd` if none.

## Learning Curve
Advanced — the default configuration works out of the box; ordinary users can benefit without understanding ledger, hash, or line-level diff; but to make dashboard metrics match expectations (like `valveReads`, `pinAfter`, `safeRatio` thresholds), you need to first understand DSH's "tool result → model context" chain and Cordis configuration mechanism.

## Known Issues & Limitations
- After compaction, "mounted" guarantees become invalid: mount content shadowed by checkpoint has left the model context, the plugin identifies and removes it via `sourceEventSeqs`, and re-anchors on next read; the browser UI's shadow list has no channel to sync, so old rows remain until the file's next remount
- Incremental/dedup/remount all replace tool result text, so web-side read cards degrade to generic cards (canonical value intact, audit unaffected)
- Depends on read/write/edit tool canonical value structure; if DSH changes these shapes, the plugin guard degrades to native passthrough and injects a warning into the session on first trigger (src/index.ts:872-885). Shape changes are pinned by integration tests
- Files exceeding `maxManagedBytes` (default 16 MiB) and paths matching `excludeGlobs` are not managed, passed through as-is; the plugin doesn't do sampling fingerprints — sampling has "changed but not detected" risk
- rc.6's custom session event types can't be safely persisted (persistence read path hard-rejects unknown types), so the ledger carrier uses structured `source` fields on standard `user/message` events instead of new custom event types
- "Freshness" is heuristic: segment expiration doesn't mean content actually left context (only compaction does), but rather "attention has decayed, model basically can't see it" — so expired resend is intentional token overhead; sessions without usage data (like some adapters) show gray "unknown" and don't expire
- Browser-side conversation view is paginated history window (default tail page 50 messages, loads earlier pages on scroll up), dashboard folding accumulates across snapshots, mount messages scrolling out of window still retain that file's rows in the list until the next remount
- Dashboard features "click line to jump back to chat," cross-session total ledger display, and "file changed" real-time badge not yet implemented (no corresponding channel on browser side) — see config or logs for details
- Installation note: On Windows, `dsh plugin add .` includes the drive letter in the profile path causing the plugin to install but not activate; must use `pnpm dsh:install` or pre-built tarball; `npx @deepseek-ai/dsh` may have long no-output on first run (it's pulling the full CLI package)

---

This document is auto-generated by [deepseek-plugin.org](https://deepseek-plugin.org). HTML page: [dsh-file-mount](https://deepseek-plugin.org/plugins/acefun29/dsh-file-mount)
Wiki generated by AI (model: `MiniMax-M2.5`)
