Skip to content

Commit 4620720

Browse files
committed
fix(acp): forward subagent session events to ACP client
When a prompt is run through the Task tool, events arrive with a child session ID that is not directly tracked by ACPSessionManager. Add resolveRootSession() which walks the parentID chain via the SDK to find the nearest ACP-tracked ancestor, caching results to avoid repeated lookups. Replace all sessionManager.tryGet() calls in handleEvent with resolveRootSession() so that tool call and message delta events from sub-agent sessions are correctly forwarded to the ACP client.
1 parent 8b7f285 commit 4620720

1 file changed

Lines changed: 54 additions & 5 deletions

File tree

packages/opencode/src/acp/agent.ts

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { pathToFileURL } from "url"
3636
import { Filesystem } from "../util"
3737
import { Hash } from "@opencode-ai/shared/util/hash"
3838
import { ACPSessionManager } from "./session"
39-
import type { ACPConfig } from "./types"
39+
import type { ACPConfig, ACPSessionState } from "./types"
4040
import { Provider } from "../provider"
4141
import { ModelID, ProviderID } from "../provider/schema"
4242
import { Agent as AgentModule } from "../agent/agent"
@@ -147,6 +147,8 @@ export class Agent implements ACPAgent {
147147
private bashSnapshots = new Map<string, string>()
148148
private toolStarts = new Set<string>()
149149
private permissionQueues = new Map<string, Promise<void>>()
150+
// Maps child session IDs to their root ACP-tracked session ID
151+
private childToRootSession = new Map<string, string>()
150152
private permissionOptions: PermissionOption[] = [
151153
{ optionId: "once", kind: "allow_once", name: "Allow once" },
152154
{ optionId: "always", kind: "allow_always", name: "Always allow" },
@@ -187,11 +189,55 @@ export class Agent implements ACPAgent {
187189
}
188190
}
189191

192+
/**
193+
* Given a session ID, returns the ACPSessionState for that session or the
194+
* nearest ACP-tracked ancestor. Child sessions created by the Task tool
195+
* have a parentID chain leading back to the root ACP session. We cache
196+
* the mapping so we only traverse the chain once per child session.
197+
*/
198+
private async resolveRootSession(sessionID: string): Promise<ACPSessionState | undefined> {
199+
// Fast path: directly tracked by ACP session manager
200+
const direct = this.sessionManager.tryGet(sessionID)
201+
if (direct) return direct
202+
203+
// Check cache
204+
const cached = this.childToRootSession.get(sessionID)
205+
if (cached) return this.sessionManager.tryGet(cached)
206+
207+
// Walk the parentID chain via the SDK until we find a known root session
208+
let currentID = sessionID
209+
const visited: string[] = []
210+
while (true) {
211+
visited.push(currentID)
212+
const info = await this.sdk.session
213+
.get({ sessionID: currentID, directory: "" }, { throwOnError: false })
214+
.then((x) => x.data)
215+
.catch(() => undefined)
216+
217+
if (!info) break
218+
219+
const parentID = info.parentID
220+
if (!parentID) break
221+
222+
const root = this.sessionManager.tryGet(parentID)
223+
if (root) {
224+
// Cache the mapping for all visited IDs
225+
for (const id of visited) {
226+
this.childToRootSession.set(id, root.id)
227+
}
228+
return root
229+
}
230+
currentID = parentID
231+
}
232+
233+
return undefined
234+
}
235+
190236
private async handleEvent(event: Event) {
191237
switch (event.type) {
192238
case "permission.asked": {
193239
const permission = event.properties
194-
const session = this.sessionManager.tryGet(permission.sessionID)
240+
const session = await this.resolveRootSession(permission.sessionID)
195241
if (!session) return
196242

197243
const prev = this.permissionQueues.get(permission.sessionID) ?? Promise.resolve()
@@ -274,7 +320,7 @@ export class Agent implements ACPAgent {
274320
log.info("message part updated", { event: event.properties })
275321
const props = event.properties
276322
const part = props.part
277-
const session = this.sessionManager.tryGet(part.sessionID)
323+
const session = await this.resolveRootSession(part.sessionID)
278324
if (!session) return
279325
const sessionId = session.id
280326

@@ -466,16 +512,19 @@ export class Agent implements ACPAgent {
466512

467513
case "message.part.delta": {
468514
const props = event.properties
469-
const session = this.sessionManager.tryGet(props.sessionID)
515+
const session = await this.resolveRootSession(props.sessionID)
470516
if (!session) return
471517
const sessionId = session.id
518+
// Use the child session's own cwd for SDK lookups (may differ from root)
519+
const childSession = this.sessionManager.tryGet(props.sessionID)
520+
const directory = childSession?.cwd ?? session.cwd
472521

473522
const message = await this.sdk.session
474523
.message(
475524
{
476525
sessionID: props.sessionID,
477526
messageID: props.messageID,
478-
directory: session.cwd,
527+
directory,
479528
},
480529
{ throwOnError: true },
481530
)

0 commit comments

Comments
 (0)