Spill Storage
The spill storage seam — a capability seam that persists a tool's oversized text and returns a model-facing locator plus retrieval guidance, split across packages: Service Definition (dsh-spill, ctx.spillStore), Service Provider (dsh-spill-local, private session-scoped files on the host filesystem), and Consumer (dsh-spill-policy, the tools/post-execute policy). Spill is one optional capability, not part of the agent-loop spine — so its vocabulary lives here, not in core.md. Preview mechanics stay in dsh-output-retention; this seam only saves the final text the policy hands it.
Source: packages/spill/spill/src/types.ts
The save request
saveText is the sole service operation: persist content verbatim, return an opaque locator, a backend-supplied retrieval hint, and the exact byte count. The request carries the save-time storage namespace (owner), the tool and call that produced it (source, used for naming and inspection — not access control), and a suggestedName the backend may use as a naming hint (it is not a path).
/** One request to persist text to a spill artifact. */
interface SaveTextSpill {
owner: SpillOwner
source: SpillSource
/**
* A caller-suggested base name (e.g. `web_fetch.txt`). The backend sanitizes
* it to a single safe path segment before use — it is a hint, never a path.
*/
suggestedName: string
/** The full text to persist (UTF-8). */
content: string
}/**
* Save-time storage namespace for a spilled artifact. The session id lets a
* backend group storage under the producing session, but the returned
* {@link SpillLocator} is the model-facing handle. Forked sessions inherit
* locators already present in the seeded log; those artifacts are not copied or
* re-owned, and spills produced after the fork use the child session id.
*/
interface SpillOwner {
sessionId: SessionId
}SpillOwner.sessionId is the save-time storage namespace. Forked sessions inherit existing spill locators from the seeded log; those artifacts are not copied or re-owned, and spills produced after the fork use the child session id. A retention-period cleanup may expire old locators with other old session artifacts; the spill seam does not define a per-session cleanup policy.
/**
* Tool and call that produced one spilled artifact — recorded by the backend for a readable
* filename and inspection. Not interpreted for access control; purely
* descriptive.
*/
interface SpillSource {
/** The tool whose result was spilled (e.g. `web_fetch`). */
toolName: string
/** The model-issued call id the result belongs to. */
callId: CallId
/** A short human label for the artifact (e.g. `result`). */
label: string
}The result
/** A saved spill artifact: its locator, byte length, and backend-specific retrieval guidance. */
interface SpillRef {
locator: SpillLocator
bytes: number
retrievalHint: string
}SpillLocator is a branded model-facing handle returned by the backend. The local backend renders it as a filesystem path; a remote or database backend can render a URI, key, or command token. Consumers treat it as opaque and render it with retrievalHint instead of assuming read is always the right retrieval mechanism.
/**
* Opaque model-facing handle for one spilled artifact. A local backend may use a
* filesystem path; a remote or database backend may use a URI or key. Consumers
* render it with {@link SpillRef.retrievalHint}, but do not parse it.
*/
type SpillLocator = Branded<'SpillLocator'>The service
SpillStore (ctx.spillStore, defined in packages/spill/spill/src/index.ts) is a one-method abstract service: saveText(input) → Promise<SpillRef>. It persists the FULL content and REJECTS on a real storage failure (permissions, ENOSPC, backend unavailable). The seam owns storage only: no retention policy, no tool-result replacement, no retrieval/search API.
The local backend (dsh-spill-local) writes under <root>/session-<hash>/<random>-<safeName> — a configured or lazily-created private (0700) root, a sha256(sessionId) session subdir, and an exclusive owner-only (open(path, 'wx', 0o600)) write so a planted symlink cannot redirect it. Its locator is the local path and its retrievalHint tells the model to use read or grep on that path. The policy consumer (dsh-spill-policy) replaces an over-maxInlineBytes plain-text final result with a retention-library head/tail preview plus the spill reference, best-effort: a save failure keeps the original inline result rather than turning a successful call into an isError.
Cordis API
Generated from source by scripts/gen-cordis-catalog.ts (verified fresh by pnpm run verify-cordis-catalog in doc-sync; regenerate with pnpm run gen-cordis-catalog) — this section is byte-identical in both language sides of the page. Signature blocks use a ts cordis-catalog fence and keep the original source JSDoc; dispatch modes are defined in the primer, and the framework-inherited ctx API lives in cordis-api/inherited.md.
ctx.spillStore — SpillStore (abstract seam)
Abstract spill storage service. Subclass, implement saveText, and load the subclass as a plugin — it registers as ctx.spillStore (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior).
Semantics every implementation must honor:
- saveText persists the FULL
contentverbatim and returns an opaque locator, exact byte length, and model-facing retrieval guidance. - Storage is scoped by the request's SaveTextSpill.owner session; the backend chooses a private (not world-readable) location and a collision-free name derived from — never equal to — the caller's
suggestedName. saveTextREJECTS on a real storage failure (permissions, ENOSPC, backend unavailable); the caller decides how to degrade (the spill policy treats a rejection as best-effort and keeps the inline result).
/**
* Persist `input.content` to a session-scoped spill artifact.
* @param input - the owner, caller-supplied source fields, suggested name, and full text to save.
* @returns the saved artifact's {@link SpillRef}; rejects on a storage failure.
*/
abstract saveText(input: SaveTextSpill): Promise<SpillRef>