DSH plugin client resources: dsh-resource:// and useResource
The DSH plugin client resource model turns one address into live data any Web component can read: the package owning the content registers a protocol provider, and components subscribe by address. Its key constraint is that holders control lifetime — a subscribed useResource or a single pin keeps a resource alive; the first holder opens the provider's stream, and the last one to leave aborts it, drops the value and returns the snapshot to loading or none. That is also why unloading a tab body does not close its stream: the right sidebar pins each open tab's address for the tab's lifetime.
DSH plugin resource model: addresses, providers and readers
Bottom line: protocols own value types and register a single provider; components only know addresses.
ctx.resources: the resource service and theuseResourceglobal hook.- Protocol declaration: owners declare value types on
ResourceProtocolMap. - One provider per protocol: a second registration throws.
- Readers: components read the current state by address without knowing the producer.
- First consumer: right sidebar tabs.
DSH plugin dsh-resource:// syntax: protocol as host
Bottom line: dsh-resource://<type>/…, the host must be a protocol key, and only canonical lowercase spellings are accepted.
| Address | Reads as |
|---|---|
dsh-resource://file/session/s1/notes/a.md | File metadata under session s1's workspace |
dsh-resource://file/absolute/home/me/n.md | Absolute path; fails without an authorized session |
dsh-resource://subagentchat/session/c1?... | An addressed subagent conversation |
sidebar://guide | none: a navigation address, not a resource |
/home/me/n.md | none: not a URL |
- file protocol scoping:
session/<id>followed by a relative path, orabsolutefollowed by a path without its leading slash. - Construct and parse: use
fileAddressFor(sessionId, cwd, path)andparseFileAddress(address). - Read only scheme and host:
protocolOf(address)returns the lowercase host. - String comparison: case variants are different records, so never hand-assemble addresses.
DSH plugin provider registration: a frame stream that honors signals
Bottom line: open returns a frame stream, the first frame is current state, and abort must stop it.
ctx.effect(() => ctx.resources.register<'note'>({
protocol: 'note',
async *open(address, { signal }) {
yield await ctx.remote.notes.read(id, signal)
for await (const change of ctx.remote.notes.follow(id, signal)) yield change
},
}))
- First frame is state: do not wait for the first change.
- Failure is a frame:
ok: falsewith aRemoteFailure; throwing in the stream is a programming error. - Honor abort: a provider ignoring
signalruns until its next frame. - Registration timing: keep it in an effect so the protocol lives with the plugin.
- Already-held addresses: registering opens their streams immediately; after the provider disposes they read as
none.
DSH plugin reads: the four useResource states
Bottom line: none, loading, live and failed; failed keeps the last successful value so stale content can stay visible.
| status | Meaning | value | failure |
|---|---|---|---|
none | No provider, or not a resource address | undefined | undefined |
loading | Stream open, nothing emitted yet | undefined | undefined |
live | Latest frame succeeded | Latest value | undefined |
failed | Latest frame reported failure | Last successful value | That frame's failure |
- Subscribing is holding: a mounted component reads the latest value immediately if another holder keeps the resource open.
- Failure presentation is yours: the model produces no user-facing copy.
- Type parameter names the protocol:
useResource<'file'>(address)types the value. - Most streams carry metadata: the file protocol returns file stats while content comes from another read.
DSH plugin holding and release: subscriptions and pinning
Bottom line: a resource lives while held; the last holder leaving aborts the stream, drops the value and returns to loading or none.
- Two ways to hold: a subscribed
useResource, orctx.resources.pin(address, signal). - Pin semantics: keeps it open without subscribing until the signal aborts; an already-aborted signal pins nothing.
- Sidebar usage: each open tab pins its address for the tab's lifetime, so switching tabs does not close streams.
- Release consequences: abort the stream, drop the value and return to
loading(with a provider) ornone; later frames are discarded. - Bare observable:
ctx.resources.source(address)is reference-stable per address, and reading its snapshot alone is not holding.
DSH plugin client resource troubleshooting
Bottom line: separate wrong address, missing provider and no holders.
- Always none — the protocol has no provider, or the address is not a resource address (typos read as none too).
- Always loading — the provider emitted no first frame, or all holders left.
- Blank after failure — the consumer ignored
failed; keep showing the stale value with a notice. - Stream dies when switching tabs — the address was not pinned.
- Growing memory — each address record persists for the page, so reading many distinct addresses costs memory.
- Repair status, stated honestly: the address rules, frame contract, four states and holding semantics follow the official Client resources page; confirm file protocol and Remote namespace details against the package docs.
Sources: Client resources, dsh-client-resources README, Right sidebar.
FAQ
In a DSH plugin resource address the host names the protocol and must be a ResourceProtocolMap key, while the path belongs to the protocol and is percent-encoded by its owner. Adding a protocol adds a host, never a scheme.
A DSH plugin provider's open returns a RemoteResult frame stream: the first frame is current state and each change is another frame. Failure is an ok:false frame; throwing inside the stream is a programming error and is not caught.
Yes: the DSH plugin failed state keeps the last successful value alongside the failure, so consumers can show stale content with a notice. The next success clears the failure, and the consumer owns the copy.
A DSH plugin resource lives while it has holders: a subscribed useResource or a pin. When the last holder leaves, the stream aborts, the value is dropped, and the snapshot returns to loading with a provider or none without one.
No: DSH plugin treats an unknown protocol or malformed address as none with no extra diagnostics. Only wiring errors such as no tab type claiming an address throw.
Related Terms
- dsh-resource:// address
- dsh-resource:// is the DSH plugin resource address scheme where the host is a protocol key and the rest of the path belongs to that protocol. Addresses compare as strings and only canonical lowercase spellings are accepted.— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/client-resources
- ResourceProtocolMap
- ResourceProtocolMap is the protocol-to-value-type map where protocol owners declare their types through declaration merging and then register exactly one provider in an effect.— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/client-resources
- useResource
- useResource is the global DSH plugin hook returning the current snapshot for an address with status none, loading, live or failed. Subscribing is itself a way to hold the resource.— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/client-resources
- pin
- pin keeps a resource open without subscribing until the supplied signal aborts. An already-aborted signal pins nothing, and the resource is released only when the last holder leaves.— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/client-resources
Sources
- DeepSeek Harness docs — Client resources (addresses, providers, reads, pinning and limits)· deepseek-harness.github.io
- dsh-client-resources package README (providing a protocol and reading resources)· deepseek-ai (GitHub)
- DeepSeek Harness docs — Right sidebar (first consumer of the resource model)· deepseek-harness.github.io