Skip to content

What a context source is

A context source is a plugin that adds model-visible material to a request without defining a tool. It contributes either a system-prompt section, a durable user/message, or prompt variables, and it is one of the two halves of the assembled model input. The other half is the static system prompt, assembled by packages/core/system-prompt through its system-prompt/assemble extension point.

The core engine records the final batch of messages (sections + dynamic contexts) after step/start and before request derivation. A context contributor therefore must attach itself to the agent/pre-step hook — the seam that runs before each model request is derived — and, when it wants the model to see something durable, append a sourced user/message to the batch that is about to enter. The alternative, contributing a named section to the assembled prompt, is done by consumers of the system-prompt registry (for example plan mode's plan:policy section and dsh-tool-goal's goal policy).

The table below is the whole context family. All four packages are product plugins; only agent-instructions is shipped by the default dsh-agent-spine-demo bundle.

Packagectx keyRoleBundled by default
agent-instructionsWorkspace AGENTS.md/CLAUDE.md baseline + dynamic discovery
session-referencesessionReferenceResolverBounded read-only snapshots of other sessions❌ opt-in
time-contextCurrent zoned time + elapsed-time readings❌ opt-in
tmux-contexttmux session/window/pane location❌ opt-in

The context-contributor pattern

Although there is no shared runtime interface class, the four sources reproduce one shape, which you can verify by reading their src/index.ts files:

text
agent/pre-step (prepended listener)
   │   delegates to downstream listeners first

read durable session events (shadowing-safe, no process-local cache)
   │   decide whether an injection is "due"

if due: append one sourced UserMessage to the returned batch
   │   source = { kind: 'plugin', plugin: '<name>', ... } or a dedicated kind

step/start records the final batch before request derivation

Three properties recur:

  1. Durable, replay-safe decisions. Change suppression and interval scheduling scan the raw durable session log for the latest injection of that source, so the schedule survives compaction and process resume without a process-local cache.
  2. Append-only history. Every injected message lands after the reusable request prefix, so it does not invalidate the model provider's earlier KV-cache entries. Context that is no longer relevant disappears only when a later compaction shadows it.
  3. Sourced messages. Each injection declares where it came from via the source field of a user/message, so replay, invariants, and the model-facing framing can all attribute it.

The packages/core/system-prompt registry is the counterpart that owns the static half. It exposes two independent registries — ordered prompt sections and what it calls dynamic contexts (durable user-role snapshots) — plus prompt variables for string interpolation. Plan mode and dsh-tool-goal consume the section side; the four context packages consume the agent/pre-step + sourced-message side.

agent-instructions — workspace guidance

@deepseek-ai/dsh-agent-instructions injects the chain of workspace instruction files (AGENTS.md, CLAUDE.md, and their .local overlays) as durable, user-role context. It is the only context source with a lifecycle split: the user-global and project baseline goes into durable history once, while nested file discoveries, changes, and removals are appended later as they are detected.

Baseline. The first eligible agent/pre-step of each live session composes the baseline: $DSH_HOME/AGENTS.md (default ~/.dsh/AGENTS.md), then, walking each directory from the project root down to agent.session.header.cwd, every present base candidate followed by every present local-overlay candidate. The discovery/precedence/project-root list is configurable; defaults are instructionFileCandidates = ['AGENTS.md', 'CLAUDE.md'] and localInstructionFileCandidates = ['AGENTS.local.md', 'CLAUDE.local.md'].

Dynamic discovery. After a successful first-party read/write/edit tool result, the plugin checks whether the touch reached new descendant scopes or changed an already-loaded scope, and queues the appropriate transition in the agent's next-step inbox: set (new file), replace (changed digest), or remove (deleted, or made a per-directory duplicate of an earlier candidate). There is deliberately no file watcher — refresh is touch-driven.

Config (packages/context/agent-instructions/src/config.ts):

