Commit c03d4bfc5ce7

Vincent Demeester <vincent@sbr.pm>
2026-06-08 22:47:05
fix(pi): serialize AskUserQuestion UI to avoid deadlock
Concurrent tool calls in a single turn each grabbed the single TUI input focus, so multiple questions fought for it and none resolved, leaving the turn hung with no tool results. Added a module-level async mutex that presents queued questions sequentially and releases on cancel or error.
1 parent 9bdd6b2
Changed files (1)
dots
pi
agent
extensions
dots/pi/agent/extensions/ask-user.ts
@@ -16,6 +16,30 @@ import { DynamicBorder, getSettingsListTheme } from "@mariozechner/pi-coding-age
 import { Container, type SettingItem, SettingsList, Text } from "@mariozechner/pi-tui";
 import { Type } from "@sinclair/typebox";
 
+/**
+ * Serializes TUI access across concurrent tool calls.
+ *
+ * The harness may execute several tool calls from a single assistant turn
+ * concurrently. Since AskUserQuestion grabs the single TUI input focus
+ * (ctx.ui.custom/select/input), running more than one at once deadlocks:
+ * the overlays fight for focus and none ever resolves, so no tool results
+ * are produced and the turn hangs forever.
+ *
+ * This mutex chains executions so each question is presented (and answered)
+ * before the next one starts.
+ */
+let uiLock: Promise<void> = Promise.resolve();
+
+function acquireUILock(): Promise<() => void> {
+	let release!: () => void;
+	const next = new Promise<void>((resolve) => {
+		release = resolve;
+	});
+	const prev = uiLock;
+	uiLock = uiLock.then(() => next);
+	return prev.then(() => release);
+}
+
 export default function (pi: ExtensionAPI) {
 	pi.registerTool({
 		name: "AskUserQuestion",
@@ -57,6 +81,10 @@ export default function (pi: ExtensionAPI) {
 				};
 			}
 
+			// Serialize: if the model asked several questions in one turn, present
+			// them sequentially instead of letting the overlays deadlock.
+			const release = await acquireUILock();
+			try {
 			const { question, suggestions, multiSelect } = params;
 			const hasSuggestions = suggestions && suggestions.length > 0;
 
@@ -158,6 +186,9 @@ export default function (pi: ExtensionAPI) {
 				content: [{ type: "text", text: `User answered: ${answer}` }],
 				details: { question, suggestions, answer },
 			};
+			} finally {
+				release();
+			}
 		},
 	});
 }