Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
466734d
docs: revise local agent ownership around daemon
Waishnav Aug 11, 2026
5aeaa56
refactor: add durable local agent runtime core
Waishnav Aug 11, 2026
c4e821a
refactor: make local agent core host-independent
Waishnav Aug 11, 2026
abeb678
feat: add standalone local agent daemon lifecycle
Waishnav Aug 11, 2026
d17898a
feat: add local agent daemon IPC protocol
Waishnav Aug 11, 2026
9eb9b91
refactor: move manager and store ownership into agent daemon
Waishnav Aug 11, 2026
ed7b41b
refactor: route CLI agent commands through daemon client
Waishnav Aug 11, 2026
ad1f0e9
docs: route MCP agent work through the daemon
Waishnav Aug 11, 2026
cee6840
test: cover daemon startup shutdown and recovery
Waishnav Aug 11, 2026
e34e41f
feat: bound daemon shutdown and recovery
Waishnav Aug 11, 2026
6cf9b71
docs: document daemon recovery and diagnostics
Waishnav Aug 11, 2026
42c5bd1
chore: sync daemon package metadata
Waishnav Aug 11, 2026
414864e
fix: harden local agent runtime pooling
Waishnav Aug 11, 2026
037c78b
refactor: make daemon agent ownership explicit
Waishnav Aug 11, 2026
b825a3d
fix: harden local agent daemon ownership
Waishnav Aug 12, 2026
d8223d3
fix: serialize local agent session release
Waishnav Aug 12, 2026
bf266e1
fix: enforce workspace scoped agent access
Waishnav Aug 12, 2026
95f2962
test: prove daemon ownership and socket shutdown
Waishnav Aug 12, 2026
03be064
test: make daemon ownership check cross-platform
Waishnav Aug 12, 2026
2c2d8a3
fix: force provider cleanup during daemon shutdown
Waishnav Aug 12, 2026
204272d
fix: enforce combined workspace list scopes
Waishnav Aug 12, 2026
dafa843
fix: close timed-out daemon client sockets
Waishnav Aug 12, 2026
124638e
fix: publish daemon lock ownership atomically
Waishnav Aug 12, 2026
9f23b7c
fix: harden runtime acquisition and turn tracking
Waishnav Aug 12, 2026
ed67a53
fix: coordinate runtime shutdown with session release
Waishnav Aug 12, 2026
22a5ebf
fix: block new session releases during shutdown
Waishnav Aug 12, 2026
b99b32b
refactor: require client agent list scope
Waishnav Aug 12, 2026
77a5f10
refactor: avoid provider probes in availability checks
Waishnav Aug 12, 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
15 changes: 11 additions & 4 deletions docs/agent-profile-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

DevSpace agent profiles are user-owned markdown files with YAML
frontmatter. They describe roles such as reviewer, explorer, or implementer.
DevSpace owns provider invocation.
The internal on-demand `devspace-agentd` process owns provider invocation. The
CLI and MCP server use it as clients when they need agent execution.

Profiles are discovered from:

Expand Down Expand Up @@ -133,7 +134,8 @@ The Subagent skill teaches only:

```bash
devspace agents ls
devspace agents run <profile-or-id> "<prompt>"
devspace agents run <profile-or-provider> "<prompt>"
devspace agents continue <id> "<prompt>"
devspace agents show <id>
```

Expand All @@ -152,6 +154,10 @@ devspace agents show <id>
`devspace agents ls` lists existing subagent sessions for the current workspace;
it does not list profile definitions.

Use `devspace agents continue <id>` for a later turn. The logical agent ID is
the `agt_...` value returned by `run` or `ls`; provider session IDs are not
accepted as substitutes.

The full profile body stays out of the model context until DevSpace launches the
profile.

Expand All @@ -161,5 +167,6 @@ profile.
- Inferring changed files, tests, or diffs from worker output.
- Exposing raw provider transcripts by default.
- Teaching the model provider-specific CLIs.
- First-class MCP agent tools. Future tools should wrap the same provider
adapter registry used by `devspace agents`.
- First-class MCP agent tools. Future tools should call the same local agent
daemon used by `devspace agents` rather than executing providers in the MCP
server process.
3 changes: 2 additions & 1 deletion docs/chatgpt-coding-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ Skill paths may be outside the workspace. DevSpace only permits reading:
Set `DEVSPACE_SKILLS=0` to hide skills from workspace output. Set
`DEVSPACE_SUBAGENTS=1` to expose the experimental subagent catalog and
`subagent-delegation` skill. That skill teaches the minimal
`devspace agents ls`, `devspace agents run`, and `devspace agents show`
`devspace agents ls`, `devspace agents run`, `devspace agents continue`, and
`devspace agents show`
workflow. The catalog comes from `open_workspace`; `devspace agents ls` lists
existing subagent sessions for that workspace.

