Skip to content

Comments

feat(agents): dispatch Claude Code CLI runs as persistent, resumable subagents#13167

Draft
gyant wants to merge 1 commit intoopenclaw:mainfrom
gyant:feat/subagent-follow-ups
Draft

feat(agents): dispatch Claude Code CLI runs as persistent, resumable subagents#13167
gyant wants to merge 1 commit intoopenclaw:mainfrom
gyant:feat/subagent-follow-ups

Conversation

@gyant
Copy link

@gyant gyant commented Feb 10, 2026

feat(agents): dispatch Claude Code CLI runs as persistent, resumable subagents

Summary

This PR enables a workflow for dispatching Claude Code CLI processes as kept subagents that run in specific project directories, pick up MCP tools via .mcp.json, and support iterative follow-up — all driven by the existing lifecycle/announce pipeline without cron or polling.

Spawn a subagent into a git worktree → it discovers project-specific MCP servers → it completes and announces its result → you follow up to refine → the cycle continues with full prior context.

Motivation

The built-in subagent system and CLI backends (claude-cli) already support spawning headless Claude Code runs. With cleanup: "keep", sessions persist and can be resumed. But two gaps prevented this from being useful for multi-turn coding workflows:

  1. No way to follow up. The announce message didn't include the child session key, the subagent didn't expect follow-ups, and async sends silently dropped results. The main agent had no structured path to iterate on a subagent's work.

  2. No way to control where the process runs. The CLI process always launched in the agent's configured workspace directory (~/.openclaw/workspace by default). If your project has a .mcp.json that provides MCP tools, the subagent never sees them — and telling it to cd to your project breaks MCP discovery since servers are initialized relative to the process cwd at startup.

Changes

Working directory separation (cwd)

The sessions_spawn tool now accepts an optional cwd parameter that controls where the CLI/embedded process actually runs, independently from the agent's workspace directory.

  • workspaceDir — the agent's home for memory, bootstrap files, skills, and state. Resolved from agent config as before.
  • cwd — where the process runs. Defaults to workspaceDir when not specified. When set, only affects spawn() in the CLI runner and process.chdir() / createAgentSession({ cwd }) in the embedded runner.

Nothing is written into the cwd directory by the agent framework — no AGENTS.md, SOUL.md, or other scaffolding. The typical workflow:

  1. Create a git worktree for the task
  2. Spawn a subagent with cwd pointing at the worktree
  3. The Claude CLI process starts in the worktree, discovers .mcp.json, and gets project-specific MCP tools
  4. Follow up as needed, clean up the worktree when done

Files: sessions-spawn-tool.ts (schema + passthrough), agent.ts (protocol schema), server-methods/agent.ts (handler), agent/types.ts (opts), agent.ts (command), cli-runner.ts (spawn), pi-embedded-runner/run.ts + run/attempt.ts + run/params.ts + run/types.ts (embedded runner)

Follow-up messaging to kept subagent sessions

When a subagent is spawned with cleanup: "keep", the main agent can now send follow-up messages that build on the subagent's full prior context. Three things were missing:

Announce message includes follow-up key (subagent-announce.ts)

When cleanup === "keep", the announce trigger now appends the child session key and a hint that sessions_send can continue the conversation.

Subagent system prompt is context-aware (subagent-announce.ts, sessions-spawn-tool.ts)

buildSubagentSystemPrompt gains an optional resumable parameter. When the spawn tool passes resumable: true, the subagent prompt swaps "Be ephemeral" for "Resumable — your session persists after completion. You may receive follow-up messages."

Async follow-ups register for announce delivery (sessions-send-tool.ts)

When sessions_send dispatches async (timeoutSeconds: 0) to a subagent session key, it now calls registerSubagentRun — the same registration that sessions_spawn uses. This wires follow-ups into the existing lifecycle → announce pipeline so results are delivered back automatically.

CLI output available for announce flows (agent.ts, chat.ts)

