Conversation
- Create subprocess_utils.py with no_window_flags() helper - Apply CREATE_NO_WINDOW to all subprocess.run/Popen calls across 17 files - Monkey-patch anyio.open_process in Claude SDK adapter for headless ops - Fix claude_agent_sdk.py default model: use CLAUDE_MODEL env var instead of hardcoded claude-opus-4-6
…ess buffer to 5000 - Return 400 if no heads are active when triggering Night Shift - Increase progress deque from 100 to 5000 to prevent event loss on large runs
…harvest - Add on_progress callback to harvest_all() - Emit per-project events with project name, index, total, and file count
…on stages - Add on_chunk_progress callback to base extractor map_generate() - All 4 extractors pass chunk progress callback through - stages_early emits file_start/file_done for each extraction + passes on_progress to harvester
There was a problem hiding this comment.
Pull request overview
This PR restores Windows headless behavior by suppressing subprocess pop-up windows, and adds richer Night Shift progress reporting/guardrails that were lost in a prior reset.
Changes:
- Introduces
no_window_flags()and applies it broadly to subprocess launches (plus a Windows-specific anyio monkey-patch in the Claude SDK adapter). - Adds Night Shift progress events (project/file/chunk) and increases the in-memory progress buffer size.
- Adds an API guard to prevent Night Shift runs when no heads are active, and updates Claude SDK adapter defaults.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/0-nano/test_subprocess_utils.py | Adds unit coverage for no_window_flags() behavior. |
| src/multihead/subprocess_utils.py | New helper for cross-platform Windows console popup suppression. |
| src/multihead/shell/core.py | Applies creationflags=no_window_flags() to subprocess call(s). |
| src/multihead/shell/context.py | Applies popup suppression to several git/shell subprocess calls. |
| src/multihead/session_harvester/harvester.py | Adds optional progress callback + emits project_start/project_done. |
| src/multihead/resource_monitor.py | Suppresses popups for nvidia-smi subprocess sampling. |
| src/multihead/resilience.py | Suppresses popups for health-check subprocess call(s). |
| src/multihead/night_shift/stages_late.py | Suppresses popups for git subprocess usage in late stages. |
| src/multihead/night_shift/stages_early.py | Wires harvester progress + emits file/chunk progress events for extraction stages. |
| src/multihead/narrative/source_extractors/git_extractor.py | Suppresses popups for git subprocess calls. |
| src/multihead/mcp_server/_tools_decompose.py | Suppresses popups for decomposition subprocess invocation. |
| src/multihead/init_wizard/hardware.py | Suppresses popups for hardware detection subprocess calls. |
| src/multihead/github_integration.py | Suppresses popups for git/gh subprocess calls. |
| src/multihead/extractors/topic_assigner.py | Plumbs on_chunk_progress into shared extraction path. |
| src/multihead/extractors/test_results_extractor.py | Suppresses popups for test runner subprocess call. |
| src/multihead/extractors/event_extractor.py | Plumbs on_chunk_progress into shared extraction path. |
| src/multihead/extractors/entity_extractor.py | Plumbs on_chunk_progress into shared extraction path. |
| src/multihead/extractors/claim_extractor.py | Plumbs on_chunk_progress into shared extraction path. |
| src/multihead/extractors/ci_extractor.py | Suppresses popups for gh api subprocess calls. |
| src/multihead/extractors/base.py | Adds on_chunk_progress hook and emits progress during sequential generation. |
| src/multihead/diagnostics.py | Suppresses popups for diagnostic subprocess call(s). |
| src/multihead/claim_corroboration.py | Suppresses popups for git subprocess calls. |
| src/multihead/autonomous_executor/strategies.py | Suppresses popups for executor subprocess launches. |
| src/multihead/api/routes_system.py | Adds popup suppression to Windows restart process creation flags. |
| src/multihead/api/routes_nightshift.py | Adds active-head guard, increases progress buffer to 5000. |
| src/multihead/adapters/claude_agent_sdk.py | Changes default model + monkey-patches anyio subprocess creation on Windows. |
| scripts/claude_worker.py | Suppresses popups for worker subprocess calls (Claude + tmux). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
|
|
||
| def test_no_window_flags_windows(): | ||
| with patch.object(sys, "platform", "win32"): |
There was a problem hiding this comment.
On non-Windows platforms, subprocess.CREATE_NO_WINDOW is typically not defined, so this test will raise AttributeError even though sys.platform is patched. Patch/define subprocess.CREATE_NO_WINDOW in the test (e.g., via patch.object(..., create=True)) or assert via getattr(subprocess, "CREATE_NO_WINDOW", ...) so the test suite passes on Linux/macOS.
| with patch.object(sys, "platform", "win32"): | |
| with patch.object(sys, "platform", "win32"), patch.object(subprocess, "CREATE_NO_WINDOW", 0, create=True): |
| def no_window_flags() -> int: | ||
| """Return CREATE_NO_WINDOW on Windows, 0 elsewhere.""" | ||
| if sys.platform == "win32": | ||
| return subprocess.CREATE_NO_WINDOW |
There was a problem hiding this comment.
subprocess.CREATE_NO_WINDOW may not exist on non-Windows interpreters (even if sys.platform is spoofed in tests). Using getattr(subprocess, "CREATE_NO_WINDOW", 0) (or a hasattr check) avoids an AttributeError and makes this helper more robust.
| return subprocess.CREATE_NO_WINDOW | |
| return getattr(subprocess, "CREATE_NO_WINDOW", 0) |
| return {"status": "already_running"} | ||
| _nightshift_status["running"] = True | ||
| _nightshift_status["current_stage"] = None | ||
| _nightshift_status["progress"] = deque(maxlen=100) | ||
| _nightshift_status["progress"] = deque(maxlen=5000) | ||
|
|
There was a problem hiding this comment.
Progress events are appended to _nightshift_status["progress"] (via the debug progress callback) while /status converts the same deque to a list. Mutating a deque during iteration can raise RuntimeError, and (because _emit swallows exceptions) can silently drop progress events. Consider synchronizing both appends and reads (e.g., protect with a lock, or funnel updates through an async task that holds _nightshift_lock).
| concurrency: int = Query(1, description="Parallel LLM calls per stage (1=sequential)"), | ||
| ) -> dict[str, Any]: | ||
| """Trigger a Night Shift run in the background.""" | ||
| # Check for at least one active head before starting | ||
| head_manager = request.app.state.head_manager | ||
| states = head_manager.get_states() | ||
| has_active = any( | ||
| info.get("state") == HeadState.ACTIVE.value | ||
| for info in states.values() | ||
| ) | ||
| if not has_active: | ||
| return JSONResponse( | ||
| status_code=400, | ||
| content={"error": "No active heads — wake a head before running Night Shift"}, | ||
| ) |
There was a problem hiding this comment.
This endpoint is annotated to return dict[str, Any] but returns a JSONResponse for the 400 case. To keep the return type consistent and improve generated OpenAPI docs, prefer raising fastapi.HTTPException(status_code=400, detail=...) (or update the return type annotation/response model to include the JSONResponse path).
| """Build a callback that emits file_start/file_done/chunk_progress events.""" | ||
| total = len(chunks) |
There was a problem hiding this comment.
total = len(chunks) is assigned but never used, which is easy to miss and may be flagged by linters. Either remove it or use it (and consider aligning the docstring with the implementation: the returned callback currently emits only chunk_progress, not file_start/file_done).
| """Build a callback that emits file_start/file_done/chunk_progress events.""" | |
| total = len(chunks) | |
| """Build a callback that emits chunk_progress events for the given chunks.""" |
Summary
Restores Windows headless behavior and adds Night Shift progress reporting, lost during 2026-03-20 reset.
Popup suppression (W1-W3)
subprocess_utils.pywithno_window_flags()helperCREATE_NO_WINDOWto all 24 subprocess calls across 17 filesanyio.open_processin Claude SDK adapter (lazy, only when adapter loads)Model defaults (W4)
claude-opus-4-6toclaude-sonnet-4-6(matches CLAUDE_MODEL pattern)Active head guard (W7)
Progress events (W8-W9)
project_start/project_doneevents during session harvestfile_start/file_done/chunk_progressevents during extraction stages (entity, topic, event, claim)Tests
no_window_flags()