Expand Down
3 changes: 2 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ agent without reading provider-specific launch details. `devspace agents ls`
lists existing subagent sessions for the current workspace, scoped by the
workspace environment injected into shell commands. The `subagent-delegation`
skill teaches the model to use only the minimal `devspace agents ls`,
`devspace agents run`, and `devspace agents show` workflow.
`devspace agents run`, `devspace agents continue`, and `devspace agents show`
workflow.

Starter profile templates are available under `examples/agents/`. Copy or adapt
them into one of the active profile directories before use.
Expand Down
5 changes: 4 additions & 1 deletion docs/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,10 @@ When `DEVSPACE_SUBAGENTS=1`, DevSpace loads agent profiles from
`~/.devspace/agents/*.md` and project `.devspace/agents/*.md`, then exposes a
compact profile catalog through `open_workspace`. The bundled
`subagent-delegation` skill keeps the model-facing workflow to
`devspace agents ls`, `devspace agents run`, and `devspace agents show`.
`devspace agents ls`, `devspace agents run`, `devspace agents continue`, and
`devspace agents show`.
Those commands automatically manage the internal local agent daemon; `devspace
serve` is not a prerequisite.
`devspace agents ls` lists existing subagent sessions, not profile
definitions.

Expand Down
54 changes: 54 additions & 0 deletions docs/local-agent-daemon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Local agent daemon

Local agent execution is owned by an on-demand `devspace-agentd` process, not
by the MCP server and not by an individual CLI invocation. The daemon is an
internal implementation detail: the normal workflow remains:

```text
devspace agents run/continue/show/ls
devspace-agentd
├── LocalAgentManager
├── LocalAgentStore
├── LocalAgentRuntimePool
└── provider runtimes
```

The CLI starts the daemon automatically when an agent command needs it. The
MCP server can use the same local client when an MCP operation needs agent
functionality, but `devspace serve` is not required for local-agent execution.
The daemon is scoped to one DevSpace `stateDir`, so one SQLite store and one
runtime owner serve all clients using that configuration.

Communication uses a private Unix domain socket on Linux/macOS or a named pipe
on Windows. The endpoint is not exposed through the public MCP HTTP port.
Provider session identifiers and logical agent records are durable; live
provider runtimes are disposable and may be recreated after a daemon restart.

The daemon state directory contains the socket or pipe identity, an atomic
lock, a PID marker, and diagnostic logs. A second client cannot start another
daemon for the same state directory. Stale lock and socket files are recovered
only after the recorded PID is no longer alive.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

The daemon is started on demand and may exit after its active turns, clients,
and warm runtime idle periods have ended. Users do not need to manage it during
normal operation. Diagnostic commands are available for startup, process, and
cleanup problems:

```bash
devspace agents daemon status
devspace agents daemon stop
devspace agents daemon logs
```

Agent identity is explicit at the client boundary. `agents run` starts a new
logical agent from a profile or provider; `agents continue <id>` continues an
existing logical agent. Provider session IDs are never accepted as logical
agent IDs, and the daemon does not resolve ambiguous prefixes.