CLI-backed runs don't write to the gateway chat store, so readLatestAssistantReply returned nothing when the announce flow tried to read the subagent's output. Now, CLI output is injected via chat.inject before emitting the lifecycle event. The handler accepts createIfMissing: true so transcripts can be bootstrapped on first write.

Design rationale

The sessions_send follow-up path reuses the full subagent lifecycle pipeline (registerSubagentRun → lifecycle listener → runSubagentAnnounceFlowfinalizeSubagentCleanup) rather than building a parallel delivery mechanism. Each follow-up generates a unique runId, so multiple concurrent follow-ups to different subagents are tracked independently.

The cwd / workspaceDir separation is intentionally minimal — cwd only affects the process working directory, not skills resolution, bootstrap file loading, or any other agent state. This keeps the security model intact: the operator controls the workspace via config, while cwd lets the orchestrating agent point tasks at specific project directories.

Testing

pnpm build && pnpm check && pnpm test

Type-check clean, formatter clean, all 252 tests pass (41 test files).

Verified manually:

  • sessions_spawn with cleanup: "keep" — announce message includes the follow-up session key
  • sessions_send to the child session key — subagent resumes with full prior context
  • sessions_spawn with cwd pointing at a project with .mcp.json — CLI process discovers MCP servers
  • Spawning a new subagent and asking "What's the last thing I asked you?" — correctly reports no history, confirming session isolation
  • Async sessions_send results announced back automatically via the lifecycle flow

Sign-Off

  • AI-assisted: Yes — developed collaboratively with Claude (Opus 4.6)
  • Testing: Type-check clean, all existing tests pass, manual verification confirmed above

@openclaw-barnacle openclaw-barnacle bot added app: web-ui App: web-ui gateway Gateway runtime commands Command implementations agents Agent runtime and tooling labels Feb 10, 2026
@gyant gyant force-pushed the feat/subagent-follow-ups branch from 116b32c to 6ab43d1 Compare February 10, 2026 05:25
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 10, 2026

Additional Comments (1)

src/gateway/server-methods/chat.ts
chat.inject bypasses send policy
chat.inject appends an assistant message after only loadSessionEntry succeeds; it does not enforce resolveSendPolicy (unlike chat.send). With createIfMissing: true, this means any gateway caller with access to this method can write assistant messages into transcripts for sessions where messaging would otherwise be denied by policy.

If chat.inject is intended to be internal-only, it should enforce an equivalent policy/scope check (or reuse resolveSendPolicy) before writing the transcript.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/server-methods/chat.ts
Line: 661:668

Comment:
**chat.inject bypasses send policy**
`chat.inject` appends an assistant message after only `loadSessionEntry` succeeds; it does not enforce `resolveSendPolicy` (unlike `chat.send`). With `createIfMissing: true`, this means any gateway caller with access to this method can write assistant messages into transcripts for sessions where messaging would otherwise be denied by policy.

If `chat.inject` is intended to be internal-only, it should enforce an equivalent policy/scope check (or reuse `resolveSendPolicy`) before writing the transcript.

How can I resolve this? If you propose a fix, please make it concise.

@gyant gyant marked this pull request as draft February 10, 2026 05:27
@gyant gyant force-pushed the feat/subagent-follow-ups branch 2 times, most recently from 46704d5 to 6c173cc Compare February 10, 2026 06:00
@gyant gyant marked this pull request as ready for review February 10, 2026 06:01
@gyant gyant marked this pull request as draft February 10, 2026 16:13
@gyant gyant force-pushed the feat/subagent-follow-ups branch from 6c173cc to e98a10c Compare February 10, 2026 20:31
@gyant gyant changed the title feat(agents): enable follow-up messaging to kept subagent sessions feat(agents): dispatch Claude Code CLI runs as persistent, resumable subagents Feb 11, 2026
@gyant gyant force-pushed the feat/subagent-follow-ups branch from 03640fc to 46d610b Compare February 11, 2026 22:55
Close the feedback loop for subagents spawned with cleanup: "keep" so
the main agent can send follow-up messages and receive results back
automatically via the existing lifecycle/announce pipeline.

