Architecture and safety model
August 16, 2026 · View on GitHub
English | 简体中文
This document contains the bridge semantics that are intentionally kept out of the user-facing README. It describes the current 0.1.0-alpha.1 behavior, not a permanent compatibility promise.
Positioning and Host lifecycle
dsh-Agentlink is a caller-side bridge, not a DSH Cordis bundle. A supported caller such as Codex or Claude Code starts it as a local STDIO MCP server; the bridge connects to an independently running official DSH Web Host.
The bridge is connect-only. It does not start, daemonize, stop, or own dsh web, and it has no Host pidfile or port lock. The user or an OS service owns the Host lifecycle. This keeps DSH sessions visible through the official Web UI after an individual caller's MCP process exits.
Identity and state model
A BridgeTask, a DSH root session, and a DSH turn are different objects:
- One BridgeTask has one stable
taskId -> rootSessionIdidentity mapping. - The root session can run many turns;
turn_completeddoes not remove the task or prevent another follow-up. - Session-backed DSH subagent descendants are discovered from
session.list/subagent.list, reconciled independently, and retainparentSessionIdplusorigin="subagent"in the task ledger.
Status does not collapse connectivity and execution into one enum:
availability:connected | host_unreachable | session_not_foundexecution:starting | running | awaiting_approval | awaiting_input | turn_completed | failed | canceled | interrupted- Public
statusisunknownwhen availability overrides the current execution observation, whilelastKnownExecutionStatusis retained.
Queue depth is derived from the latest complete session/queue snapshot:
nextTurn:placement="queued"steering:placement="steering"context:placement="context"nextStep: steering plus contexttotal: all pending items
Queue state is marked stale or unknown as soon as events.mux disconnects.
Event ledger and recovery
DSH session/history is the only source of truth for conversation content. The bridge does not copy prompts, user or assistant text, tool arguments/results, or question bodies into its files. It persists only coordination state in three separate stores:
tasks/<taskId>.jsoncontains only{taskId, sessionId}.claims/<taskId>.jsoncontains the canonical cwd, task/session owner, claim mode, and creation time.ledgers/<taskId>/events.jsonlis a rebuildable coordination index for task cursors, lineage, source watermarks, non-content execution/pending state, issued rpcIds, and final-message pointers.
Each JSONL record has a monotonic task cursor/mergeIndex, sourceSessionId, optional sourceSeq, optional parentSessionId, origin, event type, and a scrubbed coordination object. It never contains the full mux/history envelope. mergeIndex is bridge observation and persistence order, not a DSH global causal order.
Task-ledger appends and workspace-claim changes use task/registry-scoped inter-process locks. A writer rereads current disk state while holding the lock before allocating a cursor or changing a claim. Immutable task mappings use atomic temp-file plus hard-link creation. Bridge processes sharing one DSH_BRIDGE_HOME therefore share coordination state and must point to the same Host. Use a separate bridge home when changing Host origins. Lock critical sections are short and local-filesystem only; automatic stale-lock reaping is deliberately disabled because PID/mtime observation cannot be atomically coupled to a destructive rename. A hard-killed writer can leave a fail-closed lock that requires explicit operator recovery.
Recovery is subscribe-first:
- Open
events.muxand buffer live frames. - Read each
session/subscribed.lastSeqwatermark, or use the open stream as the fence for cold sessions. - Page
session.history/subagent.historybackwards to the persisted per-session high-watermark. - Sort and deterministically deduplicate durable events by
(sourceSessionId, sourceSeq). - Drain buffered live frames, then expose the committed task cursor.
rc.6 ignores events.mux.since; it is not a durable backlog. Delivery is documented as at-least-once with deterministic dedupe, never exactly-once. A gap that cannot be reconstructed is returned as unrecoverable_gap; an obsolete cursor is cursor_expired with earliestCursor. The bridge does not silently skip either condition.
dsh_tail returns bounded digests and nextCursor. While the Host is connected, it resolves source pointers from session.history at call time: assistant chunks are omitted or compacted, tool output is reduced, and questions, approvals, errors, turn outcomes, and the final assistant message remain complete in the response only. When the Host is unavailable, it returns contentUnavailable instead of reconstructing conversation text from a bridge copy.
At each root turn/end, the ledger folds only the last user-visible assistant/message pointer (sessionId + seq); dsh_status resolves that pointer from live history. A terminal turn with no pointer reports terminal_missing_final, not successful empty output.
The event pump runs even when no caller is tailing. After bridge restart, it rebuilds coordination folds from JSONL and reconciles them against authoritative DSH history. It never rebuilds content from bridge files.
Questions and approvals
dsh_followup cannot answer a pending DSH interaction. The bridge continuously consumes events.mux and keeps a per-rpcId pending map in memory. While connected, current requested frames are returned verbatim in dsh_status and dsh_tail; question text is never persisted.
On reconnect, rc.6 replays still-pending requests with stable rpcIds. After the mux baseline quiet period, absent prior requests receive coordination tombstones so they cannot silently revive; a later valid replay reopens the item. This is an explicit rc.6 heuristic capability, not a Host transaction.
Only typed response tools are public:
dsh_answer_question(taskId, requestId, answers[])dsh_resolve_approval(taskId, requestId, outcome="allow_once"|"reject")
They validate the rpcId type, task/session lineage, question ids, order, and options, then issue exactly one non-retried POST /api/respond client response. The Host carrier receipt is authoritative: bad-response keeps the item pending; not-pending means it was already answered, canceled, raced, or expired.
Safety rules:
- The bridge never automatically allows an approval.
- Every
approval/requestedis treated as a DSH sandbox escalation. allow_oncemaps only to wire outcomeallowed-once; it is not a persistent policy change.- A configured timeout is only one best-effort reject while this process and connection are alive. It is not a Host-level guarantee.
- For unattended fail-closed operation, configure DSH approval policy
never; absence of an answerer remains fail-closed. - Questions are shown verbatim and are never automatically answered on the user's behalf, especially for credentials, publishing, releases, or other sensitive actions.
Follow-up and cancellation semantics
dsh_followup(mode="queue") targets DSH next-turn; mode="steer" targets next-step. Queue can start a later turn after the current turn ends. Steer enters the active turn at the next step. Neither write is automatically retried.
Every session mutation performs a fresh session.list/history reconciliation first. Follow-up also reads live session.models and reports the actual current route. Mutation tools accept optional sinceCursor and expectedRevision. If the reconciled view differs, the bridge returns stale_view with the observed changes instead of issuing the write.
The unary rpcId generated for each bridge prompt is retained as coordination metadata. A matching user/message.data.source.rpcId is marked initiatedBy="bridge"; unmatched messages are external_or_unknown. This is a freshness check, not a transaction: DSH Web can still race between preflight and the write.
dsh_cancel(scope="turn") calls rc.6 session.cancel. It cancels only the active root turn with queued inbox work preserved. Built-in foreground shell tools use cooperative abort and escalate the foreground process group from SIGTERM to SIGKILL after about three seconds, but:
run_in_backgroundjobs are not killed by that turn signal and require DSHjob_kill.- Third-party tools are cancellable only when they honor
AbortSignal.
dsh_cancel(scope="queue") consumes the current mux queue snapshot and issues one session.updateQueue(remove) per item id. It is non-atomic. The result separates requested, removed, alreadyClaimed, and failed; it never promises all-or-nothing queue clearing.
Workspace coordination
dsh_delegate resolves the requested cwd through realpath and acquires a persistent workspace claim. The default is exclusive-write; read-only permits overlapping bridge-local read-only claims, while any overlapping ancestor or descendant exclusive claim conflicts. Claims are shared across bridge processes using the same bridge home and remain after turn/end, because a person can continue the session later from DSH Web. This is cooperative coordination only: workspaceMode does not select, enforce, or verify the DSH Host filesystem sandbox, and agentPreset names DSH agent composition rather than a workspace or permission policy. Tool responses expose workspaceClaimSemantics.controlsDshSandbox=false so callers can present this boundary without parsing prose.
Release a claim only with dsh_release_workspace. Release does not close or cancel the DSH session. Follow-up, question answers, and allow_once require the task's claim to remain active; safety cancellation and approval rejection stay available without it.
The claim is cooperative. It prevents conflicting delegations seen by this bridge store, but cannot stop DSH Web, another bridge home, the caller, a shell, or an editor from writing files. A supervising caller must not edit an exclusively claimed cwd. A dedicated git worktree per writable delegation is the recommended strong isolation boundary.
If claim acquisition races after DSH session creation, the bridge returns a conflict identifying the unprompted session/task mapping; it does not silently run without a claim.
Explicit limitations
- No Host process lifecycle management, authentication layer, pidfile, port lock, or automatic Host start.
- No automatic retry of non-idempotent writes (
session.create, prompt/follow-up, cancel, queue mutation,/api/respond). - A WebSocket disconnect produces
host_unreachable/unknown, not task failure; read-only reconnect/history recovery continues automatically. - Host restart loses its process-local active turn, pending interactions, queue, and background-job state. The bridge does not claim seamless continuation.
- If fresh history still ends at
turn/startbut freshsession.listsays the session is no longer running, status records a content-freeinterruptedcoordination marker. A later durableturn/endsupersedes it during reconciliation. - DSH durable session/history can survive a Host restart, but a created-only zero-event session may be lazily absent. The bridge process-restart mock test uses a session with durable events; no live rc.6 restart was performed by the implementation run.
- Queue state is unknown after mux connect or reconnect until rc.6 emits an actual
session/queuesnapshot. The bridge does not infer an empty queue fromsession/subscribed. - Ordinary user-created session forks are not folded into a BridgeTask; session-backed subagent descendants are.
- Host-origin affinity is configuration-scoped rather than stored in the strict task mapping. Do not reuse one
DSH_BRIDGE_HOMEafter changingDSH_HOST_URL; per-task cross-Host migration is unsupported. - Workspace claims do not provide OS-level exclusion, and fresh write preflight cannot eliminate a Web-client time-of-check/time-of-use race. Full simultaneous multi-caller plus interactive-Web conflict freedom is not claimed.
- Exactly-once delivery, atomic queue clear,
events.mux.sinceresume, argument-dependent caller approval policy, automatic background-job cancellation, and Host-package detection throughhost.describe.versionare unsupported. - Real browser-visible end-to-end interaction is an operator acceptance step, not part of
npm test. Follow the validation guide after changing DSH, the model route, the agent preset, or bridge transport behavior.
See Known issues for current source-preview defects and operational workarounds.