Skip to content

Commit d1d3d42

Browse files
author
Ryan Vogel
committed
fix: suppress subagent APN completion and error events
1 parent 1d68cd2 commit d1d3d42

2 files changed

Lines changed: 111 additions & 1 deletion

File tree

packages/opencode/src/server/push-relay.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type State = {
2828
pair: Pair
2929
stop: () => void
3030
seen: Map<string, number>
31+
parent: Map<string, string | undefined>
3132
gc: number
3233
}
3334

@@ -77,6 +78,60 @@ function serverID(input: { relayURL: string; relaySecret: string }) {
7778
return createHash("sha256").update(`${input.relayURL}|${input.relaySecret}`).digest("hex").slice(0, 16)
7879
}
7980

81+
function recordSession(event: Event) {
82+
if (!obj(event.properties)) return
83+
const next = state
84+
if (!next) return
85+
86+
if (event.type !== "session.created" && event.type !== "session.updated" && event.type !== "session.deleted") {
87+
return
88+
}
89+
90+
const info = obj(event.properties.info) ? event.properties.info : undefined
91+
const id = str(info?.id)
92+
if (!id) return
93+
94+
if (event.type === "session.deleted") {
95+
next.parent.delete(id)
96+
return
97+
}
98+
99+
next.parent.set(id, str(info?.parentID))
100+
}
101+
102+
function routeSession(sessionID: string) {
103+
const next = state
104+
if (!next) {
105+
return {
106+
sessionID,
107+
subagent: false,
108+
}
109+
}
110+
111+
const visited = new Set<string>()
112+
let current = sessionID
113+
let target = sessionID
114+
let subagent = false
115+
116+
while (true) {
117+
if (visited.has(current)) break
118+
visited.add(current)
119+
120+
if (!next.parent.has(current)) break
121+
const parentID = next.parent.get(current)
122+
if (!parentID) break
123+
124+
subagent = true
125+
target = parentID
126+
current = parentID
127+
}
128+
129+
return {
130+
sessionID: target,
131+
subagent,
132+
}
133+
}
134+
80135
/**
81136
* Classify an IPv4 address into a reachability tier.
82137
* Lower number = more likely reachable from an external/overlay network device.
@@ -167,17 +222,21 @@ function list(hostname: string, port: number, advertised: string[] = []) {
167222
}
168223

169224
function map(event: Event): { type: Type; sessionID: string } | undefined {
225+
recordSession(event)
226+
170227
if (!obj(event.properties)) return
171228

172229
if (event.type === "permission.asked") {
173230
const sessionID = str(event.properties.sessionID)
174231
if (!sessionID) return
175-
return { type: "permission", sessionID }
232+
const route = routeSession(sessionID)
233+
return { type: "permission", sessionID: route.sessionID }
176234
}
177235

178236
if (event.type === "session.error") {
179237
const sessionID = str(event.properties.sessionID)
180238
if (!sessionID) return
239+
if (routeSession(sessionID).subagent) return
181240
if (!shouldNotifyError(event.properties.error)) return
182241
return { type: "error", sessionID }
183242
}
@@ -187,6 +246,7 @@ function map(event: Event): { type: Type; sessionID: string } | undefined {
187246
if (!sessionID) return
188247
if (!obj(event.properties.status)) return
189248
if (event.properties.status.type !== "idle") return
249+
if (routeSession(sessionID).subagent) return
190250
return { type: "complete", sessionID }
191251
}
192252

@@ -405,6 +465,7 @@ export namespace PushRelay {
405465
pair,
406466
stop: unsub,
407467
seen: new Map(),
468+
parent: new Map(),
408469
gc: 0,
409470
}
410471

packages/opencode/test/server/push-relay.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ function emit(type: string, properties: unknown) {
1414
})
1515
}
1616

17+
function created(sessionID: string, parentID?: string) {
18+
emit("session.created", {
19+
sessionID,
20+
info: {
21+
id: sessionID,
22+
parentID,
23+
},
24+
})
25+
}
26+
1727
async function waitForCalls(count: number) {
1828
for (let i = 0; i < 50; i++) {
1929
if (fetchMock.mock.calls.length >= count) return
@@ -101,4 +111,43 @@ describe("push relay event mapping", () => {
101111
await waitForCalls(1)
102112
expect(callBody()?.eventType).toBe("permission")
103113
})
114+
115+
test("does not relay subagent completion events", async () => {
116+
created("ses_root")
117+
created("ses_subagent", "ses_root")
118+
119+
emit("session.status", {
120+
sessionID: "ses_subagent",
121+
status: { type: "idle" },
122+
})
123+
124+
await new Promise((resolve) => setTimeout(resolve, 40))
125+
expect(fetchMock.mock.calls.length).toBe(0)
126+
})
127+
128+
test("does not relay subagent errors", async () => {
129+
created("ses_root")
130+
created("ses_subagent", "ses_root")
131+
132+
emit("session.error", {
133+
sessionID: "ses_subagent",
134+
error: { name: "UnknownError", data: { message: "boom" } },
135+
})
136+
137+
await new Promise((resolve) => setTimeout(resolve, 40))
138+
expect(fetchMock.mock.calls.length).toBe(0)
139+
})
140+
141+
test("relays subagent permission prompts to parent session", async () => {
142+
created("ses_root")
143+
created("ses_subagent", "ses_root")
144+
145+
emit("permission.asked", {
146+
sessionID: "ses_subagent",
147+
})
148+
149+
await waitForCalls(1)
150+
expect(callBody()?.eventType).toBe("permission")
151+
expect(callBody()?.sessionID).toBe("ses_root")
152+
})
104153
})

0 commit comments

Comments
 (0)