Skip to content

fix: Fix tool-bridge slot leak when session.abort() throws inside runAgentTurn (#73)#77

Open
KochC wants to merge 1 commit into
mainfrom
fix/issue-73
Open

fix: Fix tool-bridge slot leak when session.abort() throws inside runAgentTurn (#73)#77
KochC wants to merge 1 commit into
mainfrom
fix/issue-73

Conversation

@KochC

@KochC KochC commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

Wrapped the entire post-bridge-acquisition body of runAgentTurn() in an outer try/finally that calls releaseToolBridge(bridge), so the pool slot is always returned even if session.create(), event.subscribe(), or session.promptAsync() throw before the event-loop is entered.

Closes #73

Diff
diff --git a/index.js b/index.js
index d55980c..65d511c 100644
--- a/index.js
+++ b/index.js
@@ -752,59 +752,62 @@ async function runAgentTurn(client, model, messages, system, callerTools, onChun
     toolsMap = buildToolsMap(baseTools, bridge)
   }
 
-  const session = await client.session.create({ body: { title: `Proxy: ${model.id}` } })
-  const sessionID = session.data.id
-  const prompt = buildPrompt(messages)
-  const toolIDSet = bridge ? new Set(bridge.toolIDs) : null
+  // Wrap all work that happens after bridge acquisition in a try/finally so the
+  // slot is always returned to the pool even if session.create, event.subscribe,
+  // or session.promptAsync throw before the event-loop runs.
+  try {
+    const session = await client.session.create({ body: { title: `Proxy: ${model.id}` } })
+    const sessionID = session.data.id
+    const prompt = buildPrompt(messages)
+    const toolIDSet = bridge ? new Set(bridge.toolIDs) : null
 
-  // Subscribe to the event stream before sending the prompt so we don't miss events.
-  const { stream } = await client.event.subscribe()
+    // Subscribe to the event stream before sending the prompt so we don't miss events.
+    const { stream } = await client.event.subscribe()
 
-  await client.session.promptAsync({
-    path: { id: sessionID },
-    body: {
-      model: { providerID: model.providerID, modelID: model.modelID },
-      system,
-      tools: toolsMap,
-      parts: [{ type: "text", text: prompt }],
-    },
-  })
+    await client.session.promptAsync({
+      path: { id: sessionID },
+      body: {
+        model: { providerID: model.providerID, modelID: model.modelID },
+        system,
+        tools: toolsMap,
+        parts: [{ type: "text", text: prompt }],
+      },
+    })
 
-  let errorMessage = null
-  let content = ""
-  const toolCallsByID = new Map()
-  let toolMessageID = null
+    let errorMessage = null
+    let content = ""
+    const toolCallsByID = new Map()
+    let toolMessageID = null
 
-  const recordToolPart = (part) => { ... }
+    const recordToolPart = (part) => { ... }
 
-  try {
     for await (const event of stream) { ... }
-  } finally {
-    releaseToolBridge(bridge)
-  }
 
-  // ... build result and return
+    // ... build result and return
+    return { sessionID, content, toolCalls, tokens, finish }
+  } finally {
+    releaseToolBridge(bridge)
   }
 }

Automated fix by OpenCode Agent

…hrows inside runAgentTurn

Move all post-bridge-acquisition work in runAgentTurn() into a single outer
try/finally that calls releaseToolBridge(bridge). Previously, session.create(),
event.subscribe(), and session.promptAsync() were called *before* the inner
try/finally that released the slot, so any throw in that window permanently
lost the bridge slot from the pool. The outer try/finally guarantees the slot
is returned even if these calls throw, preventing pool exhaustion.

Also adds a regression test that fires 20 sequential requests with a always-
failing session.create (more than the default pool size of 8) to confirm the
pool is never exhausted when errors occur after bridge acquisition.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix tool-bridge slot leak when session.abort() throws inside runAgentTurn

1 participant