Status: implemented for the local dsh-sessions workflow.
This document defines how DeepSeek Harness session logs are stored, discovered,
and inspected by agents in this repository. The design deliberately keeps the
session log as a boring, ordinary filesystem artifact so agents can use the
existing read, grep, rg, and bash tools instead of a second conversation
reader.
The session-log design has four goals:
- Keep DSH's built-in event-sourced session model authoritative.
- Store logs in a directly readable text format.
- Give agents a stable path to each session log through
session_status. - Avoid duplicating DSH's persistence, replay, or message-reconstruction code
in
dsh-sessions.
The design does not provide a second session database, a custom transcript format, or a pretty-printed conversation projection.
DSH owns the session lifecycle and persistence services. The plugin is an adapter over those services:
Agent / session tools
│
▼
dsh-sessions
│
├── ctx.agents live session state
├── ctx.sessionPersistence durable event log and artifact path
└── ctx.sessionQuery durable title observations
The session event log remains the source of truth. dsh-sessions does not
maintain a parallel index or copy the log into another file.
The implementation is split across:
sessions/src/service.ts: merges live and persisted sessions and exposes the backend-owned log path;sessions/src/tools/session-status.ts: registers the model-facing status tool;sessions/src/creation-service.ts: creates fresh sessions and queues their initial prompt;sessions/cordis.patch.yml: configures the plain JSONL persistence backend.
The local profile uses the JSONL persistence backend with this configuration:
- id: session-persistence-jsonl
config:
root: !!js dshHomePath('sessions-plain')
compression: none
packChunks: falseThe resulting layout is:
~/.dsh/sessions-plain/
└── <encoded-project>/
└── <encoded-session-id>/
└── session.jsonl
The actual absolute path is backend-owned and is returned by DSH's public
sessionPersistence.locate() seam. The plugin does not reconstruct the path
from the session ID itself.
compression: none means the file is UTF-8 JSONL rather than a
session.jsonl.zstd Zstandard stream. packChunks: false independently keeps
each persisted event as its own JSONL record. The latter is not required for
readability, but makes rg and line-oriented inspection easier.
The settings are independent:
| Setting | Value | Effect |
|---|---|---|
compression |
none |
Writes plain .jsonl instead of .jsonl.zstd. |
packChunks |
false |
Does not combine consecutive streaming chunk events. |
The log remains lossless. Disabling compression does not remove events, summarize messages, or change the event model.
Project and session identifiers are encoded into safe single path segments by
the host JSONL backend. Agents should use the returned session_path value as
opaque data and should not derive a path by concatenating an untrusted session
ID.
Each file is an append-only sequence of JSON objects. The first record is the session header. Later records are durable session events.
A header has the following shape conceptually:
{
"type": "session",
"version": 0,
"id": "session-...",
"createdAt": 1760000000000,
"cwd": "/path/to/project",
"delegationDepth": 0,
"agentPreset": "ephemeral-ai-harness"
}Event records carry the event sequence, timestamp, event type, surface operation, and event-specific data. A simplified example is:
{
"seq": 0,
"time": 1760000000001,
"type": "user/message",
"surfaceOp": "append",
"data": {
"id": "message-1",
"role": "user",
"content": [{"type": "text", "text": "Inspect the project."}],
"source": {"kind": "user"}
}
}The exact event vocabulary belongs to @deepseek-ai/dsh-session. Consumers
should treat unknown event types as forward-compatible data rather than
assuming that every record is a user or assistant message.
session_status is inspection-only. It does not resume a cold session, invoke
the model, or mutate persistence.
session_status({ recent_n?: number })
session_status({ session_id: string })Each result row has this shape:
interface SessionStatusView {
session_id: string
title?: string
status: 'running' | 'idle' | 'cold' | 'missing'
updated_at?: string
session_path?: string
}The plugin merges:
ctx.sessionPersistence.list()for materialized persisted sessions;ctx.agents.list()for live sessions;ctx.sessionQuery.readTitleSnapshots()for durable titles;ctx.sessionPersistence.locate()for backend-owned artifact paths.
Duplicate IDs are merged with live state taking precedence. Status is mapped as follows:
| Condition | Status |
|---|---|
| Live agent currently processing | running |
| Live agent exists but is not processing | idle |
| Persisted session has no live agent | cold |
| No live agent or persisted header for an exact lookup | missing |
The default list is the 50 most recently updated sessions. Exact lookups return one row and do not apply the recent-session limit.
session_path is omitted when the active persistence backend does not expose a
per-session artifact. The JSONL backend does expose one, so materialized local
sessions normally return an absolute .jsonl path.
The intended workflow is:
1. session_status({ recent_n: 50 })
2. Select a row's session_path.
3. Use ordinary filesystem tools on that path.
Examples:
SESSION_LOG='/Users/example/.dsh/sessions-plain/project/session-123/session.jsonl'
# Search for user prompts, tool calls, or a particular error.
rg -n 'authentication|tool-call|error' "$SESSION_LOG"
# Read the raw event stream.
read "$SESSION_LOG"
# Inspect selected JSONL records when jq is available.
rg '"type":"user/message"|"type":"tool/' "$SESSION_LOG" | jq .The exact command depends on the available tool surface. The important rule is
that the path comes from session_status; agents must not guess the storage
root or decode the session ID into a filesystem path.
The plain JSONL file is intentionally raw. It may contain:
- session metadata;
- user and assistant message events;
- tool-call and tool-result events;
- reasoning or streaming chunk events;
- lifecycle boundaries;
- request metadata and future event types.
This is different from a pretty conversation transcript. That tradeoff is intentional: ordinary tools can search the complete durable record without the plugin owning a second projection or hiding trace data. Agents that need a conversation summary can inspect the relevant event records and summarize them in the current turn.
session_create creates a fresh DSH agent and queues its first user message. It
returns after the target agent accepts that message:
session_create({
prompt: string,
preset?: string,
model?: {
provider: string,
model: string,
reasoningEffort?: string,
},
cwd?: string,
})The selected cwd is canonicalized before it is written into the session
header. When a child is created by an existing agent, the parent location and
model/preset context may be inherited according to the creation contract in
sessions/SPEC.md.
Persistence may materialize a session lazily. A path returned by the backend is a target location, not a guarantee that the file already exists at the exact instant an agent receives it. Agents should handle a short write-in-progress window and retry a read if necessary.
The host persistence backend owns durability and recovery:
- events are appended in sequence order;
- writes are synchronized before they are considered durable;
- interrupted tails are handled by the persistence coordinator;
- the session header and event log are validated by the host backend;
- consumers must not edit, reorder, or delete records in place.
Agents should treat session logs as read-only inspection artifacts. To change a
session, use the supported DSH agent/session APIs rather than modifying
session.jsonl with a text editor.
The path is a convenience for authorized filesystem tools, not an authorization mechanism. It may expose prompts, tool arguments, tool results, or other sensitive workspace information; normal filesystem permissions and the DSH tool policy still apply.
The former local root is:
~/.dsh/sessions
It contains compressed session.jsonl.zstd artifacts. The new design uses a
separate root:
~/.dsh/sessions-plain
This separation is deliberate. The JSONL backend does not silently mix
.jsonl and .jsonl.zstd artifacts in one root, and the plugin does not
automatically rewrite the existing logs. Existing compressed sessions therefore
require an explicit, validated migration before they can be included in the
plain-log root.
A future migration utility must:
- stop or quiesce writers;
- decompress each source artifact;
- validate the header and event sequence;
- write the destination artifact durably;
- verify the destination by reopening it through DSH persistence;
- retain the original compressed artifact until verification succeeds.
Renaming .jsonl.zstd to .jsonl is not a valid migration.
The previous plugin-owned session_read tool reconstructed live and cold
sessions into a bounded, pretty-printed message surface. That duplicated part
of DSH's session projection and hid raw event information.
With plain, unpacked JSONL and session_path discovery:
- the standard filesystem tools can search the complete record;
- no separate bounded reader needs to stay in sync with DSH message semantics;
- no browser-specific tool renderer is required;
- persistence remains owned by DSH;
- the plugin surface is reduced to discovery and creation.
The cost is that agents see raw events rather than a preformatted transcript. That is acceptable for the current inspection and automation workflows.
After installing the package into a profile:
- Run
dsh --profile web --dump-config. - Confirm
session-persistence-jsonlusessessions-plain. - Confirm
compression: noneandpackChunks: false. - Start a new DSH Web session.
- Call
session_statusand confirmsession_pathends insession.jsonl. - Confirm the file is readable as UTF-8 JSONL.
- Search the file with the ordinary
rg/read/bashtools. - Confirm the model tool list contains
session_statusandsession_create, but notsession_read. - Confirm the DeepSeek Harness checkout has no modified files.