From 1f57f9ffc8942fad9e078c37fa0c3549ca520ac8 Mon Sep 17 00:00:00 2001
From: garnet
Date: Tue, 12 May 2026 21:11:12 -0500
Subject: [PATCH] fix(orchestrator): lazy threadpool imports for pyodide
consumers (sy-2wa)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Move `import threading` + `from concurrent.futures import ThreadPoolExecutor,
as_completed` from module top to function-local imports in orchestrator,
synthesis, and perturbation. The `synth_panel.ensemble` load chain is now
fully threadpool-free at module load time, unblocking Cloudflare Python
Workers / pyodide consumers (boardroom DECISION-skill officers) where
ThreadPoolExecutor exists as a stub but `.submit()` silently hangs.
Adds tests/test_threadpool_lazy_import.py — subprocess-based load-chain
hygiene assertions: (1) concurrent.futures stays out of sys.modules after
a fresh ensemble import, (2) ensemble loads cleanly under a poisoned
concurrent.futures module, (3) ThreadPoolExecutor/as_completed are never
bound at the top of orchestrator/synthesis/perturbation.
Bumps __version__ to 1.5.0 + refreshes server-card.json / site renders.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
CHANGELOG.md | 31 ++++++
site/.well-known/mcp/server-card.json | 6 +-
site/index.html | 8 +-
site/index.md | 2 +-
src/synth_panel/__version__.py | 2 +-
src/synth_panel/orchestrator.py | 24 +++-
src/synth_panel/perturbation.py | 5 +-
src/synth_panel/synthesis.py | 7 +-
tests/test_threadpool_lazy_import.py | 151 ++++++++++++++++++++++++++
9 files changed, 222 insertions(+), 14 deletions(-)
create mode 100644 tests/test_threadpool_lazy_import.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 51210bc..cb3fe19 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,37 @@ For auto-generated release notes, see [GitHub Releases](https://github.com/DataV
(Empty — next-cycle work lands here.)
+## [1.5.0] - 2026-05-13
+
+Pyodide / Cloudflare Python Workers consumers can now import
+``synth_panel.ensemble`` without dragging ``ThreadPoolExecutor`` into
+the load chain. Boardroom (and any other Workers-style consumer that
+adopts ``synthesize_panel``) is unblocked: the ensemble surface is
+fully threadpool-free at load time, so transitively-bound ``.submit()``
+calls can no longer deadlock the Worker runtime.
+
+### Changed
+
+- **Lazy threading imports across the ensemble load chain (sy-2wa).**
+ ``synth_panel.orchestrator``, ``synth_panel.synthesis``, and
+ ``synth_panel.perturbation`` no longer bind ``ThreadPoolExecutor`` /
+ ``as_completed`` / ``threading`` at module top. The imports are
+ hoisted into the threaded entry points (``run_panel_parallel``,
+ ``synthesize_panel_mapreduce``, ``generate_panel_variants_parallel``,
+ ``WorkerRegistry.__init__``) so ``from synth_panel.ensemble import
+ synthesize_panel`` never touches ``concurrent.futures``. Boardroom's
+ 22 s ``asyncio.wait_for`` fallback around ``synthesize_panel`` (PR #11)
+ can now be removed (or kept as defense-in-depth).
+
+### Added
+
+- **``tests/test_threadpool_lazy_import.py`` (sy-2wa).** CI test that
+ asserts ``concurrent.futures`` stays out of ``sys.modules`` after a
+ fresh ``synth_panel.ensemble`` load, runs the same import against a
+ poisoned ``concurrent.futures`` (any access raises), and pins the
+ ``orchestrator`` / ``synthesis`` / ``perturbation`` module namespaces
+ as ``ThreadPoolExecutor``-free.
+
## [1.4.0] - 2026-05-12
OpenRouter cost actuals are now surfaced explicitly alongside the local
diff --git a/site/.well-known/mcp/server-card.json b/site/.well-known/mcp/server-card.json
index 5c68611..5660000 100644
--- a/site/.well-known/mcp/server-card.json
+++ b/site/.well-known/mcp/server-card.json
@@ -3,7 +3,7 @@
"name": "io.github.DataViking-Tech/synthpanel",
"title": "SynthPanel",
"description": "Run synthetic focus groups using AI personas. 12 MCP tools for single prompts, full panel runs, and v3 branching (adaptive) instruments across any LLM provider (Claude, OpenAI, Gemini, xAI).",
- "version": "1.4.0",
+ "version": "1.5.0",
"websiteUrl": "https://synthpanel.dev",
"repository": {
"url": "https://github.com/DataViking-Tech/SynthPanel",
@@ -11,7 +11,7 @@
},
"serverInfo": {
"name": "synthpanel",
- "version": "1.4.0"
+ "version": "1.5.0"
},
"capabilities": {
"tools": { "listChanged": false },
@@ -23,7 +23,7 @@
"registryType": "pypi",
"registryBaseUrl": "https://pypi.org",
"identifier": "synthpanel",
- "version": "1.4.0",
+ "version": "1.5.0",
"runtimeHint": "uvx",
"runtimeArguments": [
{ "type": "positional", "value": "synthpanel[mcp]" },
diff --git a/site/index.html b/site/index.html
index 08722d0..803143c 100644
--- a/site/index.html
+++ b/site/index.html
@@ -45,8 +45,8 @@
"applicationCategory": "DeveloperApplication",
"applicationSubCategory": "Research Tool",
"operatingSystem": "Cross-platform",
- "softwareVersion": "1.4.0",
- "dateModified": "2026-05-12",
+ "softwareVersion": "1.5.0",
+ "dateModified": "2026-05-13",
"license": "https://opensource.org/licenses/MIT",
"codeRepository": "https://github.com/DataViking-Tech/SynthPanel",
"downloadUrl": "https://pypi.org/project/synthpanel/",
@@ -140,7 +140,7 @@
class="mb-4 inline-flex items-center gap-2 rounded-full border border-emerald-400/30 bg-emerald-400/5 px-3 py-1 text-xs font-medium text-emerald-300"
>
- v1.4.0 — public beta
+ v1.5.0 — public beta