fix: close OpenCode event streams before ending requests

Completed parallel conversations could leave buffered event connections open.
Chat and native context compaction now cancel their own subscriptions before
ending SDK iteration, without cancelling another conversation or its Server.

Strengthen cleanup-order regressions and verify real OpenCode parallel runs,
cancellation and compaction. The remote Agent uses ACP stdio and transcript
polling rather than this SDK event stream and needs no corresponding change.

Validation: 99 focused tests passed; the final candidate passed 4089 tests
with 66 skipped, typecheck and lint. No external model calls were made.
This commit is contained in:
mesalogo
2026-09-13 17:28:40 +08:00
parent a44de1441a
commit 59a1624396
4 changed files with 81 additions and 13 deletions
+26 -2
View File
@@ -295,6 +295,24 @@ function eventSubscriptionOptions(signal: AbortSignal) {
};
}
async function* consumeEventSubscription<T>(
stream: AsyncGenerator<T, void, unknown>,
controller: AbortController,
): AsyncGenerator<T, void, unknown> {
try {
while (true) {
const next = await stream.next();
if (next.done) return;
yield next.value;
}
} finally {
// The SDK removes its abort listener on iterator return without cancelling
// the reader. Abort first, while that listener can still close the body.
controller.abort();
await stream.return();
}
}
function awaitWithAbort<T>(
operation: Promise<T>,
signal: AbortSignal,
@@ -2466,7 +2484,10 @@ export class OpenCodeRuntime implements AgentRuntime {
const reportedMessageIds = new Set<string>();
const childTaskIdsBySession = new Map<string, string>();
let waitingForRetry = false;
for await (const event of subscription.stream) {
for await (const event of consumeEventSubscription(
subscription.stream,
subscriptionController,
)) {
const childProgress = subagentProgress.update(event);
if (
childProgress &&
@@ -3101,7 +3122,10 @@ export class OpenCodeRuntime implements AgentRuntime {
const usageEvents: RuntimeModelUsageEvent[] = [];
const reportedMessageIds = new Set<string>();
const usageCapture = (async () => {
for await (const event of subscription.stream) {
for await (const event of consumeEventSubscription(
subscription.stream,
subscriptionController,
)) {
if (
event.type === "message.updated" &&
event.properties.sessionID === sessionId &&