Three gaps prevented multi-turn subagent workflows:

1. The announce message didn't include the child session key, so the
   main agent couldn't address follow-ups.

2. The subagent system prompt said "Be ephemeral" even for kept
   sessions, so the subagent didn't expect follow-up messages.

3. Async follow-ups via sessions_send (timeoutSeconds: 0) had no
   announce-back path — results were silently lost.

Changes:

- subagent-announce.ts: When cleanup === "keep", include the child
  session key and a sessions_send hint in the trigger message. Add
  "session keys" to the "don't mention" instruction. Add optional
  resumable param to buildSubagentSystemPrompt that swaps "Be
  ephemeral" for "Resumable" when true.

- sessions-spawn-tool.ts: Pass resumable: cleanup === "keep" to
  buildSubagentSystemPrompt.

- sessions-send-tool.ts: In the async path (timeoutSeconds === 0),
  register subagent targets via registerSubagentRun instead of
  startA2AFlow. This wires follow-ups into the same lifecycle ->
  announce pipeline that sessions_spawn uses. Non-subagent targets
  continue using the A2A flow unchanged.

- agent.ts: Inject CLI output via chat.inject before emitting the
  lifecycle end event. CLI-backed runs don't write to the gateway
  chat store, so readLatestAssistantReply found nothing when the
  announce flow ran.

- chat.ts: Set createIfMissing: true on the chat.inject handler so
  new subagent transcripts can be bootstrapped on first write.

fix: address review feedback from greptile

- agent.ts: Cap CLI output injection at 100KB to prevent unbounded
  transcript bloat from large tool output or diffs.

- sessions-send-tool.ts: Look up the original spawn's cleanup intent
  via findSubagentRunByChildSessionKey() instead of hardcoding
  cleanup: "keep". Falls back to "keep" when no record is found.

- chat.ts: Add resolveSendPolicy check to chat.inject, matching the
  same enforcement that chat.send uses. Sessions with a "deny" policy
  now reject inject writes.

- subagent-registry.ts: Add findSubagentRunByChildSessionKey() to
  support looking up the original spawn record by child session key.

- chat.inject.parentid.test.ts: Update mock to include cfg and
  canonicalKey fields required by the new send policy check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

fix(agents): prevent A2A announce step from leaking into subagent sessions

The sessions_send tool's timeoutSeconds > 0 path unconditionally fired
runSessionsSendA2AFlow after completing a send. For subagent targets,
this caused the A2A peer-to-peer ping-pong loop to attempt announcing
to the main agent — and when that timed out (e.g. main session queue
full), the subsequent "announce step" ran on the subagent session
itself, injecting a spurious "[timestamp] Agent-to-agent announce step."
message into the CLI subagent.

The timeoutSeconds === 0 path already correctly split between
registerSubagentRun (for subagent targets) and startA2AFlow (for peer
agents). The > 0 path was missing this guard because subagent sessions
were not targetable by sessions_send before follow-up messaging was
added.

Fix: gate startA2AFlow on !isSubagentSessionKey(resolvedKey) in the
timeoutSeconds > 0 path. The reply is already returned as the tool
result, so the A2A flow is redundant for subagent targets.

Also adds isFollowUp flag to SubagentRunRecord to suppress the
follow-up hint in announce messages for follow-up runs, preventing
the parent LLM from being repeatedly encouraged to send follow-ups
after each follow-up completion.

feat(agents): add cwd override to sessions_spawn for MCP discovery

Separate the process working directory (cwd) from the agent's workspace
directory. The workspace remains the agent's home for memory, bootstrap,
and state files, while cwd controls where the CLI/embedded process
actually runs. This enables spawning subagents into git worktrees where
.mcp.json lives, giving them project-specific MCP tool access without
polluting the worktree with agent scaffolding.
@openclaw-barnacle
Copy link

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added the stale Marked as stale due to inactivity label Feb 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling app: web-ui App: web-ui commands Command implementations gateway Gateway runtime stale Marked as stale due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant