P2 closes the App Server/Desktop execution slice between the application model
and the existing Agent kernel. CLI and Desktop share the Agent assembly, policy
engine, and canonical SessionStore, while retaining independent client and
lifecycle adapters.
Desktop JSON-RPC adapter
│
▼
TurnService ───────────────▶ ApprovalService
│ │
▼ ▼
SessionRuntimeRegistry SQLite projection
│
▼
AgentSessionFactory
│
▼
build_agent_session() ──▶ AgentSession ──▶ AgentRunner / tools / provider
│
└──────────────────────────────▶ canonical SessionStore JSONL
CLI/TUI/MCP ──▶ build_agent_session() + SessionStore (no TurnService dependency)
TurnServiceowns durable Turn orchestration and terminal-state guarantees.ExecutionRegistryowns one lazy background asyncio loop and a bounded semaphore. The stdio request thread never runs provider or tool work.SessionRuntimeRegistryretains oneAgentSessionfor each loaded Desktop Thread. Turns reuse that Session until eviction or application shutdown.AgentSessionFactoryis the only application-to-kernel adapter. Production usesbuild_agent_session(); tests inject deterministic sessions.TurnEventProjectortranslates SQ/EQ events into Items. It knows no JSON-RPC or Tauri types.ApprovalServiceowns both the durable approval record and the in-memory future that resumes the exact suspended tool call.SessionStoreis canonical for Session identity and visible conversation. SQLite Items and the event log are the rebuildable Desktop execution projection. Live delivery may overflow; clients recover throughevent/replayandturn/read.
turn/start performs one short BEGIN IMMEDIATE transaction:
- Read Thread and Project.
- Require a trusted Project and re-resolve the workspace inside the Project boundary.
- Reject an archived Thread or another active Turn in the same Thread.
- Insert a queued Turn and completed UserMessage Item.
- Move the Thread to running and append durable projection events.
- Commit, publish the events, then enqueue the background execution.
The registry defaults to two concurrent Turns across different Threads. A Turn
waiting for the semaphore remains queued. Cancelling such a Turn invokes a
pre-start cancellation callback so it cannot remain queued forever.
When execution begins, TurnService marks the Turn running, acquires the Thread's long-lived AgentSession, and appends the user prompt to canonical JSONL before running it. A newly loaded runtime takes history from the same SessionStore; when another DeepCode process has appended visible messages, the runtime reloads that history before the next Turn. The projector maps SQ/EQ events as follows:
| SQ/EQ event | Durable projection |
|---|---|
agent_message_delta |
Coalesced in-progress AssistantMessage Item |
agent_message |
Completed AssistantMessage projection |
| file write/edit tool | FileChange Item |
| shell command | CommandExecution Item |
| other tool | ToolCall Item |
error |
failed Error Item |
task_complete |
Completion Item and terminal Turn decision |
Delta writes are coalesced by time/size and the final message always overwrites the draft, avoiding one SQLite transaction per token. A terminal assistant message is also appended once to canonical JSONL.
Every path ends in exactly one of completed, failed, or interrupted.
Missing task_complete, factory/provider exceptions, projection failures,
explicit interrupt, queued cancellation, and application shutdown are all
converted to a durable terminal state. _finish() is idempotent, so races
between a user interrupt and normal completion cannot create two terminal
transitions.
CLI and Desktop now share a product-level Session access selection: ask,
read_only, or full_access. The canonical Session stores an optional
override; Turn admission resolves it with the configured default and persists
one immutable ExecutionSecurityProfile. Workers consume that snapshot rather
than choosing a client-specific default. Legacy permissionMode, sandbox, and
environment settings remain supported for older direct/batch integrations and
are never relabelled as product Full Access.
Ask and Read-only profiles retain the sensitive-path guard. Explicitly confirmed Full Access disables that guard together with the command sandbox and workspace write fence; explicit deny rules still take precedence.
When the permission engine returns ask, the AgentRunner awaits the callback
provided by TurnService. ApprovalService atomically:
- creates a pending ApprovalRequest Item and Approval record;
- changes the Turn to
waiting_approvaland Thread towaiting; - appends
item.created,approval.requested,turn.updated, andthread.status_changedevents; - suspends on an asyncio Future associated with that approval ID.
approval/respond accepts only approved_once, approved_session, or
denied. It updates Approval, Item, Turn, Thread, and events in one transaction,
publishes those events, then resolves the suspended Future on its owning loop.
Session grants are scoped to a Thread and tool name. Interrupting a waiting Turn
marks the Approval cancelled and the approval Item declined before the Turn
settles.
The frontend never authorizes a tool directly. It only submits a decision; the Python backend remains the enforcement boundary.
Access changes govern newly admitted Turns. Executing and queued Turns keep their immutable snapshots; both clients present those frozen states separately from the Session selection used by new submissions.
turn/interrupt cancels the registry future. Cancellation propagates through
the event consumer, AgentSession, AgentRunner, approval wait, and active tool.
AgentSession preserves the original submission correlation on the terminal
task_complete(interrupted) event.
Shell, code-mode, and hook commands start in a DeepCode-owned process group. On
timeout or cancellation the runtime sends group SIGTERM, waits briefly, then
sends SIGKILL to surviving descendants. Windows uses a new process group and
taskkill /T /F. ToolRegistry and delegated AgentControl resources remain
owned by the loaded AgentSession across Turns and are closed when that Session
is evicted or the application exits.
Application shutdown drains all tasks before stopping the background event loop. This includes queued cancellation callbacks and running Turn finalizers. After jobs settle, every live AgentSession is closed on that same loop so its AgentControl, tools, hooks, and child resources receive one lifecycle end.
CLI and Desktop inherit one timeout policy from AgentRunner and the provider
adapters:
- Non-streaming model calls have a 300-second wall-clock deadline, configurable
with
DEEPCODE_LLM_TIMEOUT_S. - Streaming calls use an activity deadline instead of that short wall-clock
deadline. Every provider event renews the default 90-second idle window,
including reasoning events. Provider-returned reasoning is projected through
a typed channel separate from assistant text; each client decides whether to
collapse or display it. Configure the idle window with
DEEPCODE_STREAM_IDLE_TIMEOUT_S. - Active streams have no total runtime limit by default because token limits,
interruption, and the idle deadline already bound normal execution. Operators
can add a hard ceiling with
DEEPCODE_LLM_STREAM_MAX_RUNTIME_S; a non-positive value disables it. - An explicitly supplied
llm_timeout_sremains a per-call hard ceiling for compatibility with bounded automation callers.
This separation prevents long reasoning and tool-continuation Turns from being cancelled merely because they remain active for more than five minutes, while a genuinely stalled connection still fails and enters the normal retry policy.
SessionStart therefore fires once per loaded AgentSession, not once per Turn;
UserPromptSubmit and approval context remain Turn-scoped. This matches the
established CLI session lifecycle without coupling CLI command routing to the
App Server.
At application open, before accepting requests, recover_incomplete() scans
queued, running, and waiting-approval Turns left by the previous process. In one
transaction per startup pass it:
- cancels pending approvals with reason
application_restarted; - marks pending/in-progress Items failed or declined;
- appends a failed Completion Item;
- marks each Turn interrupted and its Thread idle;
- appends item, approval, thread, and
turn.recoveredevents.
Recovery never replays a side effect automatically. A future retry feature must start a new Turn or resume from an explicit workflow checkpoint. Recovery repairs only disposable Desktop runtime state; it does not rewrite or replace canonical Session identity or existing JSONL messages.
P2 adds:
turn/start
turn/read
turn/interrupt
approval/respond
turn/start and turn/read return a complete snapshot containing the Turn,
ordered Items, and Approvals. Event notifications use the same canonical Event
shape and can be replayed by per-Thread sequence. The schema remains the only
source for generated desktop TypeScript contracts.
Automated coverage includes successful projection, streaming coalescence, approval/resume, approval cancellation, running and queued interruption, bounded cross-Thread concurrency, process-tree cleanup, crash recovery, stable SQ/EQ correlation, and the full stdio JSON-RPC approval/file-change/replay flow.
P2 intentionally does not include the Rust sidecar supervisor or production
React UI; those are P3. It also does not implement turn/steer, Git/diff/file
read APIs, workflow checkpoints, terminal PTYs, or Artifact payload storage.
Large raw tool output is still summarized by the existing Agent event adapter;
moving expandable full output into Artifact storage belongs to a later phase.