Shutdown gives active turns a bounded graceful window. If that window expires,
the process exits with active records left durable; the next daemon startup
reconciles stale `starting` and `running` records to `error` without discarding
their `providerSessionId` or `latestResponse`.
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"node": ">=22.19 <27"
},
"bin": {
"devspace": "dist/cli.js"
"devspace": "dist/cli.js",
"devspace-agentd": "dist/local-agent-daemon-main.js"
},
"files": [
"dist",
Expand All @@ -28,7 +29,7 @@
"dev": "node scripts/dev-server.mjs",
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
"start": "node dist/cli.js serve",
"test": "tsx src/config.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
"test": "tsx src/config.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-daemon-lifecycle.test.ts && tsx src/local-agent-daemon-protocol.test.ts && tsx src/local-agent-daemon.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/local-agent-manager.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"keywords": [],
Expand Down
10 changes: 9 additions & 1 deletion skills/subagent-delegation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ Use only these commands for normal delegation:

```bash
devspace agents ls
devspace agents run <profile-or-provider-or-id> "<prompt>"
devspace agents run <profile-or-provider> "<prompt>"
devspace agents continue <id> "<prompt>"
devspace agents show <id>
```

`ls` shows existing subagent sessions for the current workspace. DevSpace scopes
it automatically from the shell environment injected by the workspace tool.
Use the returned logical `agt_...` ID with `continue`; provider session IDs and
prefixes are not interchangeable with logical agent IDs.

`run <profile> "<prompt>"` starts a new configured profile and prints a
DevSpace agent id.
Expand All @@ -37,6 +40,11 @@ profile is needed. Built-in providers are listed by `open_workspace`.
running, `show` waits briefly. If there is still no final response, call `show`
again later.

The commands automatically start the internal `devspace-agentd` process when
needed. `devspace serve` is not required for local-agent execution. The daemon
owns shared agent sessions and provider runtimes for the configured DevSpace
state directory.

Do not run provider CLIs such as `codex`, `claude`, `opencode`, `pi`,
`cursor-agent`, or `copilot` directly unless you are explicitly debugging
DevSpace agent integration.
Expand Down
82 changes: 65 additions & 17 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { execFile, execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { createServer as createNetServer } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { loadConfig } from "./config.js";
import { localAgentDaemonPaths } from "./local-agent-daemon-lifecycle.js";
import { encodeLocalAgentDaemonResponse } from "./local-agent-daemon-protocol.js";
import { LocalAgentStore } from "./local-agent-store.js";

const execFileAsync = promisify(execFile);

const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as {
version: string;
};
Expand Down Expand Up @@ -65,24 +71,66 @@ try {
);
store.close();

const output = execFileSync("node", ["--import", "tsx", "src/cli.ts", "agents", "ls"], {
cwd: process.cwd(),
encoding: "utf8",
env: {
...process.env,
DEVSPACE_CONFIG_DIR: configDir,
DEVSPACE_ALLOWED_ROOTS: projectRoot,
DEVSPACE_STATE_DIR: stateDir,
DEVSPACE_WORKSPACE_ID: "ws_current",
DEVSPACE_WORKSPACE_ROOT: projectRoot,
DEVSPACE_SUBAGENTS: "1",
DEVSPACE_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough",
},
const daemonSocket = localAgentDaemonPaths(stateDir).endpoint;
const daemon = createNetServer((socket) => {
let buffer = "";
socket.setEncoding("utf8");
socket.on("data", (chunk: string | Buffer) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline === -1) return;
const request = JSON.parse(buffer.slice(0, newline)) as { requestId: string; method: string };
const result = request.method === "agent.list"
? [current]
: request.method === "hello"
? {
state: "ready",
protocolVersion: 1,
pid: process.pid,
endpoint: daemonSocket,
startedAt: "now",
activeTurns: 0,
runtimeCount: 0,
clientConnections: 1,
}
: null;
socket.end(encodeLocalAgentDaemonResponse({
requestId: request.requestId,
protocolVersion: 1,
ok: true,
result,
}));
});
});
await new Promise<void>((resolveListen, rejectListen) => {
daemon.once("error", rejectListen);
daemon.listen(daemonSocket, resolveListen);
});

try {
const { stdout: output } = await execFileAsync("node", ["--import", "tsx", "src/cli.ts", "agents", "ls"], {
cwd: process.cwd(),
encoding: "utf8",
env: {
...process.env,
DEVSPACE_CONFIG_DIR: configDir,
DEVSPACE_ALLOWED_ROOTS: projectRoot,
DEVSPACE_STATE_DIR: stateDir,
DEVSPACE_WORKSPACE_ID: "ws_current",
DEVSPACE_WORKSPACE_ROOT: projectRoot,
DEVSPACE_SUBAGENTS: "1",
DEVSPACE_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough",
},
});

assert.match(output, new RegExp(`${current.id} idle reviewer codex gpt-5\\.4 thinking=high`));
assert.doesNotMatch(output, /profile reviewer/);
assert.doesNotMatch(output, new RegExp(other.id));
assert.match(output, new RegExp(`${current.id} idle reviewer codex gpt-5\\.4 thinking=high`));
assert.doesNotMatch(output, /profile reviewer/);
assert.doesNotMatch(output, new RegExp(other.id));
} finally {
await new Promise<void>((resolveClose, rejectClose) => {
daemon.close((error) => error ? rejectClose(error) : resolveClose());
});
}

assert.equal(loadConfig({
DEVSPACE_CONFIG_DIR: configDir,
Expand Down
Loading
Loading