Skip to content

feat(agent): one PGX branch per Claude Code subagent, routed automatically - #35

Merged
alexshapalov merged 2 commits into
mainfrom
feat/subagent-isolation
Oct 26, 2026
Merged

alexshapalov merged 2 commits into
mainfrom
feat/subagent-isolation

Conversation

@alexshapalov

Copy link
Copy Markdown
Contributor

Why

The 2026-10-26 multi-agent benchmark found that Claude Code subagents all shared the parent session's PGX branch: branches were keyed on CLAUDE_CODE_SESSION_ID, which every subagent inherits, and nothing handled SubagentStart/SubagentStop or the agent_id Claude Code puts in a subagent's PreToolUse payload. Three subagents, one database. A second finding: a killed agent's open transaction committed eight minutes later (PostgreSQL defaults never notice a dead client inside a long statement).

What

Identity. (project, host, session_id, agent_id). A subagent's branch is agent-<host>-<session8>-<agent8>; the parent keeps its name and existing records (no migration). Short-name collisions fall back to a 16-hex hash of the full identity; the longest name is 57 bytes.

Hooks (Claude Code). pgx agent install claude now writes six entries (an older four-entry install is completed in place):

  • SubagentStart → the subagent's own branch; its context line names the branch, no credentials.
  • PreToolUse (Bash) with agent_id → updatedInput: <pgx> agent exec --agent <id> --cwd <project> -- <shell> -c '<original>' # pgx-routed <branch>. The original text is one quoted argument (pipes, heredocs, redirects, quoting, background jobs, exit status untouched); the password only travels in the wrapper's environment; PGSERVICE is cleared. When the branch cannot be ensured the hook answers permissionDecision: deny with the reason. It never answers allow; the rewritten command goes through the host's normal permission flow (the rewrite starts with the pgx invocation so a prefix allow rule can match it).
  • SubagentStop → terminate the branch's backends, retire with grace. Parent untouched.

Orphaned transactions. Every agent branch gets client_connection_check_interval=5s, idle_in_transaction_session_timeout=15m, statement_timeout=1h (ALTER DATABASE … SET, configurable or disableable in .pgx.toml; the source is never altered). pgx agent down terminates the branch's backends before retiring/dropping and reports backends_terminated/backends_remaining; the guarantee is stated precisely in the docs (a COMMIT past its point of no return still completes; the guards cover the window before down).

CLI fixes from the benchmark. pgx agent exec: explicit-id branches are labelled exec (not the command basename), --keep leaves the branch active on its idle TTL and the next exec with the same id attaches to it, JSON gains agent_id/source/created/fresh/method/elapsed_ms/status. pgx agent down --id finds branches under any host label, --branch <name> refuses any other branch, --agent <id> on every lifecycle command. pgx agent ls shows AGENT and deadline_kind. pgx agent doctor reports subagent branches and the new hook audit log (~/.pgx/agent/hooks.log, ids and outcomes only).

