feat(agents): dispatch Claude Code CLI runs as persistent, resumable subagents#13167
Draft
gyant wants to merge 1 commit intoopenclaw:mainfrom
Draft
feat(agents): dispatch Claude Code CLI runs as persistent, resumable subagents#13167gyant wants to merge 1 commit intoopenclaw:mainfrom
gyant wants to merge 1 commit intoopenclaw:mainfrom
Conversation
116b32c to
6ab43d1
Compare
Contributor
Additional Comments (1)
If Prompt To Fix With AIThis 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. |
46704d5 to
6c173cc
Compare
6c173cc to
e98a10c
Compare
03640fc to
46d610b
Compare
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.
46d610b to
7b21fde
Compare
bfc1ccb to
f92900f
Compare
|
This pull request has been automatically marked as stale due to inactivity. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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. Withcleanup: "keep", sessions persist and can be resumed. But two gaps prevented this from being useful for multi-turn coding workflows: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.
No way to control where the process runs. The CLI process always launched in the agent's configured workspace directory (
~/.openclaw/workspaceby default). If your project has a.mcp.jsonthat provides MCP tools, the subagent never sees them — and telling it tocdto your project breaks MCP discovery since servers are initialized relative to the processcwdat startup.Changes
Working directory separation (
cwd)The
sessions_spawntool now accepts an optionalcwdparameter 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 toworkspaceDirwhen not specified. When set, only affectsspawn()in the CLI runner andprocess.chdir()/createAgentSession({ cwd })in the embedded runner.Nothing is written into the
cwddirectory by the agent framework — noAGENTS.md,SOUL.md, or other scaffolding. The typical workflow:cwdpointing at the worktree.mcp.json, and gets project-specific MCP toolsFiles:
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 thatsessions_sendcan continue the conversation.Subagent system prompt is context-aware (
subagent-announce.ts,sessions-spawn-tool.ts)buildSubagentSystemPromptgains an optionalresumableparameter. When the spawn tool passesresumable: 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_senddispatches async (timeoutSeconds: 0) to a subagent session key, it now callsregisterSubagentRun— the same registration thatsessions_spawnuses. 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
readLatestAssistantReplyreturned nothing when the announce flow tried to read the subagent's output. Now, CLI output is injected viachat.injectbefore emitting the lifecycle event. The handler acceptscreateIfMissing: trueso transcripts can be bootstrapped on first write.Design rationale
The
sessions_sendfollow-up path reuses the full subagent lifecycle pipeline (registerSubagentRun→ lifecycle listener →runSubagentAnnounceFlow→finalizeSubagentCleanup) rather than building a parallel delivery mechanism. Each follow-up generates a uniquerunId, so multiple concurrent follow-ups to different subagents are tracked independently.The
cwd/workspaceDirseparation is intentionally minimal —cwdonly 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, whilecwdlets the orchestrating agent point tasks at specific project directories.Testing
Type-check clean, formatter clean, all 252 tests pass (41 test files).
Verified manually:
sessions_spawnwithcleanup: "keep"— announce message includes the follow-up session keysessions_sendto the child session key — subagent resumes with full prior contextsessions_spawnwithcwdpointing at a project with.mcp.json— CLI process discovers MCP serverssessions_sendresults announced back automatically via the lifecycle flowSign-Off