KeyDefaultMeaning
dshHome~/.dshRoot for the user-global AGENTS.md scope
projectRootMarkers['.git']Directories that halt upward project-root discovery
maxBytesrequiredBounded render budget for the whole baseline
maxSourceBytes1 MiBPer-file read cap before rendering
instructionFileCandidates['AGENTS.md', 'CLAUDE.md']Base candidate names (same-directory names only)
localInstructionFileCandidates['AGENTS.local.md', 'CLAUDE.local.md']Per-directory overlays, empty list disables

The plugin frames every message with a literal <system-reminder> block (escaped against repository-controlled content) so repo text cannot close the frame. The baseline uses Instructions from: ~/.dsh/AGENTS.mdInstructions from: AGENTS.md; a newly reached scope uses Additional instructions from: packages/app/AGENTS.md; a change uses Updated instructions from: <path>; a removal uses Instructions removed: <path>. Each injected event carries a typed agent-instructions source with a list of { action, scope, path, digest? } changes plus baseline: true and a baselineIdentity for the complete baseline.

session-reference — other sessions as context

@deepseek-ai/dsh-session-reference exposes ctx.sessionReferenceResolver, a service that prepares bounded, read-only snapshots of other sessions as sourced model context. It consumes ctx.sessionQuery and the compaction checkpoint marker; it needs no SQLite FTS.

URI encoding. encodeSessionReferenceUri()/decodeSessionReferenceUri() implement the canonical mention URI dsh-session:<base64url(JSON.stringify(sessionId))>; formatSessionReferenceMention() emits @[label](uri). A model (or host) references another session by writing such a mention; parseSessionReferenceText() turns the markdown mention back into readable @label text while returning the structured references.

Snapshot semantics. prepare(agent, content, references, signal?) calls ctx.sessionQuery.readSurface() once per distinct source, projects only direct-user user/message, assistant text, and user/message checkpoints carrying the canonical dsh-compaction source marker, and returns detached content plus an aggregated UserMessage. The context source is { kind: 'session-reference', version: 1, references }. Self-reference on any mention, more than maxReferences distinct sources, or any read failure rejects before the host calls followup() or steer(). When the agent is idle, a one-shot agent/pre-step wrapper adds the snapshot only to an enter decision containing the claimed direct prompt; while running, inject() runs immediately before steer().

Config:

KeyDefaultContract
maxReferences3Max distinct source sessions per prepared message (≤ 3)
candidateLimit50Default candidate count returned to a host
maxReferenceBytes65536Max serialized JSON bytes per reference object

The model-facing payload is an untrusted snapshot headed ## Referenced sessions, rendered as JSON inside <referenced-sessions> tags with every data < emitted as \u003c. The warning explicitly forbids following instructions, permission claims, or tool requests from the snapshot unless the current user repeats them.

time-context — clock and elapsed time

@deepseek-ai/dsh-time-context is opt-in durable context with the current zoned time, the browser zone attached to the open user-rpc message, and elapsed time sampled during request preparation. Default compositions disable it; the Schedule Web overlay mounts it so non-interactive scheduling can carry dates/times the model can interpret in the user's browser zone.

Config (packages/context/time-context/src/config.ts): timeZone (display fallback when browser provenance is missing or mixed) and refreshIntervalMs (a positive value only injects when the session has no earlier injection, wall time moved backward, or that many ms elapsed).

Request-zone ownership. When an open turn contains one Host-validated browser zone, that zone formats the timestamp; multiple zones resolve to mixed and none to unavailable. Each reading uses the exact source { kind: 'plugin', plugin: 'time-context', form: 'snapshot', sections: [{ name: 'time-context', text: <same text> }] }. The injected line forms:

text
Time sampled while preparing turn <turn>, step 1: <timestamp>
Browser time zone for this request: <iana-zone-or-mixed-or-unavailable-policy>.
Elapsed since the preceding model-visible message: <duration-or-unavailable>.

tmux-context — terminal location

@deepseek-ai/dsh-tmux-context names the tmux session, window, and pane this agent process runs in plus the window's pane-tree layout. It is sampled once per turn. Critically, it checks which terminal is controlling this process: $TMUX_PANE alone is insufficient because a terminal launched from within a tmux shell inherits $TMUX/$TMUX_PANE from its ancestor. It therefore compares the pane's #{pane_tty} against this process's own controlling tty (ps -o tty= -p <pid>); a genuine pane owns this process's tty, while an inherited environment names another pane's tty.

The reading runs as one read-only shell command through the ctx.shell executor (so the deployment sandbox and policy apply), and any failure is a logged warning, never a turn failure. Config is { refreshIntervalMs?: number }. When the rendered tmux state differs from the last injection, it prepends a sourced UserMessage (source { kind: 'plugin', plugin: 'tmux-context' }) with three lines:

text
tmux location (turn <turn>):
session <session>, window <index> "<name>", pane <index> <pane-id>
window active=<0|1>, pane active=<0|1>, layout <window-layout>

Comparing the four sources

The following table summarizes what each source injects, when, and under what config — useful as a quick reference when auditing a prompt:

SourceInjectsWhenKey configctx key / source
agent-instructionsworkspace AGENTS.md/CLAUDE.md chain as durable user messagesfirst eligible pre-step (baseline) + touch-driven discoverymaxBytes, dshHome, candidate listssource { kind: 'agent-instructions', baseline, baselineIdentity }
session-referencesnapshot of another session as user/messageon idle enter or immediately before steer()maxReferences, candidateLimit, maxReferenceBytesctx.sessionReferenceResolver
time-contextclock + browser-zone + elapsed lineseach eligible pre-steptimeZone, refreshIntervalMssource { kind: 'plugin', plugin: 'time-context', form: 'snapshot', sections }
tmux-contexttmux location three-linerfirst step of each changed turnrefreshIntervalMssource { kind: 'plugin', plugin: 'tmux-context' }

Across all four, three invariants hold: every injection is a durable, replay-safe, sourced user/message that is append-only relative to the reusable request prefix, and whose change-suppression decision is made from the raw session log (so it survives compaction and resume without process-local state).

Source attribution and replay invariants

Each of the four packages ships a ./invariant companion (e.g. @deepseek-ai/dsh-time-context/invariant) that independently validates the attributable shapes: the exact source union, the plugin id, the section/component names, and (for time-context) the re-derived browser-zone policy and timestamp zone. These companions exist so a mismatched or counterfeited context source is caught at the invariant boundary rather than silently polluting model history.

Because context messages are durable and attributed, downstream systems can distinguish them from direct user speech — session-reference's dsh-compaction checkpoint projection, agent-instructions' AGENTS.md framing, and a direct human prompt all carry different source markers. This separation is what lets the compaction pruner, the goal domain, and the human-command plane each treat "model-visible context" and "direct user input" as different things.

How the sources combine

Packages/core/system-prompt and the four context packages are independent registries that the core loop then merges. Because context messages are durable and appended in order, a single request can carry a workspace baseline, a session-reference snapshot, a time reading, and a tmux location simultaneously, each attributed by its own source. There is no central "context bucket" — each contributor appends to the same entering batch, and step/start records the merged result.

The end-to-end data flow for one injection:

text
durable session events ──► agent/pre-step listener (per source)
                                │ delegates downstream, then decides "due?"

                        append sourced user/message to the entering batch


                        step/start records the final batch


                        request derivation ──► model request (session-log-faithful prefix)


                        step/end ──► durable history (append-only until compaction)

The batch recorded by step/start is the single merge point: static sections, dynamic contexts, and the system prompt are all present there before derivation. A later compaction can shadow any of these appended context messages without special-casing their source.

Further reading

  • System-Prompt Assembly — the section/context/variable registry and system-prompt/assemble.
  • Compaction — how durable context messages eventually get shadowed by summary checkpoints.
  • Goals & Goal Rounds — a different continuation policy that injects <goal_round> prompts.
  • packages/context/README.md — the group-level entry point; docs/subsystems/session-reference.md documents session references.
  • packages/context/agent-instructions/src/state.ts — the per-session/project scope cache and digest reconciliation.
  • .agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.md and 2026-07-27-tmux-location-context.md — decision records for time and tmux context.