fix: suppress subagent APN completion and error events

pull/19545/head
Ryan Vogel 2026-04-02 20:11:12 +00:00
parent 1d68cd288c
commit d1d3d420bf
2 changed files with 111 additions and 1 deletions

View File

@ -28,6 +28,7 @@ type State = {
pair: Pair pair: Pair
stop: () => void stop: () => void
seen: Map<string, number> seen: Map<string, number>
parent: Map<string, string | undefined>
gc: number 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) 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<string>()
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. * Classify an IPv4 address into a reachability tier.
* Lower number = more likely reachable from an external/overlay network device. * 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 { function map(event: Event): { type: Type; sessionID: string } | undefined {
recordSession(event)
if (!obj(event.properties)) return if (!obj(event.properties)) return
if (event.type === "permission.asked") { if (event.type === "permission.asked") {
const sessionID = str(event.properties.sessionID) const sessionID = str(event.properties.sessionID)
if (!sessionID) return if (!sessionID) return
return { type: "permission", sessionID } const route = routeSession(sessionID)
return { type: "permission", sessionID: route.sessionID }
} }
if (event.type === "session.error") { if (event.type === "session.error") {
const sessionID = str(event.properties.sessionID) const sessionID = str(event.properties.sessionID)
if (!sessionID) return if (!sessionID) return
if (routeSession(sessionID).subagent) return
if (!shouldNotifyError(event.properties.error)) return if (!shouldNotifyError(event.properties.error)) return
return { type: "error", sessionID } return { type: "error", sessionID }
} }
@ -187,6 +246,7 @@ function map(event: Event): { type: Type; sessionID: string } | undefined {
if (!sessionID) return if (!sessionID) return
if (!obj(event.properties.status)) return if (!obj(event.properties.status)) return
if (event.properties.status.type !== "idle") return if (event.properties.status.type !== "idle") return
if (routeSession(sessionID).subagent) return
return { type: "complete", sessionID } return { type: "complete", sessionID }
} }
@ -405,6 +465,7 @@ export namespace PushRelay {
pair, pair,
stop: unsub, stop: unsub,
seen: new Map(), seen: new Map(),
parent: new Map(),
gc: 0, gc: 0,
} }

View File

@ -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) { async function waitForCalls(count: number) {
for (let i = 0; i < 50; i++) { for (let i = 0; i < 50; i++) {
if (fetchMock.mock.calls.length >= count) return if (fetchMock.mock.calls.length >= count) return
@ -101,4 +111,43 @@ describe("push relay event mapping", () => {
await waitForCalls(1) await waitForCalls(1)
expect(callBody()?.eventType).toBe("permission") 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")
})
}) })