Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e3f1418
refactor(coding-agent): daemon back all client modes (ENG-4685)
kevinjosethomas Jul 17, 2026
03024af
fix(coding-agent): harden daemon client modes (ENG-4685)
kevinjosethomas Jul 17, 2026
bed74cd
fix(coding-agent): address daemon client review findings (ENG-4685)
kevinjosethomas Jul 17, 2026
7cd5af8
fix(coding-agent): close daemon client review gaps (ENG-4685)
kevinjosethomas Jul 17, 2026
35b62c8
fix(coding-agent): preserve daemon client ordering (ENG-4685)
kevinjosethomas Jul 17, 2026
4ae3d53
fix(coding-agent): validate rpc command shapes (ENG-4685)
kevinjosethomas Jul 17, 2026
77b9739
Merge remote-tracking branch 'origin/main' into eng-4685-daemon-clien…
kevinjosethomas Jul 17, 2026
797c4dd
fix daemon worker adoption
kevinjosethomas Jul 17, 2026
b9bfe95
Merge remote-tracking branch 'origin/main' into eng-4685-daemon-clien…
kevinjosethomas Jul 17, 2026
e7f97c7
fix daemon owned-session authorization (fixes #456)
kevinjosethomas Jul 17, 2026
3444560
hide client-owned daemon sessions (fixes #456)
kevinjosethomas Jul 17, 2026
f61ef04
route stale daemon clients locally (fixes #456)
kevinjosethomas Jul 17, 2026
4ad36b2
Merge branch 'main' into eng-4685-daemon-client-modes
kevinjosethomas Jul 17, 2026
afa1b5c
fix(coding-agent): drain rpc prompts before eof
kevinjosethomas Jul 17, 2026
61100c6
merge main into eng-4685-daemon-client-modes
kevinjosethomas Jul 17, 2026
87db6ad
fix(coding-agent): preserve rpc prompt streaming
kevinjosethomas Jul 17, 2026
a7ffbe7
fix(coding-agent): preserve prompt wait calls
kevinjosethomas Jul 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## [Unreleased]

- Changed interactive, print, JSON, RPC, piped-stdin, and no-session clients to use the same daemon-owned runtime while preserving their existing commands, output protocols, and lifecycle behavior ([ENG-4685](https://linear.app/primeintellect/issue/ENG-4685)).
- Added RPC controls for schedules, heartbeats, agent messaging, and live session observation ([ENG-4685](https://linear.app/primeintellect/issue/ENG-4685)).
- Fixed daemon-backed headless startup, rollback routing, RPC wire compatibility, and duplicate client runtime preparation ([ENG-4685](https://linear.app/primeintellect/issue/ENG-4685)).
- Changed the fullscreen follow shortcut from `Alt+Down` to `Ctrl+Shift+Down` for more reliable terminal input ([ENG-4684](https://linear.app/primeintellect/issue/ENG-4684/altdown-doesnt-work)).
- Fixed the Agents View reordering sessions whenever prompts or heartbeats updated their activity timestamps ([ENG-4650](https://linear.app/primeintellect/issue/ENG-4650/agents-view-shifts-session-list-constantly)).
- Added parent-scoped subagent lifecycle APIs: create children with readable default or orchestrator-chosen names, recover running or completed children through `rlm.list_subagents()`, continue them through agent messaging, and close/remove them with `rlm.delete_subagent()`.
Expand Down
53 changes: 27 additions & 26 deletions packages/coding-agent/docs/daemon-implementation-summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,14 @@ Prime Agent isolates each persistent root session tree in its own process. Proce
## Process Topology

```text
interactive clients
|
v
detached supervisor
|- catalog subprocess
|- resident worker: root A + RLM descendants + kernels
`- resident worker: root B + RLM descendants + kernels

print / JSON / RPC / interactive --no-session
|
v
frontend process --private owner channel--> owned worker + root tree
interactive / print / JSON / RPC clients
|
v
detached supervisor
|- catalog subprocess
|- resident worker: root A + RLM descendants + kernels
|- resident worker: root B + RLM descendants + kernels
`- client-owned worker: hidden root + RLM descendants + kernels
```

The supervisor owns public sockets, client attachments, routing, global agent-message routing, worker health, and update coordination. Each resident worker owns scheduling and execution for its root tree. The supervisor does not execute providers, tools, compaction, bash, kernels, session schedules, or session-history scans.
Expand All @@ -41,25 +37,27 @@ Normal interactive sessions use resident workers:

There are no session, worker, client, or workload caps in this layer.

## Owned Workers
## Client-Owned Workers

Headless and ephemeral modes use the same process isolation without joining the resident daemon registry:
Headless and ephemeral clients use the same supervisor, worker process, `AgentSessionRuntime`, extension binding, scheduler, RLM host, and kernel lifecycle as interactive clients. Their workers have a client-owned lifecycle:

- `prime-agent -p`, piped stdin, and JSON mode proxy stdin/stdout/stderr through an owned worker and return its existing exit status.
- RPC keeps stdin/stdout in the frontend. It forwards strict LF-delimited JSONL to the worker and preserves every public command, response, event, and extension-UI shape.
- If a persisted RPC worker crashes, the frontend reports an ordinary failure for commands whose result is uncertain, reopens the exact persisted session, and continues accepting commands. It never replays an uncertain command.
- RPC EOF and frontend `SIGTERM`/`SIGHUP` stop only the owned worker and its subprocesses.
- Interactive `--no-session` uses an owned in-memory worker and creates no recovery descriptor.
- Owned workers never appear in the agents view and never require the global daemon.
- `prime-agent -p`, piped stdin, and JSON mode remain one-shot clients and return their existing output and exit status.
- RPC retains strict LF-delimited JSONL framing, accepts repeated prompts until EOF, and preserves every public command, response, event, and extension-UI shape.
- Interactive `--no-session` uses a client-owned in-memory session.
- Normal completion or RPC EOF explicitly removes the worker without archiving its session. `SIGTERM` and `SIGHUP` retain their existing exit codes.
- Unexpected client loss starts a bounded cleanup grace period. Reconnection with the same stable protocol client ID cancels cleanup and reattaches to the same worker.
- The supervisor can recover a crashed client-owned worker while the owner remains connected. After supervisor replacement, the owner resupplies its launch environment before a dead worker is restarted.
- Default session listings, the agents view, global peer routing, and global schedule aggregation omit client-owned workers. Owner-scoped heartbeat and cron operations still reach the hidden worker.
- The full launch environment is held only in supervisor memory and is never written to the worker descriptor.
- Direct SDK calls to `runPrintMode(runtime, ...)` and `runRpcMode(runtime)` remain in-process and source-compatible. Non-serializable extension factories therefore continue to work for embedders.

An inherited, unreferenced IPC owner channel links the worker to its frontend. Frontend loss disconnects the channel and terminates the worker, which reaps its kernels and detached children; the unreferenced channel does not prevent clean RPC EOF or print completion.
The daemon is infrastructure, not a client mode. `interactive`, `print`, `json`, and `rpc` describe client behavior; the retained `--mode daemon` command is the compatible internal process entrypoint used to start the supervisor and session workers.

## Session Ownership

Persisted sessions use process-safe leases keyed by canonical JSONL path. Resident and owned workers both acquire leases.
Persisted sessions use process-safe leases keyed by canonical JSONL path. Resident and client-owned workers both acquire leases.

- Resident workers acquire a target lease before opening an existing session. Owned CLI workers acquire it before runtime writes, and all workers acquire the replacement lease before switching runtimes.
- Workers acquire a target lease before opening an existing session and acquire the replacement lease before switching runtimes.
- The previous lease is released only after replacement succeeds.
- Concurrent opens return `session_already_active` with the owning active-session ID.
- Concurrent daemon creates for the same path share one worker launch.
Expand All @@ -68,7 +66,7 @@ This prevents a script and resident daemon worker from writing the same session

## Scheduled Jobs

Each resident worker runs one scheduler for the root and RLM descendant sessions it owns. Schedule state is persisted per session at `session-artifacts/<session-id>/scheduled-jobs.json`; workers never scan or write a shared global cron file.
Each worker runs one scheduler for the root and RLM descendant sessions it owns. Schedule state is persisted per session at `session-artifacts/<session-id>/scheduled-jobs.json`; workers never scan or write a shared global cron file.

- Creating or changing a heartbeat writes its session store and wakes the same worker's scheduler.
- Due ticks are claimed and advanced durably before prompt delivery, so a crash never replays an uncertain prompt.
Expand All @@ -80,9 +78,9 @@ Each resident worker runs one scheduler for the root and RLM descendant sessions

The first worker-owned release migrates the legacy global `cron-jobs.json` into the corresponding session artifact directories before adopting workers. Archived, deleted, or descriptorless sessions are cancelled rather than revived.

## Public Protocol v2
## Public Protocol v3

The public daemon socket remains JSONL-framed. Version 2 adds:
The public daemon socket remains JSONL-framed. Versions 2 and 3 add:

- stable command envelopes carrying protocol version, client ID, and command ID;
- generation-aware event cursors `{ generation, sequence }`;
Expand All @@ -91,6 +89,9 @@ The public daemon socket remains JSONL-framed. Version 2 adds:
- attach acknowledgement followed by `session_snapshot_begin`, `session_snapshot_chunk`, and `session_snapshot_end`;
- 512 KiB target transcript chunks;
- file-backed transcript caches above 4 MiB.
- client-owned worker creation, completion, and promotion;
- owner-scoped recovery with in-memory launch environments;
- daemon-side headless completion, session-header, synchronous bash, and retry-setting commands.

The one-release v1 update handoff uses raw v1 requests only to prepare and stop the old daemon. Busy older daemons that cannot produce a recovery manifest are left running.

Expand Down
40 changes: 40 additions & 0 deletions packages/coding-agent/docs/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,46 @@ Response:

Messages are `AgentMessage` objects (see [Message Types](#message-types)).

### Daemon Coordination

RPC clients can use the same daemon-owned coordination features as the interactive client. These commands are additive; existing commands and event shapes are unchanged.

| Command | Fields | Successful response data |
|---------|--------|--------------------------|
| `send_message` | `targetActiveSessionId`, `message`, optional `deliveryMode` (`auto`, `steer`, `follow_up`) | Agent-message delivery receipt |
| `agent_messages_status` | none | Messaging safety status |
| `agent_messages_pause` | none | Updated messaging safety status |
| `agent_messages_resume` | none | Updated messaging safety status |
| `agent_messages_clear` | none | `{ "cleared": number }` |
| `list_schedules` | optional `includeInactive` | `{ "jobs": [...] }` |
| `add_schedule` | `schedule`, `prompt` | `{ "job": {...} }` |
| `cancel_schedule` | `jobId` | `{ "job": {...} }` |
| `list_heartbeats` | none | `{ "heartbeats": [...] }` |
| `get_heartbeat` | none | `{ "heartbeat": {...} }` or `null` |
| `set_heartbeat` | `schedule`, `prompt`, optional `deliveryMode` (`steer`, `follow_up`) | `{ "heartbeat": {...} }` |
| `update_heartbeat` | `action` (`pause`, `resume`, `clear`) | `{ "heartbeat": {...} }` or `null` |
| `manage_heartbeat` | `activeSessionId`, `jobId`, `action` (`pause`, `resume`, `stop`) | `{ "heartbeat": {...} }` |

Adding a schedule or heartbeat promotes an invocation-local RPC session to a resident daemon session so the scheduled work remains available after RPC stdin closes.

Use `observe` to subscribe to another active root or subagent session:

```json
{"id":"watch-1","type":"observe","activeSessionId":"target-session-id"}
```

The response contains the target's current messages. Later target events are wrapped so they cannot be confused with events from the RPC client's own session:

```json
{"type":"observed_session_event","activeSessionId":"target-session-id","event":{"type":"agent_start"}}
```

When the target closes, RPC emits `observed_session_closed`. Stop watching explicitly with:

```json
{"type":"unobserve","activeSessionId":"target-session-id"}
```

### Model

#### set_model
Expand Down
10 changes: 6 additions & 4 deletions packages/coding-agent/src/cli-main.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { enableCompileCache } from "node:module";
import { maybeStartInteractiveDaemonEarly } from "./cli/daemon-launch.js";
import { maybeStartDaemonEarly } from "./cli/daemon-launch.js";
import {
closeOwnedSessionWorkerOwnerWatch,
installOwnedSessionWorkerOwnerWatch,
isOwnedSessionWorkerProcess,
maybeRunOwnedSessionWorkerFrontend,
} from "./cli/owned-session-worker.js";
import { APP_NAME } from "./config.js";
Expand All @@ -18,13 +19,14 @@ export async function runCli(): Promise<void> {
process.env.PI_CODING_AGENT = "true";
process.emitWarning = (() => {}) as typeof process.emitWarning;

// Boot a cold daemon concurrently with this process's heavy imports.
maybeStartInteractiveDaemonEarly(process.argv.slice(2));

installOwnedSessionWorkerOwnerWatch();

const handledByOwnedWorker = await maybeRunOwnedSessionWorkerFrontend(process.argv.slice(2));
if (!handledByOwnedWorker) {
if (!isOwnedSessionWorkerProcess()) {
// Boot a cold daemon concurrently with this process's heavy imports.
maybeStartDaemonEarly(process.argv.slice(2));
}
const [{ EnvHttpProxyAgent, setGlobalDispatcher }, { main }] = await Promise.all([
import("undici"),
import("./main.js"),
Expand Down
Loading