dsh-upload-index-cache
Verified@argszero/dsh-upload-index-cache Β· v0.1.1 Β· MIT
Resolving a retained image to its DeepSeek file id re-reads and re-validates the whole upload index, once per image per HTTP attempt: DeepSeekUploadIndex.get() calls load(), which reads files-v3.json and runs its parser over every record. Measured on the
Install
dsh plugin add @argszero/dsh-upload-index-cache Confirm the layer applied with dsh --profile default --dump-config β see the install guide.
Source
Tags
Readme
@argszero/dsh-upload-index-cache
Retains the parsed DeepSeek upload index across lookups, so resolving the n-th retained image in a request does not re-read and re-validate the whole file for the n-th time.
One report, one measurement, one seam: #8814 β "Repeated parsing of the DeepSeek Files index slows image-heavy sessions".
The problem
Resolving a retained image to its DeepSeek file id costs a whole-index read and re-validation, per image, per HTTP attempt:
adapter.ts:95 prepareFileIds(messages, versions, files)
images.ts:99 for each image reference: await files.resolve(...)
request-files.ts:116 resolve β DeepSeekFileStore.ensureUploaded
file-store.ts:188 ensureUploadedOnce β (:200) this.index.get(scope, variantId, now, margin)
upload-index.ts:149 get() β (:123) load() = readFile + parse + validate + linear .find
Measured on the published 0.2.1-alpha.1 bundle, index = 500 records /
201 433 bytes, Node 26.5.0 on macOS:
| stage | cost | share of one lookup |
|---|---|---|
readFile |
31.67 ms | 13.5% |
JSON.parse |
74.13 ms | 31.7% |
| per-record validation | 126.39 ms | 54.1% |
linear .find |
1.53 ms | 0.7% |
stat |
5.40 ms | 2.3% |
| read + parse + validate | 232.19 ms | 99.3% |
The inflight map in DeepSeekFileStore deduplicates only while an upload
promise has not settled, so it does nothing for a session whose images are
already uploaded β which is every request after the first. A request carrying
five retained images reads and validates that file five times.
What this plugin does
It memoizes load() β the read and the parse β and re-reads only when the
file's identity changes:
- the identity is
(ino, size, mtimeMs), and the inode is the load-bearing field:writeFileAtomic(@deepseek-ai/dsh-atomic-write) renames a random-suffix sibling over the target, so every local write changes it. A key of modification time alone cannot promise to see a write that lands inside the same millisecond as a preceding read; the inode can, and does. - a lookup
stats the file, andstats it again before retaining a freshly parsed document. A read that raced a write is served β it is a consistent snapshot of some generation β but is not retained, so an older generation can never end up installed after a newer write finished.
It does not re-implement anything. The parser, the expiry rule, the file
lock, the atomic write, the v3 format and get()'s own linear scan are all
still core's, so the only thing a consumer can observe differently is how often
the file is read. That is also why the memo is correct for commit(), remove()
and clear() without knowing anything about them: all three build a new
records array with filter() and assign it to a local, so none of them
mutates the array the memo holds.
Install
npm install @argszero/[email protected]
Then mount it. The package ships a bundle patch
(dsh.bundle.patch: ./cordis.patch.yml), so a deployment that follows the
harness's bundle convention gets it by adding one entry:
- insert:
- id: upload-index-cache
name: '@argszero/dsh-upload-index-cache'
Mounting it directly works the same way and takes the same config:
import cache from '@argszero/dsh-upload-index-cache'
ctx.plugin(cache, { mode: 'observe' }) // measure before you cache
Measuring before caching
mode: 'observe' installs the same discovery and the same counters and patches
nothing. It answers the question a cache otherwise answers by assumption β
"how often is this read in my deployment" β and it is meant to be run first.
const cache = ctx.get('uploadIndexCache')
cache.snapshot()
{
"mode": "cache",
"counters": { "lookups": 41, "hits": 35, "misses": 6, "bypassed": 0, "unstored": 0, "errors": 0 },
"coverage": {
"adaptersSeen": 1,
"adaptersWithoutIndex": 0,
"passes": 2,
"covered": [{ "path": "β¦/llm-deepseek/files-v3.json", "source": "register-adapter" }],
"observed": [],
"skipped": [],
"notCovered": [
"An upload index that no registered adapter owns is invisible to this plugin: it finds live instances through the LLM registry.",
"Coverage is per instance. An index constructed after a discovery pass is covered by the next `llm/adapters-updated` event, not immediately.",
"Indexes larger than 8388608 bytes are read the ordinary way; they are reported as bypassed, not covered."
]
},
"settings": { "maxBytes": 8388608, "maxAgeMs": 0 }
}
notCovered is not decoration. A performance plugin that reports only its wins
leaves an operator unable to tell "the cache is working" from "the cache found
nothing to work on", and those two look identical in a latency graph.
cache.reset() zeroes the counters and drops every retained document.
Configuration
| option | default | meaning |
|---|---|---|
mode |
'cache' |
cache retains the parsed document; observe counts the same lookups and patches nothing; off installs nothing |
maxBytes |
8388608 (8 MiB) |
index files larger than this are read the ordinary way, every time. 0 removes the ceiling |
maxAgeMs |
0 |
re-read a retained document once it is this old even when the identity still matches. 0 makes the identity the only trigger |
onlyPaths |
[] |
cover only indexes whose path starts with one of these prefixes; empty covers every index the plugin can reach |
report |
true |
log the mount line and each index it starts retaining |
The default ceiling is 8 MiB because an index that has grown past it is a
deployment whose index deserves attention rather than one more silent cache. The
default age bound is 0 because the inode identity is the precise signal and a
second, blunter trigger only hides filesystem problems.
Where it attaches
DeepSeekUploadIndex is not a cordis service.
registerDeepSeekProvider() never passes resolveFiles, so
DeepSeekAdapter's constructor does this.files = new DeepSeekFileStore()
(adapter.ts:29) and the store's constructor does new DeepSeekUploadIndex() β
a private field of a private field. There is no ctx.get('β¦') that reaches it,
and no public accessor that returns a registered adapter.
Two seams, in order:
llm.registerAdapter(public). A method the deepseek host calls with the adapter it just built, so wrapping it yields the real adapter object. This covers adapters registered after the plugin mounts, and it is the seam the coverage report namesregister-adapter.- the registry's adapter map (private).
LlmRuntimekeepsadapters = new Map<string, AdapterRegistration>(); the field name survives compilation, so it is readable. It is guarded (instanceof Map, per-entry shape checks) and it is what covers adapters registered before mount, and adapters whose routes are replaced. The report names itadapter-registry.
Both are re-run on every llm/adapters-updated. Because the registry publishes
that event during the host's own registration call, the map pass usually
reaches a new adapter before the public seam can name it; the reported seam is
upgraded rather than left to depend on listener order.
A discovery that finds nothing is not an error: at mount time in a fresh process there may be no adapter yet, and the event says when there is one.
Honest boundaries
- An index no registered adapter owns is invisible to this plugin. It finds
live instances; it does not import the package to reach a class. Importing
would be the wrong trade twice over: a
profiledeployment is exactly the layout in which a second copy of a harness package can be materialized, and patching that copy's prototype would be a plugin that reports success while changing nothing.DeepSeekFileStorebuilt by hand, in a plugin or in a test, is not covered, and the report says so on every snapshot. - Callers of the memoized
load()share one parsed object. The unpatched method hands each caller a fresh parse; the memoized one hands back the object the first caller received, for as long as the file's identity is unchanged. Nothing in the deepseek package mutates whatload()returns βcommit(),remove()andclear()are the whole list of writers and all three are read-only over it β so this is unobservable today. It is still a real difference, it is the price of not paying the copy, and it is stated here rather than left to be discovered. - It saves a bounded fraction, not all of a lookup. The linear
.findinget()(0.7%) and thestatthe memo performs (2.3%) are still paid on every lookup. - It is not a cache of the remote files. It caches one local JSON file's parse. Expiry, quota handling and upload deduplication are untouched.
- It fails open. A
stator a bookkeeping error is logged once and the ordinary read happens. A cache whose bug froze every image lookup would be a worse failure than the cost it removes.
Compatibility
The plugin patches the instance's load() and reads the instance's path,
so the API it depends on is the shape of DeepSeekUploadIndex, reached through
@deepseek-ai/dsh-llm's registry. Peer ranges name the lines that were verified
by pinning every harness package to one line and running the suite against it
(npm run test:probe-lines), never inferred:
| peer | range |
|---|---|
@deepseek-ai/dsh-llm |
>=0.1.7-rc.2 <0.2.0 || >=0.2.0-rc.2 <0.2.1 |
@deepseek-ai/cordis |
^4.0.2 |
Verified against the 0.1.7-rc.2 and 0.2.0-rc.2 lines. Each || segment's
floor is the build that was actually run, so the range and the probed set are the
same set β a line this plugin has not been run on is a line it does not claim.
The target of the patch, @deepseek-ai/dsh-llm-deepseek, is not a peer: the
plugin never imports it (doing so is the second-copy failure described above),
so declaring it would be a dependency the artifact does not have. Its
DeepSeekUploadIndex β an own path string and a load() returning
{ formatVersion: 3, records } β was checked by source inspection on the
0.1.7-rc.2, 0.2.0-rc.2 and 0.2.1-alpha.1 lines, and the suite runs against
the 0.2.0-rc.2 build.
@deepseek-ai/dsh-llm is a peer even though the plugin imports it only for the
llm service's type: inject = ['llm'] makes the registry a hard requirement,
and mounting this plugin where there is no registry is not better than not
mounting it.
Tests
npm test # the full suite, against a real cordis context and a real DeepSeek adapter
npm run test:inject # mutate the source one rule at a time; every arm must be caught
npm run test:probe-lines # pin every harness package to one line and re-run the suite
The integration suite does not simulate the seam. It builds a real
DeepSeekAdapter holding a real DeepSeekUploadIndex at a temp path, registers
it on a real LlmRuntime, and proves the memo serves by making the index file
unreadable after the first lookup and watching the second one succeed anyway β
which is only possible if no read happened. The control arm runs the same
sequence with the plugin absent and watches the same second lookup fail with
EACCES.
License
MIT