dlgt CLI v1 reference
This reference is also published as raw Markdown at https://combinatrix.ai/dlgt/cli.md for agents to fetch with curl.
This document is the normative command reference. See RPC for the programmatic interface and Design for the product boundary, invariants, lifecycle rationale, security model, and acceptance criteria.
Product definition
dlgt is a local, single-binary runtime for live, addressable, and attachable Codex, Claude, Cursor, Grok, OpenCode, and Pi subagents.
The only public runtime object is a Session:
Session
One dlgt-owned harness process and PTY
One controller at a time
At most one active execution
No server-side queueProvider turns and execution receipts may remain internal for lifecycle correlation while the daemon is alive, but they are not public CLI resources and do not have public IDs.
Other terms:
Harness The provider adapter, codex, claude, cursor, grok, opencode, or pi
Profile A reusable client-side launch specification
Alias A human-readable address for an active Session
Title A non-unique human description used to generate an AliasIdentifier and naming model
Every successful new returns one provider-qualified Session ID:
codex:019f6307-341e-7e81-8a33-7ab61e804345
claude:8bc7859c-4c82-4b9a-a00d-2f3c483a9629The suffix is the provider's own Codex thread ID or Claude session ID. This single session.id is both the live dlgt address and the durable resume address. There is no separate provider Session ID or resume reference.
During startup only, dlgt correlates the not-yet-bound process with an internal:<short-id>. It atomically rekeys all retained state to the provider-qualified ID before accepting the first prompt or returning success. The internal launch ID is not a public Session ID; it may appear only as error.launch_id when startup fails before provider binding. That failed launch stays addressable for diagnostics: show, fetch, scrollback, and logs --raw accept the launch_id.
Aliases are for humans:
title: run review
alias: @run-review-361csxBy default, new slugifies the title and adds a random suffix. A caller may request an exact alias with --alias. An alias cannot be reused while owned by a starting, idle, busy, blocked, canceling, or stopping Session. It becomes available after that Session reaches a terminal stopped state. Historical records remain addressable by Session ID.
Automation should retain the returned Session ID and use it for all later commands. Aliases are ephemeral conveniences, not primary keys.
Output contract
All control-plane commands emit one JSON document to stdout.
Success:
{"ok":true,"session":{"id":"codex:019f6307-341e-7e81-8a33-7ab61e804345","state":"idle"}}Failure:
{"ok":false,"error":{"code":"SESSION_BUSY","message":"session already has active work","session_id":"codex:019f6307-341e-7e81-8a33-7ab61e804345"}}Failures also return a non-zero process exit status. stderr is reserved for failures that occur before dlgt can serialize a valid response, such as a panic or corrupted executable startup.
The exceptions are deliberate:
attach raw interactive terminal
logs --raw raw PTY bytes
help / skill text
rpc --stdio JSONL request/response streamPretty-printed JSON is opt-in through --pretty; default output is compact and deterministic.
Top-level help
dlgt - local subagent runtime
USAGE
dlgt <COMMAND> [OPTIONS]
DELEGATION
new Create a Session with its first prompt
restart Restart a Session
send Send work to an existing idle Session or --resume a provider conversation
fetch Read new state, results, events, and screen from a cursor
cancel Interrupt the active execution
SESSIONS
list, ls List Sessions
show Show Session state and latest result
attach Attach to the Session screen
stop Stop the Session
OBSERVABILITY
scrollback Read rendered terminal scrollback
logs Read raw retained PTY bytes (requires --raw)
CONFIGURATION
models Discover Harness models
profiles List or inspect Profiles
harnesses List Harness capabilities
doctor Diagnose the local dlgt installation
skill Print the embedded dlgt skill
RUNTIME
server Run or stop the daemon
update Install the latest release and embedded Skills
rpc Use JSONL RPCEach release uses its own socket at $DLGT_HOME/run/<version>/dlgt.sock. Most commands address the daemon for the invoking binary's version. send first scans all live versioned sockets for an exact provider-qualified Session ID and routes to its owning daemon; multiple matches fail rather than choosing or launching. dlgt list --all-versions queries every currently running version and annotates each Session with runtime_version and runtime_socket. Session state, results, events, and terminal history exist only while their owning daemon remains alive.
Busy Session snapshots include two integer diagnostics when state is exactly busy:
{
"state": "busy",
"busy_for_ms": 183000,
"pty_quiet_for_ms": 72000
}busy_for_ms measures from reservation of the current turn. pty_quiet_for_ms measures PTY silence, clamped to that same busy interval so output from an earlier turn cannot inflate it. If no PTY output has occurred, it equals busy_for_ms. These counters are diagnostic only; PTY silence never changes Session state or makes a busy Session accept another prompt. Both fields are omitted for every other state.
Successful commands may include an optional non-error notice:
{"ok":true,"sessions":[],"info":{"code":"UPDATE_AVAILABLE","current_version":"0.1.4","latest_version":"0.2.0","command":"dlgt update"}}Each long-lived versioned daemon performs the first update check asynchronously at startup and repeats it every six hours. Transient check failures preserve the last successful notice; a successful check with no newer release clears the notice.
dlgt update verifies the release's attested checksum manifest, then installs the archive through the installer embedded in the running binary. It never downloads executable installer code after verification. The embedded installer checks the authenticated archive digest, pins the archive target to the running binary's build target, atomically replaces the current executable, and refreshes the embedded Codex and Claude Skills. Existing older-version daemons and their live Sessions continue on their versioned sockets.
new
new is the only Session creation command and always requires its first prompt. Launch and local reservation are one atomic operation. The command waits briefly for provider confirmation from Codex's app-server turn lifecycle or Claude's matching UserPromptSubmit hook, then returns success with submission set to confirmed or pending. Observation of later progress and the answer is a separate fetch.
Its command-specific help is available through either equivalent spelling:
dlgt new --help
dlgt help newdlgt new
--title <TITLE>
[--alias <@ALIAS>]
[--profile <PROFILE>]
[--harness codex|claude|cursor|grok|opencode|pi]
[--model <MODEL>]
[--effort <LEVEL>]
[--cwd <DIR>]
[--harness-option <KEY=VALUE>]...
[--no-auto-approve]
[--startup-timeout <DURATION>]
[--clean-env]
[--pass-env <KEY>]...
[--env <KEY=VALUE>]...
[--unset-env <KEY>]...
--request-id <ID> (required)
[--prompt-file <PATH> | --stdin | -- <PROMPT>] (required)Rules:
--titleis required and may be non-unique.A Profile or Harness must resolve the Harness selection.
Model and effort are optional. Omission selects the provider default.
By default dlgt launches workers auto-approved:
--dangerously-bypass-approvals-and-sandboxfor Codex and--permission-mode=autofor Claude.--no-auto-approve(or Profileauto_approve = false) keeps the Harness's own approval prompts. An explicitpermission-mode=...Harness option replaces the implicit Claude mode. An explicit Codexapproval_policy=...orsandbox_mode=...Harness option likewise suppresses the implicit Codex bypass flag.Before launching either Harness, dlgt records the Session working directory as trusted in that provider's local workspace state. For Claude this updates
~/.claude.jsonand suppresses only the workspace trust dialog; tool permissions follow the auto-approve rule above.--harness-option KEY=VALUEis repeatable, stored with the Session, and reused byrestart. Claude receives each entry as--KEY=VALUE. Codex receives each entry as--config KEY=VALUEon both its managed app-server and remote TUI processes. Options whose values are managed by dlgt are rejected.--startup-timeoutis optional and defaults to 60 seconds, but startup is never unbounded.Session creation and acceptance of the first prompt are one atomic daemon operation; omitting it returns
INVALID_ARGUMENT.--prompt-filereads the exact UTF-8 prompt from a file. A relative path is resolved from the directory wheredlgtis invoked, independently of the Session's--cwd.--prompt-file,--stdin, and a prompt after--are mutually exclusive. Both file and stdin input avoid putting the prompt text in argv.Prefer
--prompt-filefor long prompts from an agent-controlled shell. It needs no pipe, heredoc, or input redirection.--request-idis required, and must be non-empty and at most 128 bytes. Retrying the same ID with a byte-identical payload returns the original acceptance receipt with"replayed": trueinstead of creating a second Session, even if the Session has since moved on. The same ID with a different payload isINVALID_ARGUMENT. The daemon retains the last 1,024 receipts for its lifetime.It is required rather than optional because an idempotency key only works if it exists before the first attempt: by the time a caller notices a lost response, an optional key would already be too late, and callers reliably do not add safety options they were not forced to.
--aliasis unaffected and remains a human convenience, not the deduplication key.If an exact requested alias is active,
newfails withALIAS_IN_USEand creates no Session or provider process.If startup succeeds but prompt acceptance fails, dlgt terminates the Harness, releases the Alias, and returns one structured launch failure. A failed audit record may remain addressable by its Session ID, but no live half-created Session is returned.
If local delivery succeeds but the provider does not confirm submission within five seconds, the command still exits successfully with
submission: "pending", the Session ID, and a boundedfetchaction. The prompt may still start. Do not resend it with a new request ID; replaying the original request ID returns the same execution and can later reportsubmission: "confirmed".
Example:
dlgt new \
--title "prompting Claude worker" \
--harness claude \
--no-auto-approve \
--cwd . \
--request-id review-1 \
-- "Review the current design"The unsafe full bypass remains available only when deliberately requested:
dlgt new \
--title "unrestricted Claude worker" \
--harness claude \
--harness-option dangerously-skip-permissions=true \
--cwd . \
--request-id review-2 \
-- "Review the current design"Codex configuration uses the same option with a TOML value:
dlgt new \
--title "Codex with live search" \
--harness codex \
--harness-option 'web_search="live"' \
--cwd . \
--request-id codex-search-1 \
-- "Research the current behavior"dlgt new \
--title "run review" \
--profile fable-review \
--cwd . \
--request-id run-review-1 \
-- "Review the current design"{
"ok": true,
"session": {
"id": "codex:019f6307-341e-7e81-8a33-7ab61e804345",
"alias": "@run-review-361csx",
"title": "run review",
"harness": "codex",
"state": "busy"
},
"execution_seq": 1,
"submission": "confirmed",
"cursor": "4"
}cursor is this Session's observation position, taken immediately before the local reservation is recorded, so passing it to fetch --cursor cannot miss output a fast provider emitted in between. Positions are small counting numbers, so they survive an agent's conversation context. Follow the submission with:
dlgt fetch codex:019f6307-341e-7e81-8a33-7ab61e804345 \
--cursor 4 --wait 15mrestart
dlgt restart <SESSION_ID>
[--startup-timeout <DURATION>]
[--clean-env]
[--pass-env <KEY>]...
[--env <KEY=VALUE>]...
[--unset-env <KEY>]...
[--pretty]restart replaces a Session's provider process while preserving its alias, retained history, execution sequence, and provider conversation. Codex normally keeps the same Session ID. If Claude reports a rotated provider session ID, dlgt atomically rekeys the Session and returns the new canonical session.id.
Rules:
- Active
idle,busy, andblockedSessions may be restarted, as may terminalstoppedandfailedSessions. An active execution is durably completed asinterruptedbefore the replacement process starts. starting,stopping, andrestartingSessions reject a second lifecycle operation withSESSION_UNAVAILABLE.- The Session ID must contain a provider conversation ID.
- A terminal Session should be addressed by its provider-qualified Session ID because its alias may already belong to a newer active Session.
- If another active Session now owns the old alias, restart fails with
ALIAS_IN_USE; it never renames either Session implicitly. - Restarting an active Session keeps its alias reserved throughout the process replacement.
- Startup is bounded by
--startup-timeout, which defaults to 60 seconds. - Launch environment values are freshly supplied by the invoking client and are not retained for replay.
- Existing results, events, raw output, and scrollback remain readable; new executions continue the same monotonic
execution_seq.
send
dlgt send <SESSION_ID|@ALIAS>
--request-id <ID> (required)
[--pretty]
[--prompt-file <PATH> | --stdin | -- <PROMPT>] (required)Resume a provider conversation after its owning daemon exits with the same Session ID returned by new:
dlgt send <codex:PROVIDER_THREAD_ID|claude:PROVIDER_SESSION_ID> --resume
[--model <MODEL>]
[--effort <LEVEL>]
[--cwd <DIR>]
[--harness-option <KEY=VALUE>]...
[--no-auto-approve]
[--startup-timeout <DURATION>]
[--clean-env]
[--pass-env <KEY>]...
[--env <KEY=VALUE>]...
[--unset-env <KEY>]...
--request-id <ID> (required)
[--pretty]
[--prompt-file <PATH> | --stdin | -- <PROMPT>] (required)The launch options above are accepted only with --resume, and --harness is rejected because the provider prefix selects the Harness. A matching live Session is reused; if none exists dlgt reserves the provider conversation, launches a new Session, waits for bind/readiness, and atomically accepts the prompt. Success returns the same canonical session.id and caller correlation ID. If Claude rotates its provider session ID while resuming, success returns that new canonical ID. Plain send never launches and returns SESSION_NOT_RUNNING with a --resume hint when its target is not live.
Rules:
- The target Session must already exist.
- Launch, model, and environment options are accepted only with
--resume; Profile options are not accepted bysend. - The prompt is required.
--is recommended so prompt text beginning with an option-like token is never parsed as a CLI option; multiple remaining words are joined with spaces. --prompt-file,--stdin, and a positional prompt are mutually exclusive. Prefer--prompt-filefor long prompts from an agent-controlled shell so no shell pipe or redirection wraps the directdlgt sendinvocation.- If the Session is idle, dlgt accepts the prompt and transitions it to busy.
- If the Session is busy, canceling, blocked, stopping, stopped, or attached, the command fails immediately and has no side effects. Busy and canceling return
SESSION_BUSY; blocked returnsSESSION_BLOCKED; attached returnsSESSION_ATTACHED; other non-idle states returnSESSION_UNAVAILABLE. --request-idis required and behaves exactly as it does fornew, including with--resume: the same ID and payload replay the original receipt with"replayed": true, and the same ID with a different payload isINVALID_ARGUMENT.- There is no
--wait,--create,--enqueue,--after, or--fail-if-busy. Creation, queueing, and waiting are notsendresponsibilities, and busy rejection is always the default.
Asynchronous example:
dlgt send codex:019f6307-341e-7e81-8a33-7ab61e804345 --request-id review-3 \
-- "Review the revised design"{"ok":true,"session":{"id":"codex:019f6307-341e-7e81-8a33-7ab61e804345","state":"busy"},"execution_seq":2,"submission":"confirmed","cursor":"4"}Busy rejection:
{"ok":false,"error":{"code":"SESSION_BUSY","session_id":"codex:019f6307-341e-7e81-8a33-7ab61e804345"}}Read the answer with one fetch:
dlgt send codex:019f6307-341e-7e81-8a33-7ab61e804345 --request-id review-4 \
-- "Review the revised design" \
&& dlgt fetch codex:019f6307-341e-7e81-8a33-7ab61e804345 --wait 15mRetained result
Every accepted execution receives a per-Session monotonic execution_seq. This number is returned by new or send, echoed by lifecycle events and the retained result, and never accepted as a CLI selector. It is correlation data, not a public execution resource or ID.
The retained result shape is:
{
"execution_seq": 2,
"status": "completed",
"final_text": "Review result...",
"final_text_source": "hook",
"error": null,
"started_at_ms": 1784024104395,
"completed_at_ms": 1784024252019,
"usage": null
}status is one of completed, failed, canceled, or interrupted. final_text is the Harness-reported final assistant message and is always a string for completed, although it may be empty. Failed terminal states may provide partial final text and must provide a structured error. Usage is nullable because availability differs by Harness.
final_text_source is a diagnostic:
hook the Harness lifecycle event reported the text
transcript the Harness reported nothing and dlgt recovered the text from
the Session's own provider transcript, bounded to this execution
missing no text was recovered; the execution status is still authoritativeA failed recovery never turns a completed execution into a failure.
fetch
dlgt fetch <SESSION_ID|@ALIAS>
[--cursor <N>]
[--wait <DURATION>]
[--screen[=<MAX_STABLE_LINES>] | --no-screen]
[--max-bytes <BYTES>]
[--pretty]fetch is the one observation command. It returns, in a single JSON document, the current Session snapshot, every terminal result and lifecycle event after the cursor, the forward screen delta, and the response's cursor position.
Every observation succeeds. A long poll that expires with nothing new is {"ok":true,"reason":"timeout"} with empty deltas, not an error. The same cursor position is returned whenever the underlying watermark vector did not move; an empty public delta can still mint a new position when internal bookkeeping advanced beneath it. reason is one of:
snapshot bounded baseline; no cursor was supplied
change new events, results, or stable rows were delivered
result the bound execution has a retained terminal result
blocked the Session needs a human answer; the live screen is included
page_full more data already exists beyond the returned cursor
gap retention evicted part of the cursor window
timeout nothing new in the cursor windowRules:
- Without
--cursor,fetchreturns a bounded baseline: current state, the latest retained result, the last 128 stable screen lines, the live screen, and a fresh position. This is the documented recovery path after a lost position or a lost response. --waitrequires an explicit duration and accepts up to 24h. Omitted, the command returns immediately.--waitbinds to the execution that is active, or latest, at the first evaluation and completes when that execution has a retained terminal result. A later execution never extends the bind. Blocked input, a page-full response, a retention gap, and the deadline all return early.- A live-screen repaint alone never completes a
--wait. Spinner redraws are included opportunistically when something else wakes the poll or the deadline fires. - Replaying the same cursor replays the same immutable events, results, and stable rows. Nothing is consumed or advanced server-side; the Session snapshot and live screen are always current.
Response:
{
"ok": true,
"schema_version": 1,
"runtime": {"version":"0.4.0","instance_id":"1f2c…"},
"reason": "result",
"has_more": false,
"gaps": [],
"cursor": "4",
"sessions": [
{
"session": {"id":"claude:8bc7859c","state":"idle"},
"events": [
{"schema_version":1,"seq":104,"type":"session.idle","session_id":"claude:8bc7859c","execution_seq":7,"result_status":"completed"}
],
"results": [
{
"execution_seq": 7,
"status": "completed",
"final_text": "Review result...",
"final_text_source": "hook",
"final_text_offset": 0,
"final_text_complete": true,
"error": null,
"started_at_ms": 1784024104395,
"completed_at_ms": 1784024252019,
"usage": null
}
],
"screen": {
"epoch": 3,
"reset": false,
"reset_reason": null,
"stable": ["Checking tests...", "Found one race."],
"live": ["Writing final review..."],
"live_truncated": false
},
"gaps": []
}
]
}Bounds and pagination
Response size is part of the contract:
| Dimension | Default | Limit |
|---|---|---|
| Serialized response | 32 KiB | 256 KiB |
| Long poll | none | 24h |
| Lifecycle events per page | 64 | 64 |
| Results per page | 4 | 4 |
| Stable screen lines per page | 128 | 512 |
| Live rows | current screen, cropped to 40 | 40 |
has_more: true means requested data already exists beyond the returned cursor; that response uses reason: "page_full" and the next call returns immediately even with --wait.
--max-bytes is a hard bound on the complete compact response line: the {"ok":true,...} wrapper and its trailing newline are reserved before any content is committed, and the finished line is measured. The cursor is derived from what the document actually carries, so it can never advance past content that was left out. When the budget squeezes a response, dlgt keeps state, then gaps, then terminal results, then events, then blocked information, and drops screen text first.
Three things are outside the bound, all deliberately:
- an optional
infonotice, which the daemon injects rarely and independently of the request (seeUPDATE_AVAILABLEabove); --pretty, which exists for humans and inflates the output arbitrarily;- the raw JSONL RPC envelope, whose request
idthe caller chooses. See RPC; that id is capped at 200 bytes.
The contract covers the compact CLI response without info.
Progress comes from chunking, never from oversizing. A long final_text is chunked at a UTF-8 boundary and continued through final_text_offset and final_text_complete; a screen row wider than the remaining budget is chunked mid-line. Both continuations are carried in the cursor, so every has_more response advances at least one watermark.
screen.stable always contains complete rows and nothing else. A row that had to be split is carried in one of two explicitly ordered slots:
{
"screen": {
"fragment_before": {"row_id": 8412, "offset": 4096, "text": "...", "complete": true},
"stable": ["Found one race."],
"fragment_after": {"row_id": 8414, "offset": 0, "text": "...", "complete": false}
}
}fragment_beforecontinues the row the previous response split. It logically precedesstable[0]and is the only placecomplete: trueappears.fragment_afteris the row this response had to split. It logically follows the last entry ofstableand is never complete.
The screen delta of one response is therefore, in order: fragment_before, then every row of stable, then fragment_after.
Callers must retain every fragment piece: concatenate the pieces for a given row_id in offset order until one arrives with complete: true, which yields the whole row. Discarding pieces until a complete one arrives loses data, because the completing piece carries only its own tail. A row never appears in both stable and a fragment slot.
Lifecycle events are delivered as a strictly ascending prefix: the response never skips an event to deliver a later one, so the event watermark is always a position with nothing outstanding behind it.
If --max-bytes is too small to carry the response envelope plus one chunk of progress, fetch fails with INVALID_ARGUMENT and names a budget verified to work rather than emitting an oversized document. That number is obtained by actually rendering a response at it, not estimated, so retrying at exactly the reported value succeeds. It is a verified candidate, not a minimum: whether a budget makes progress is not monotonic, so a value between the rejected budget and the reported one may or may not work, and only the reported value carries the guarantee. The practical floor is roughly 1 KiB for a single Session; the default of 32 KiB is far above it.
Cursors
A cursor is a position, not a token. Every acceptance, and every fetch response whose watermark vector advanced, mints the addressed scope's next position -- 1, 2, 3 -- and the daemon holds the watermarks behind it. A fetch whose vector did not move returns the caller's own position; an empty public delta can still mint a new position when internal bookkeeping advanced. Leading zeros in a supplied position are accepted and canonicalized away.
{"cursor":"4"}Positions are deliberately small: a caller carries one across turns, and an opaque 200-character token does not survive an agent's context. Over RPC the value may be spelled as a JSON number or a string of digits; anything else is CURSOR_INVALID. The number is an ordinal within the scope you address, not a capability: fetch B --cursor 3 returns Session B's own third observation window, which is a legitimate replay, not an error. Idempotency is carried by --request-id, never by the cursor.
Positions bind to an internal Session identity, so a Claude provider-ID rotation does not invalidate them.
A position is meaningful only within one daemon lifetime. Nothing in the number says which daemon minted it, so a number kept across a restart is not rejected: it names the new daemon's window of that position, or fails CURSOR_EXPIRED if that daemon has not minted that far. What comes back is always current data -- the previous daemon's state is gone, so there is nothing stale to return -- but the number no longer means what you think it means. After RPC_UNAVAILABLE, SESSION_NOT_RUNNING, or any daemon restart, drop remembered positions and re-enter with send --resume (whose acceptance carries a fresh position) or one cursorless fetch.
The daemon retains the most recent 64 positions per Session, within a global cap of 4,096 across every scope; beyond the bound the oldest are dropped. A response whose watermark vector did not move keeps the position you sent instead of minting a new one, so an idle long poll normally spends none of them; internal bookkeeping can still advance the vector beneath an empty public delta and mint.
CURSOR_EXPIRED the position was never minted, or is no longer retained
CURSOR_INVALID the value is not a numberBoth are non-zero exits, and the recovery for both is a single cursorless fetch.
Retention gaps
Retention is bounded per daemon: 10,000 stable screen rows per Session, 50,000 lifecycle events, and 128 results or 16 MiB of result bodies per Session. When a cursor predates an eviction, fetch exits 0 with reason: "gap", a structured gaps entry, a bounded baseline, and a fresh cursor. It never silently resets.
{"gaps":[{"component":"screen","reason":"retention_overrun"}]}component is screen, events, or results. Per-Session gaps (screen, results) appear on the Session bucket; scope-wide gaps (events) appear in the top-level gaps array, so a gap cannot disappear with a bucket that the response did not carry.
Waiting from an agent harness
- Claude callers: run the long
fetchthrough the harness's background mechanism. The acceptance receipt is already in hand fromneworsend, so a lost foreground response costs nothing. - Codex callers: run the long
fetchinside one exec cell and use a single long cell wait. Pre-yield output is retained and delivered at the next wait. - After any lost response, run a cursorless
fetchto rebase.
cancel
dlgt cancel <SESSION_ID|@ALIAS> [--timeout <DURATION>]cancel interrupts the active provider execution. It does not stop the Session. A successful cancellation terminalizes the current result as canceled or interrupted according to the normalized provider mapping and returns the Session to idle only after provider quiescence is proven.
Cancellation is bounded and defaults to 30 seconds. On timeout, dlgt returns CANCEL_TIMEOUT, leaves the Session in canceling, and continues observing provider quiescence in the background. fetch reveals the eventual terminal state.
Canceling an idle Session is idempotent: it returns exit 0 with {"canceled":false,"reason":"NO_ACTIVE_WORK"}.
Blocked input
Input required from a human is a first-class Session state, not a failure and not an infinite wait.
{
"ok": false,
"error": {
"code": "SESSION_BLOCKED",
"session_id": "codex:019f6307-341e-7e81-8a33-7ab61e804345",
"action": "have a human run in a terminal: dlgt attach codex:019f6307-341e-7e81-8a33-7ab61e804345"
}
}fetch reports the same state as reason: "blocked" with the live screen attached, so the caller can see the question before attaching. After a human attaches, answers, and detaches, the same fetch may be issued again. Provider-specific detection may initially be conservative, but dlgt must never infer completion from a quiet screen.
Session commands
dlgt list [--all] [--all-versions] [--pretty]
dlgt show <SESSION_ID|@ALIAS> [--pretty]
dlgt attach <SESSION_ID|@ALIAS> [--steal]
dlgt stop <SESSION_ID|@ALIAS> [--force]
dlgt restart <SESSION_ID> [environment options]listreturns active Sessions.list --allincludes terminal historical Sessions.list --all-versionsqueries every live versioned daemon socket and addsruntime_versionandruntime_socketto each Session.showreturns identity, Harness, model selection, state, current timing, latest retained result, and relevant failure data. A Session that failed before it became ready also haslaunch_failurewith the samecode,message,hint,action, andscreenthat the launch error returned.attachtakes an exclusive input lease, replays the retained terminal view, and follows the live PTY. It requires an interactive terminal on stdin and stdout and otherwise returnsATTACH_REQUIRES_TTYpointing atfetch. A second attach returnsALREADY_ATTACHEDunless--stealexplicitly transfers the lease. Detach withCtrl-b d. Mirrored multi-attach is outside v1.stoprequests graceful Session termination.stop --forceterminates the provider process group.
The daemon owns provider PTYs and the Codex app-server in separate process groups. A sibling reaper has its own process group, ignores ordinary terminal and service shutdown signals, and keeps provider groups registered until normal teardown. If the daemon exits without running destructors, control-pipe EOF makes the reaper terminate every remaining registered group.
Lifecycle events
Lifecycle events are read through fetch, which returns them for one Session alongside state, results, and screen. There is no separate event command and no cross-Session event stream: list reports current state across Sessions, and fetch reports the ordered history of one.
The versioned event schema and the complete event set are defined in RPC.
Rendered scrollback and raw logs
Normal observation uses a headless VT emulator and plain-text scrollback:
dlgt scrollback <SESSION_ID|@ALIAS>
[--lines <COUNT>]
[--before <CURSOR>]The default is the latest 100 rendered lines. v1 retains at most 10,000 rendered rows per Session. Rows are served from the same persistent stable-row store that backs fetch, so a read no longer re-renders retained raw bytes. The response includes the terminal dimensions, plain-text lines, truncation state, and an opaque cursor for older pages. scrollback is the human debugging view; agents should use fetch, which returns the forward delta instead of an overlapping tail.
{
"ok": true,
"session_id": "codex:019f6307-341e-7e81-8a33-7ab61e804345",
"screen": {"rows":24,"cols":120},
"lines": ["Review complete.","","Main concerns:","1. Timeout behavior..."],
"truncated": true,
"before": "scr_84A2"
}Raw PTY bytes are explicitly diagnostic:
dlgt logs <SESSION_ID|@ALIAS> --raw
dlgt logs <SESSION_ID|@ALIAS> --raw --jsonPlain dlgt logs without --raw is invalid. --raw writes bytes directly; --raw --json returns base64. There is no logs --follow. Live lifecycle observation uses fetch --wait, and live terminal observation uses attach.
Requiring --raw is an intentional capability gate. See Design for the VT rendering and raw-retention rationale.
Model discovery
dlgt models [--include-hidden]
dlgt models --harness codex [--include-hidden]
dlgt models --harness claudeWithout --harness, dlgt models queries every Harness and returns {"harnesses":[...]}. A Harness that cannot be reached reports "discovery":"unavailable" with its error rather than failing the command.
Codex discovery uses app-server model/list and returns account-aware model IDs, display names, descriptions, defaults, supported reasoning efforts, input modalities, and service tiers.
{
"ok": true,
"harness": "codex",
"source": "app-server",
"discovery": "complete",
"models": []
}Claude Code does not currently expose an equivalent documented non-interactive picker API. dlgt always returns the stable Claude Code aliases, then augments them with current canonical IDs from the public, daily refreshed claude-models-list snapshot. Date-pinned IDs ending in -YYYYMMDD are normalized to their undated alias, so claude-haiku-4-5-20251001 is reported as claude-haiku-4-5. The snapshot is account-scoped and is not presented as the Claude Code picker. If it cannot be fetched or validated, dlgt falls back to the aliases and reports discovery as partial.
{
"ok": true,
"harness": "claude",
"source": "https://raw.githubusercontent.com/combinatrix-ai/claude-models-list/main/models.json",
"discovery": "snapshot",
"models": [
{"id":"default","kind":"alias","recommended":true},
{"id":"best","kind":"alias"},
{"id":"sonnet","kind":"alias"},
{"id":"opus","kind":"alias"},
{"id":"haiku","kind":"alias"},
{"id":"claude-fable-5","display_name":"Claude Fable 5"}
]
}Model and effort are optional at launch. Omission selects the provider's recommended default. Profiles should prefer stable provider aliases unless an exact version pin is required.
For an exact canonical Claude model ID, dlgt validates --effort against the snapshot's capabilities.effort before starting Claude Code. Floating aliases such as default, opus, and sonnet remain provider-validated because their target model can change independently of the snapshot. If the snapshot is unavailable, launch validation fails open and Claude Code remains authoritative.
Model aliases are resolved by the Harness when new launches the Session, not on each send. dlgt does not silently pin a drifting alias; show reports the provider-resolved model when the Harness makes it available.
doctor
dlgt doctor [--json] [--probe]The default command is offline and read-only. It reports the running binary, configuration parse status, every versioned daemon socket and its permissions, Codex and Claude CLI versions, and whether each installed Skill is byte-for-byte identical to the Skill embedded in this binary. Human-readable output is the default; --json emits the same check records with ok, warn, fail, or skip status.
--probe additionally initializes Codex app-server and runs model/list, then checks the latest published dlgt release. It starts no model turn and performs no repair. If no current daemon exists, the probe may leave the versioned daemon running until its normal idle timeout. Every finding includes evidence and, when applicable, one explicit recovery hint; doctor never silently deletes sockets or overwrites Skills. The command exits successfully after producing a complete report; automation should gate on the report's status, not the top-level transport ok field.
Profiles and launch environment
dlgt profiles
dlgt profiles list
dlgt profiles show <NAME>
dlgt harnesses [<HARNESS>]Bare dlgt profiles is the same request as dlgt profiles list.
Profiles are client-side launch specifications. The client expands them before RPC so the daemon does not need to reread mutable configuration.
[profiles.fable-review]
harness = "claude"
model = "best"
effort = "high"
harness_options = ["permission-mode=auto"]
clean_env = true
pass_env = ["PATH", "HOME", "SSH_AUTH_SOCK"]
[profiles.codex-live-search]
harness = "codex"
harness_options = ['web_search="live"']Environment precedence:
client snapshot or clean base < Profile < explicit launch optionsProfile harness_options are followed by explicit --harness-option values. They configure the provider CLI rather than the launch environment. Claude entries become --KEY=VALUE; Codex entries become --config KEY=VALUE for both provider processes.
- Default launch environment is a snapshot of the invoking client's environment, never the daemon's startup environment.
--clean-envstarts from a minimal runtime base.--pass-env KEYcopies one client value with--clean-env.--env KEY=VALUEsets or overrides a value.--unset-env KEYremoves a value.- Environment options apply when creating or restarting a Session. Values are freshly snapshotted for each process launch and are never stored for replay.
- dlgt applies final lifecycle safety overrides to owned Harness children:
check_for_update_on_startup=falsefor Codex andDISABLE_AUTOUPDATER=1for Claude. They cannot be overridden per Session and do not change provider global configuration. - Launch environment values are passed in RPC memory, never argv, and are never directly serialized into Session records,
list,show,fetch, Profiles, or error JSON. Provider output is untrusted and can deliberately echo its environment, so results, scrollback, and especiallylogs --rawmust be treated as potentially sensitive output rather than as a redaction boundary.
Exit statuses
0 command succeeded, including every fetch observation
1 usage, configuration, identity, launch, cursor, or RPC error
3 bounded cancellation timeout; the cancellation continues
4 Session or launch is blocked on human input
5 Session is busy and rejected a sendThe JSON error code is the primary machine-readable reason. Exit status is the shell-level summary. SESSION_BLOCKED and LAUNCH_BLOCKED use exit 4 and SESSION_BUSY uses exit 5. NO_RESULT, SESSION_ATTACHED, ALREADY_ATTACHED, ATTACH_REQUIRES_TTY, ALIAS_IN_USE, SESSION_NOT_RUNNING, SESSION_UNAVAILABLE, CURSOR_EXPIRED, and CURSOR_INVALID use exit 1. CANCEL_TIMEOUT uses exit 3. Idle cancel is an idempotent exit-0 no-op.
fetch never uses a non-zero exit to describe an execution. A failed, canceled, or interrupted execution is reported inside results[].status, and a blocked Session is reported as reason: "blocked".
The stable v1 structured error-code families are:
INVALID_ARGUMENT Invocation cannot be retried unchanged
NOT_FOUND Session or configuration object does not exist
NO_RESULT Session has never accepted work
SESSION_NOT_RUNNING No live Session matches; retry with --resume
ALIAS_IN_USE Exact Alias belongs to a non-terminal Session
SESSION_BUSY Active execution; retry after it terminalizes
SESSION_BLOCKED Human input is required
SESSION_ATTACHED Exclusive attach lease prevents semantic send
SESSION_UNAVAILABLE Session state cannot accept the requested operation
ALREADY_ATTACHED Another client owns the attach lease
ATTACH_REQUIRES_TTY attach needs an interactive terminal; use fetch
CURSOR_EXPIRED Cursor position was never minted or is no longer held
CURSOR_INVALID Cursor value is not a position number
CANCEL_TIMEOUT Cancel wait expired; cancellation continues
LAUNCH_BLOCKED Harness startup stopped at a screen only a human can answer
LAUNCH_FAILED Harness startup or initial prompt acceptance failed
RPC_UNAVAILABLE Daemon transport is unavailable; retry may succeed
INTERNAL dlgt runtime invariant failureCommands may add contextual fields, but must not overload one code with a different retry or human-action policy.
If new fails before provider binding, its error includes the temporary launch_id for diagnostics and never presents it as a Session ID. A failure after binding includes the canonical session_id.
A launch failure (LAUNCH_BLOCKED or LAUNCH_FAILED) also carries screen, the provider's last rendered rows, and a hint naming scrollback and logs --raw for that ID. LAUNCH_BLOCKED adds an action the user must take; dlgt never answers such a screen itself. The known blocker is Codex's "Hooks need review" dialog, which appears when a configured hook is new or changed. dlgt does not trust hooks or pass --dangerously-bypass-hook-trust; the user runs codex once in that directory and reviews the hooks.
Design and RPC contracts
The provider lifecycle mapping, acceptance criteria, and design rationale are in Design. The public JSONL method set and schemas are in RPC.
Cursor interactive CLI
Cursor support uses the official interactive cursor-agent executable in a live PTY. It does not start ACP, MCP server mode, print mode, or the desktop application. Install and log in to Cursor CLI first. DLGT_CURSOR_BIN can select another executable path when starting the dlgt daemon.
dlgt new --harness cursor --title "Cursor review" --cwd . \
--request-id cursor-review-1 -- "Review this change"
dlgt fetch cursor:<conversation-id> --wait 5m
dlgt send cursor:<conversation-id> --request-id cursor-review-2 -- "Review the revision"
dlgt stop cursor:<conversation-id>
dlgt send cursor:<conversation-id> --resume --request-id cursor-review-3 -- "Continue"On launch, dlgt adds one idempotent bridge to each of beforeSubmitPrompt, afterAgentResponse, and stop in $HOME/.cursor/hooks.json, preserving existing handlers. The bridge remains installed but is inert outside dlgt children: the executable, owning socket, and unique launch selector come from child-only environment variables. A sidecar dlgt-hooks.lock serializes registration across dlgt daemons. Invalid JSON or handler arrays are rejected without overwriting the file. No hooks are added to the workspace.
New sessions and resumes pass the first prompt to the interactive CLI. Cursor may not emit sessionStart on resume, so dlgt binds the conversation only on a matching beforeSubmitPrompt. This confirms the provider's submission hook, not a backend acknowledgement; another user hook can still block that prompt. Follow-ups are bracketed PTY pastes. Final results require a matching stop and afterAgentResponse pair, in either arrival order; error and aborted are terminal without response text. A missing hook leaves the result pending, never inferred from the screen. Existing hooks that block prompts or generate automatic follow-ups can affect the conversation.
The adapter supports --model, --harness-option mode=plan|ask, and --harness-option sandbox=enabled|disabled. Default auto-approval passes --trust --force; --no-auto-approve omits both. Other launch options and --effort are rejected. Model IDs come from CLI discovery (dlgt models --harness cursor runs cursor-agent --list-models).
Current limitations: restart is rejected before changing the session; use stop followed by send --resume. Permission prompts do not yet have a structured blocked-state signal, so inspect fetch and use attach when needed. Cursor's conversation title is provider-managed; dlgt's --title sets the local title and alias.
Authenticated macOS validation on 2026-09-22 used CLI 2026.09.18-9a7762b with gpt-5.4-nano-none: new-session completion, same-conversation follow-up, stop/resume with retained conversation context, cancellation of a running tool, and another completed prompt after cancellation all passed. Results came from lifecycle hooks, not screen inference. The automated PTY fixture additionally checks concurrent-session isolation, request replay, and Japanese multiline input. This is not the Docker pre-release matrix required by AGENTS.md.
References: Cursor CLI, parameters, and hooks. The adapter was developed against CLI 2026.09.18-9a7762b.
Grok Build
Grok support uses the interactive grok TUI in a live PTY. It does not start ACP agent mode or headless -p runs. Install and authenticate Grok Build first. DLGT_GROK_BIN can select another executable when starting the dlgt daemon.
dlgt new --harness grok --title "Grok review" --cwd . \
--request-id grok-review-1 -- "Review this change"
dlgt fetch grok:<session-id> --wait 5m
dlgt send grok:<session-id> --request-id grok-review-2 -- "Review the revision"
dlgt stop grok:<session-id>
dlgt send grok:<session-id> --resume --request-id grok-review-3 -- "Continue"On launch, dlgt installs an idempotent Claude-shaped hook bridge in $HOME/.grok/hooks/dlgt.json for SessionStart, UserPromptSubmit, Stop, StopFailure, Notification, and SessionEnd. The bridge is inert outside dlgt children (DLGT_GROK_LAUNCH / DLGT_GROK_HOOK_BIN). CamelCase Grok hook payloads are normalized to the snake_case fields Claude handlers expect. Default auto-approval passes --always-approve --trust; --no-auto-approve omits both. --model and --effort are supported; model discovery is currently unavailable in dlgt. Completion comes from lifecycle hooks, not PTY silence.
OpenCode
OpenCode support runs the interactive opencode TUI in a live PTY. It does not use opencode run, which exits after one prompt. Install OpenCode first. DLGT_OPENCODE_BIN can select another executable when starting the dlgt daemon.
# Export the Grok CLI OIDC access token. It is the `key` field of
# ~/.grok/auth.json and it expires. Sign in with the Grok CLI again to refresh
# it, then export the new value. Do not print, log, or commit the token.
export XAI_API_KEY="$(python3 -c 'import json,os; print(json.load(open(os.path.expanduser("~/.grok/auth.json")))["key"])')"
dlgt new --harness opencode --model xai/grok-4.7 --title "OpenCode review" --cwd . \
--request-id opencode-review-1 -- "Review this change"
dlgt fetch opencode:<session-id> --wait 5m
dlgt send opencode:<session-id> --request-id opencode-review-2 -- "Review the revision"
dlgt stop opencode:<session-id>
dlgt send opencode:<session-id> --resume --request-id opencode-review-3 -- "Continue"On launch, dlgt installs dlgt.js in $XDG_CONFIG_HOME/opencode/plugins/ or ~/.config/opencode/plugins/. The plugin is inert unless DLGT_OPENCODE_LAUNCH is set, which only dlgt children receive. It emits Claude-shaped SessionStart, UserPromptSubmit, Stop, and StopFailure hooks. A foreign dlgt.js that lacks that marker is left unchanged.
Default auto-approval passes --auto. --no-auto-approve omits it. --model takes provider/model. --effort is rejected. The only harness option is agent=<name>. opencode can reopen a session with --session and no prompt, so restart is supported. Discover model IDs with dlgt models --harness opencode (opencode models). Completion comes from the hooks, not PTY silence.
Pi
Pi support runs the interactive pi TUI in a live PTY. Install @earendil-works/pi-coding-agent first. DLGT_PI_BIN can select another executable when starting the dlgt daemon.
Pi's --mode rpc protocol reports agent_settled on stdout, which is the right completion signal. That same event is available to an extension inside the interactive TUI. RPC would replace the PTY session attach and follow-up pastes use, so dlgt stays on the TUI and the extension waits for agent_settled rather than agent_end (a retry or compaction can still follow agent_end).
# Same Grok CLI JWT as OpenCode. Refresh ~/.grok/auth.json `key` when it
# expires. Do not print the token.
export XAI_API_KEY="$(python3 -c 'import json,os; print(json.load(open(os.path.expanduser("~/.grok/auth.json")))["key"])')"
dlgt new --harness pi --model grok-4.7 --harness-option provider=xai \
--title "Pi review" --cwd . --request-id pi-review-1 -- "Review this change"
dlgt fetch pi:<session-id> --wait 5m
dlgt send pi:<session-id> --request-id pi-review-2 -- "Review the revision"
dlgt stop pi:<session-id>
dlgt send pi:<session-id> --resume --request-id pi-review-3 -- "Continue"--model xai/grok-4.7 also selects the provider. --effort maps to Pi's --thinking level (off, minimal, low, medium, high, xhigh, max). The only harness option is provider=<name>.
On launch, dlgt writes ~/.pi/agent/dlgt/bridge.js and passes it with --extension. That directory is outside Pi's extension autoload path. The extension is inert unless DLGT_PI_LAUNCH is set. A foreign bridge.js that lacks that marker is left unchanged.
Pi does not prompt before each tool call. Default auto-approval passes --approve, which skips the project-trust dialog so the TUI can become idle. --no-auto-approve omits it and Pi may wait on that dialog. pi --session reopens a conversation without a prompt, so restart is supported. Discover model IDs with dlgt models --harness pi (pi --list-models). IDs are provider/model. Completion comes from agent_settled, not PTY silence. These CLIs can stall when stdout is a pipe; dlgt already runs the harness in a PTY.