Skip to content

Commit 6c5db7b

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 266e965 commit 6c5db7b

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"
@@ -146,6 +146,8 @@ export class Agent implements ACPAgent {
146146
private bashSnapshots = new Map<string, string>()
147147
private toolStarts = new Set<string>()
148148
private permissionQueues = new Map<string, Promise<void>>()
149+
// Maps child session IDs to their root ACP-tracked session ID
150+
private childToRootSession = new Map<string, string>()
149151
private permissionOptions: PermissionOption[] = [
150152
{ optionId: "once", kind: "allow_once", name: "Allow once" },
151153
{ optionId: "always", kind: "allow_always", name: "Always allow" },
@@ -186,11 +188,55 @@ export class Agent implements ACPAgent {
186188
}
187189
}
188190

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

196242
const prev = this.permissionQueues.get(permission.sessionID) ?? Promise.resolve()
@@ -273,7 +319,7 @@ export class Agent implements ACPAgent {
273319
log.info("message part updated", { event: event.properties })
274320
const props = event.properties
275321
const part = props.part
276-
const session = this.sessionManager.tryGet(part.sessionID)
322+
const session = await this.resolveRootSession(part.sessionID)
277323
if (!session) return
278324
const sessionId = session.id
279325

@@ -465,16 +511,19 @@ export class Agent implements ACPAgent {
465511

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

472521
const message = await this.sdk.session
473522
.message(
474523
{
475524
sessionID: props.sessionID,
476525
messageID: props.messageID,
477-
directory: session.cwd,
526+
directory,
478527
},
479528
{ throwOnError: true },
480529
)

0 commit comments

Comments
 (0)