diff --git a/packages/opencode/src/server/push-relay.ts b/packages/opencode/src/server/push-relay.ts index a5e0f7faa5..9e1e8a538c 100644 --- a/packages/opencode/src/server/push-relay.ts +++ b/packages/opencode/src/server/push-relay.ts @@ -28,6 +28,7 @@ type State = { pair: Pair stop: () => void seen: Map + parent: Map gc: number } @@ -77,6 +78,60 @@ function serverID(input: { relayURL: string; relaySecret: string }) { return createHash("sha256").update(`${input.relayURL}|${input.relaySecret}`).digest("hex").slice(0, 16) } +function recordSession(event: Event) { + if (!obj(event.properties)) return + const next = state + if (!next) return + + if (event.type !== "session.created" && event.type !== "session.updated" && event.type !== "session.deleted") { + return + } + + const info = obj(event.properties.info) ? event.properties.info : undefined + const id = str(info?.id) + if (!id) return + + if (event.type === "session.deleted") { + next.parent.delete(id) + return + } + + next.parent.set(id, str(info?.parentID)) +} + +function routeSession(sessionID: string) { + const next = state + if (!next) { + return { + sessionID, + subagent: false, + } + } + + const visited = new Set() + let current = sessionID + let target = sessionID + let subagent = false + + while (true) { + if (visited.has(current)) break + visited.add(current) + + if (!next.parent.has(current)) break + const parentID = next.parent.get(current) + if (!parentID) break + + subagent = true + target = parentID + current = parentID + } + + return { + sessionID: target, + subagent, + } +} + /** * Classify an IPv4 address into a reachability tier. * Lower number = more likely reachable from an external/overlay network device. @@ -167,17 +222,21 @@ function list(hostname: string, port: number, advertised: string[] = []) { } function map(event: Event): { type: Type; sessionID: string } | undefined { + recordSession(event) + if (!obj(event.properties)) return if (event.type === "permission.asked") { const sessionID = str(event.properties.sessionID) if (!sessionID) return - return { type: "permission", sessionID } + const route = routeSession(sessionID) + return { type: "permission", sessionID: route.sessionID } } if (event.type === "session.error") { const sessionID = str(event.properties.sessionID) if (!sessionID) return + if (routeSession(sessionID).subagent) return if (!shouldNotifyError(event.properties.error)) return return { type: "error", sessionID } } @@ -187,6 +246,7 @@ function map(event: Event): { type: Type; sessionID: string } | undefined { if (!sessionID) return if (!obj(event.properties.status)) return if (event.properties.status.type !== "idle") return + if (routeSession(sessionID).subagent) return return { type: "complete", sessionID } } @@ -405,6 +465,7 @@ export namespace PushRelay { pair, stop: unsub, seen: new Map(), + parent: new Map(), gc: 0, } diff --git a/packages/opencode/test/server/push-relay.test.ts b/packages/opencode/test/server/push-relay.test.ts index 57ed158cc6..5e5799eb80 100644 --- a/packages/opencode/test/server/push-relay.test.ts +++ b/packages/opencode/test/server/push-relay.test.ts @@ -14,6 +14,16 @@ function emit(type: string, properties: unknown) { }) } +function created(sessionID: string, parentID?: string) { + emit("session.created", { + sessionID, + info: { + id: sessionID, + parentID, + }, + }) +} + async function waitForCalls(count: number) { for (let i = 0; i < 50; i++) { if (fetchMock.mock.calls.length >= count) return @@ -101,4 +111,43 @@ describe("push relay event mapping", () => { await waitForCalls(1) expect(callBody()?.eventType).toBe("permission") }) + + test("does not relay subagent completion events", async () => { + created("ses_root") + created("ses_subagent", "ses_root") + + emit("session.status", { + sessionID: "ses_subagent", + status: { type: "idle" }, + }) + + await new Promise((resolve) => setTimeout(resolve, 40)) + expect(fetchMock.mock.calls.length).toBe(0) + }) + + test("does not relay subagent errors", async () => { + created("ses_root") + created("ses_subagent", "ses_root") + + emit("session.error", { + sessionID: "ses_subagent", + error: { name: "UnknownError", data: { message: "boom" } }, + }) + + await new Promise((resolve) => setTimeout(resolve, 40)) + expect(fetchMock.mock.calls.length).toBe(0) + }) + + test("relays subagent permission prompts to parent session", async () => { + created("ses_root") + created("ses_subagent", "ses_root") + + emit("permission.asked", { + sessionID: "ses_subagent", + }) + + await waitForCalls(1) + expect(callBody()?.eventType).toBe("permission") + expect(callBody()?.sessionID).toBe("ses_root") + }) })