Tests. tests/agent_subagent.rs (12 black-box tests: per-subagent branches, 20 subagents of one session, concurrent starts, routing with real psql and decoy inherited DATABASE_URL/PGDATABASE/PGSERVICE, shell semantics, idempotent rewrite, fail-closed refusals, stop barrier with a live backend, guard-based abort of a SIGKILLed client's transaction, cross-project/session identity, down --id/--branch, no password in any argv), updated exec/install suites, unit tests. All agent suites, fmt, clippy (-D warnings), skill and plugin tests pass.

Docs and Skill. SKILL.md and the Claude Code / lifecycle / security / troubleshooting references, docs/pgx/local-dev/{agents,cli}.md, README; plugin copy synced (plugins/sync.sh --check clean); live validation note docs/pgx/local-dev/validation/2026-10-26-subagent-isolation-live.md.

Live validation (real Claude Code subagents, no ids, no manual steps)

  • 3 subagents (users/account/booking workloads): three branches from SubagentStart in the same second, every command routed, each branch holds only its own agent's marker/migration/tables, all retired by SubagentStop.
  • 20 subagents in one message: 20/20 created by SubagentStart (clone incl.: p50 1231 ms, p95 1371 ms, max 1401 ms, all twenty in the same instant), 20/20 workloads in their own branch, 0 contamination, 20/20 retired, 37 s end to end, runtime RSS peak 92 MB, disk fully reclaimed after grace.
  • Template/base schema md5 identical to the pre-run baseline after every phase.
  • Subagent killed mid-transaction (TaskStop in pg_sleep inside BEGIN…COMMIT): backend aborted within the 5 s guard, 0 rows committed (before: 2 rows committed 8 min later).

Limitations found live

  • Claude Code evaluates permissions on the rewritten command. In an auto-mode session the classifier refused some rewritten commands (12 of 18 in the 3-agent phase, 0 of 20 in the scale phase); normal modes prompt or match rules. Remedy, the user's decision: an allow rule for the wrapper (Bash(/path/to/pgx agent exec:*)). Documented in the Skill and troubleshooting.
  • A forcibly terminated subagent gets no SubagentStop; its branch stays on the idle TTL and the guards protect the data (pgx agent down --agent <id> --now by hand).
  • Internal helper agents that run a Bash command get a branch too (TTL-bounded); a future agent_type filter could avoid the clone.

🤖 Generated with Claude Code

alexshapalov and others added 2 commits October 26, 2026 16:03
…cally

Claude Code subagents inherit the parent's CLAUDE_CODE_SESSION_ID, so every
subagent attached to the parent's branch (benchmark 2026-10-26: three
subagents, one database). Subagents now get their own branch without any
manual step:

- Identity is (project, host, session_id, agent_id). A subagent's branch is
  agent-<host>-<session8>-<agent8>; the parent keeps its name and records.
  Collisions fall back to a 16-hex hash of the full identity (max 57 bytes).
- Claude Code hooks: SubagentStart ensures the subagent's branch and tells
  the model its name (no credentials); PreToolUse(Bash) with agent_id
  answers updatedInput, rewriting the command to
  `<pgx> agent exec --agent <id> --cwd <project> -- <shell> -c '<cmd>'`
  so it runs with the branch's DATABASE_URL/PG*; when the branch cannot be
  ensured it answers permissionDecision: deny with the reason, never allow;
  SubagentStop terminates the branch's backends and retires it with grace.
  The session's own SessionStart/CLAUDE_ENV_FILE path is unchanged.
- Server-side guards on every agent branch (ALTER DATABASE … SET):
  client_connection_check_interval (5s), idle_in_transaction_session_timeout
  (15m), statement_timeout (1h), configurable in .pgx.toml. A killed agent's
  open transaction aborts instead of committing minutes later.
- `pgx agent down` terminates backends before retiring or dropping (the
  cancellation barrier, reported as backends_terminated/remaining); `--id`
  without `--host` finds the branch under any host label; `--branch <name>`
  refuses to act on any other branch; `--agent <id>` everywhere.
- `pgx agent exec`: explicit-id branches are labelled `exec`; `--keep`
  leaves the branch active on its idle TTL (re-attached by the next exec
  with the same id); JSON gains agent_id, source, created, fresh, method,
  elapsed_ms, status; PGSERVICE/PGSERVICEFILE cleared in the child; the
  password only ever travels in the environment.
- `pgx agent ls` shows AGENT and deadline_kind; `pgx agent doctor` expects
  six Claude hooks and summarises the new hook audit log
  (~/.pgx/agent/hooks.log: ids and outcomes only); `pgx agent install
  claude` completes a four-hook installation in place.
- Tests: tests/agent_subagent.rs (isolation, 20 subagents, concurrency,
  routing semantics incl. pipes/heredocs/redirects/background/exit codes,
  fail-closed refusals, stop barrier, guard-based abort of an orphaned
  transaction, cross-project/session identity, down --id/--branch, no
  password in any argv), updated exec/install suites, unit tests.
- Docs and Skill: automatic subagent isolation, the wrapper form and the
  permission-rule remedy for prompts/auto mode, guards, --keep semantics,
  troubleshooting; plugin copy synced; live validation note
  (3 and 20 real subagents, zero contamination, killed-agent transaction
  aborted).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Linux CI runners have no /bin/zsh, so `user_shell` fell back to /bin/sh
and the assertion failed. /bin/bash exists on every runner.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@alexshapalov
alexshapalov merged commit 938f7fa into main Oct 26, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant