Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/opencode-client-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class OpencodeClientManager {
private logger: Logger;
private initPromise: Promise<OpencodeClient> | null = null;
private isDisposed = false;
private activeEventSubscriptions = new Set<AbortController>();
private cleanupHandlersRegistered = false;
private cleanupHandlers: {
exit?: () => void;
Expand Down Expand Up @@ -321,6 +322,17 @@ export class OpencodeClientManager {
return this.server !== null;
}

/**
* Track an event-stream AbortController so dispose() can tear down SSE
* connections that are still open. Returns an unregister function.
*/
registerEventSubscription(controller: AbortController): () => void {
this.activeEventSubscriptions.add(controller);
return () => {
this.activeEventSubscriptions.delete(controller);
};
}

/**
* Dispose of the client manager, stopping the server if managed.
*/
Expand All @@ -332,6 +344,13 @@ export class OpencodeClientManager {
this.isDisposed = true;
this.unregisterCleanupHandlers();

// Abort any SSE event streams still open; stopping a managed server does
// not close them, and for reused servers nothing else would.
for (const controller of this.activeEventSubscriptions) {
controller.abort();
}
this.activeEventSubscriptions.clear();

// Release the singleton slot so a later getInstance() builds a fresh
// manager instead of returning this disposed one.
if (OpencodeClientManager.instance === this) {
Expand Down
1 change: 1 addition & 0 deletions src/opencode-language-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ const mockClientManager = {
getClient: vi.fn().mockResolvedValue(mockClient),
dispose: vi.fn().mockResolvedValue(undefined),
getServerUrl: vi.fn().mockReturnValue("http://127.0.0.1:4096"),
registerEventSubscription: vi.fn().mockReturnValue(() => {}),
};

describe("opencode-language-model", () => {
Expand Down
14 changes: 14 additions & 0 deletions src/opencode-language-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,11 +303,11 @@
};

this.logger.debug?.(
`[doGenerate] raw parts from OpenCode: ${JSON.stringify(responseData.parts?.map((p) => ({ type: p.type, ...(p.type === "text" ? { text: (p as any).text?.slice(0, 200) } : {}), ...(p.type === "tool" ? { tool: (p as any).tool, callID: (p as any).callID, status: (p as any).state?.status, input: (p as any).state?.input } : {}) })))}`,

Check warning on line 306 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 20

Unexpected any. Specify a different type

Check warning on line 306 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 20

Unexpected any. Specify a different type

Check warning on line 306 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 20

Unexpected any. Specify a different type

Check warning on line 306 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 20

Unexpected any. Specify a different type

Check warning on line 306 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 20

Unexpected any. Specify a different type

Check warning on line 306 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 22

Unexpected any. Specify a different type

Check warning on line 306 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 22

Unexpected any. Specify a different type

Check warning on line 306 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 22

Unexpected any. Specify a different type

Check warning on line 306 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 22

Unexpected any. Specify a different type

Check warning on line 306 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 22

Unexpected any. Specify a different type

Check warning on line 306 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 18

Unexpected any. Specify a different type

Check warning on line 306 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 18

Unexpected any. Specify a different type

Check warning on line 306 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 18

Unexpected any. Specify a different type

Check warning on line 306 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 18

Unexpected any. Specify a different type

Check warning on line 306 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 18

Unexpected any. Specify a different type
);
const content = this.extractContentFromParts(responseData.parts ?? []);
this.logger.debug?.(
`[doGenerate] extracted content: ${JSON.stringify(content.map((c) => ({ type: c.type, ...(c.type === "text" ? { text: (c as any).text?.slice(0, 200) } : {}), ...(c.type === "tool-call" ? { toolName: (c as any).toolName } : {}) })))}`,

Check warning on line 310 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 20

Unexpected any. Specify a different type

Check warning on line 310 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 20

Unexpected any. Specify a different type

Check warning on line 310 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 22

Unexpected any. Specify a different type

Check warning on line 310 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 22

Unexpected any. Specify a different type

Check warning on line 310 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 18

Unexpected any. Specify a different type

Check warning on line 310 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 18

Unexpected any. Specify a different type
);
const usage = this.extractUsageFromParts(responseData.parts ?? []);
const finishReason = mapOpencodeFinishReason(responseData.info);
Expand Down Expand Up @@ -403,9 +403,17 @@
const streamWarnings = [...warnings];
let streamStartEmitted = false;

// The SDK's SSE generator only cancels its fetch reader from an
// abort-signal handler; closing the iterator alone leaves the socket
// open and keeps the Node event loop alive.
const eventAbortController = new AbortController();
const unregisterEventSubscription =
this.clientManager.registerEventSubscription(eventAbortController);

try {
const eventsResult = await client.event.subscribe(
directory ? { directory } : undefined,
{ signal: eventAbortController.signal },
);

const eventStream = eventsResult.stream;
Expand Down Expand Up @@ -484,6 +492,10 @@
return;
}
iteratorClosed = true;
// Abort before iterator.return(): the abort handler cancels the
// SSE reader, which also unblocks a pending iterator.next() the
// return() call would otherwise wait on.
eventAbortController.abort();
if (typeof iterator.return === "function") {
try {
await iterator.return(undefined);
Expand Down Expand Up @@ -581,6 +593,8 @@
controller.enqueue({ type: "error", error: wrappedError });
}
} finally {
eventAbortController.abort();
unregisterEventSubscription();
controller.close();
}
},
Expand Down
2 changes: 2 additions & 0 deletions src/opencode-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ vi.mock("./opencode-client-manager.js", async (importOriginal) => {
}),
dispose: vi.fn().mockResolvedValue(undefined),
getServerUrl: vi.fn().mockReturnValue("http://127.0.0.1:4096"),
registerEventSubscription: vi.fn().mockReturnValue(() => {}),
}),
resetInstance: vi.fn(),
},
createClientManagerFromSettings: vi.fn().mockReturnValue({
getClient: vi.fn().mockResolvedValue({}),
dispose: vi.fn().mockResolvedValue(undefined),
getServerUrl: vi.fn().mockReturnValue("http://127.0.0.1:4096"),
registerEventSubscription: vi.fn().mockReturnValue(() => {}),
}),
};
});
Expand Down
Loading