fix: word-wrap spinner, interruptable agent join, and delegate_task interrupt#10940
Conversation
…er (#10300) detect_provider_for_model() silently remapped models to OpenRouter when the direct provider's credentials weren't found via env vars. Three bugs: 1. Credential check only looked at env vars from PROVIDER_REGISTRY, missing credential pool entries, auth store, and OAuth tokens 2. When env var check failed, silently returned ('openrouter', slug) instead of the direct provider the model actually belongs to 3. Users with valid credentials via non-env-var mechanisms (pool, OAuth, Claude Code tokens) got silently rerouted Fix: - Expand credential check to also query credential pool and auth store - Always return the direct provider match regardless of credential status -- let client init handle missing creds with a clear error rather than silently routing through the wrong provider Same philosophy as the provider-required fix: don't guess, don't silently reroute, error clearly when something is missing. Closes #10300
|
…nterrupt Three fixes: 1. Spinner widget clips long tool commands — prompt_toolkit Window had height=1 and wrap_lines=False. Now uses wrap_lines=True with dynamic height from text length / terminal width. Long commands wrap naturally. 2. agent_thread.join() blocked forever after interrupt — if the agent thread took time to clean up, the process_loop thread froze. Now polls with 0.2s timeout on the interrupt path, checking _should_exit so double Ctrl+C breaks out immediately. 3. Root cause of 5-hour CLI hang: delegate_task() used as_completed() with no interrupt check. When subagent children got stuck, the parent blocked forever inside the ThreadPoolExecutor. Now polls with wait(timeout=0.5) and checks parent_agent._interrupt_requested each iteration. Stuck children are reported as interrupted, and the parent returns immediately.
c8bc895 to
6f619a2
Compare
|
Upstream's QQ platform hint (added in the salvaged PR NousResearch#10935/NousResearch#10940 block that landed during this merge) re-introduced the 'MEDIA:/absolute/path/to/file' literal that our prior commit 03f8a5e fixed for WeCom. The ANTHROPIC_OAUTH_BLOCKED_LITERALS guard test flagged it. Same fix pattern: 'absolute' -> 'full' to break tokenization.
…nterrupt (NousResearch#10940) * fix: stop /model from silently rerouting direct providers to OpenRouter (NousResearch#10300) detect_provider_for_model() silently remapped models to OpenRouter when the direct provider's credentials weren't found via env vars. Three bugs: 1. Credential check only looked at env vars from PROVIDER_REGISTRY, missing credential pool entries, auth store, and OAuth tokens 2. When env var check failed, silently returned ('openrouter', slug) instead of the direct provider the model actually belongs to 3. Users with valid credentials via non-env-var mechanisms (pool, OAuth, Claude Code tokens) got silently rerouted Fix: - Expand credential check to also query credential pool and auth store - Always return the direct provider match regardless of credential status -- let client init handle missing creds with a clear error rather than silently routing through the wrong provider Same philosophy as the provider-required fix: don't guess, don't silently reroute, error clearly when something is missing. Closes NousResearch#10300 * fix: word-wrap spinner, interruptable agent join, and delegate_task interrupt Three fixes: 1. Spinner widget clips long tool commands — prompt_toolkit Window had height=1 and wrap_lines=False. Now uses wrap_lines=True with dynamic height from text length / terminal width. Long commands wrap naturally. 2. agent_thread.join() blocked forever after interrupt — if the agent thread took time to clean up, the process_loop thread froze. Now polls with 0.2s timeout on the interrupt path, checking _should_exit so double Ctrl+C breaks out immediately. 3. Root cause of 5-hour CLI hang: delegate_task() used as_completed() with no interrupt check. When subagent children got stuck, the parent blocked forever inside the ThreadPoolExecutor. Now polls with wait(timeout=0.5) and checks parent_agent._interrupt_requested each iteration. Stuck children are reported as interrupted, and the parent returns immediately.
…nterrupt (NousResearch#10940) * fix: stop /model from silently rerouting direct providers to OpenRouter (NousResearch#10300) detect_provider_for_model() silently remapped models to OpenRouter when the direct provider's credentials weren't found via env vars. Three bugs: 1. Credential check only looked at env vars from PROVIDER_REGISTRY, missing credential pool entries, auth store, and OAuth tokens 2. When env var check failed, silently returned ('openrouter', slug) instead of the direct provider the model actually belongs to 3. Users with valid credentials via non-env-var mechanisms (pool, OAuth, Claude Code tokens) got silently rerouted Fix: - Expand credential check to also query credential pool and auth store - Always return the direct provider match regardless of credential status -- let client init handle missing creds with a clear error rather than silently routing through the wrong provider Same philosophy as the provider-required fix: don't guess, don't silently reroute, error clearly when something is missing. Closes NousResearch#10300 * fix: word-wrap spinner, interruptable agent join, and delegate_task interrupt Three fixes: 1. Spinner widget clips long tool commands — prompt_toolkit Window had height=1 and wrap_lines=False. Now uses wrap_lines=True with dynamic height from text length / terminal width. Long commands wrap naturally. 2. agent_thread.join() blocked forever after interrupt — if the agent thread took time to clean up, the process_loop thread froze. Now polls with 0.2s timeout on the interrupt path, checking _should_exit so double Ctrl+C breaks out immediately. 3. Root cause of 5-hour CLI hang: delegate_task() used as_completed() with no interrupt check. When subagent children got stuck, the parent blocked forever inside the ThreadPoolExecutor. Now polls with wait(timeout=0.5) and checks parent_agent._interrupt_requested each iteration. Stuck children are reported as interrupted, and the parent returns immediately.
…nterrupt (NousResearch#10940) * fix: stop /model from silently rerouting direct providers to OpenRouter (NousResearch#10300) detect_provider_for_model() silently remapped models to OpenRouter when the direct provider's credentials weren't found via env vars. Three bugs: 1. Credential check only looked at env vars from PROVIDER_REGISTRY, missing credential pool entries, auth store, and OAuth tokens 2. When env var check failed, silently returned ('openrouter', slug) instead of the direct provider the model actually belongs to 3. Users with valid credentials via non-env-var mechanisms (pool, OAuth, Claude Code tokens) got silently rerouted Fix: - Expand credential check to also query credential pool and auth store - Always return the direct provider match regardless of credential status -- let client init handle missing creds with a clear error rather than silently routing through the wrong provider Same philosophy as the provider-required fix: don't guess, don't silently reroute, error clearly when something is missing. Closes NousResearch#10300 * fix: word-wrap spinner, interruptable agent join, and delegate_task interrupt Three fixes: 1. Spinner widget clips long tool commands — prompt_toolkit Window had height=1 and wrap_lines=False. Now uses wrap_lines=True with dynamic height from text length / terminal width. Long commands wrap naturally. 2. agent_thread.join() blocked forever after interrupt — if the agent thread took time to clean up, the process_loop thread froze. Now polls with 0.2s timeout on the interrupt path, checking _should_exit so double Ctrl+C breaks out immediately. 3. Root cause of 5-hour CLI hang: delegate_task() used as_completed() with no interrupt check. When subagent children got stuck, the parent blocked forever inside the ThreadPoolExecutor. Now polls with wait(timeout=0.5) and checks parent_agent._interrupt_requested each iteration. Stuck children are reported as interrupted, and the parent returns immediately.
…nterrupt (NousResearch#10940) * fix: stop /model from silently rerouting direct providers to OpenRouter (NousResearch#10300) detect_provider_for_model() silently remapped models to OpenRouter when the direct provider's credentials weren't found via env vars. Three bugs: 1. Credential check only looked at env vars from PROVIDER_REGISTRY, missing credential pool entries, auth store, and OAuth tokens 2. When env var check failed, silently returned ('openrouter', slug) instead of the direct provider the model actually belongs to 3. Users with valid credentials via non-env-var mechanisms (pool, OAuth, Claude Code tokens) got silently rerouted Fix: - Expand credential check to also query credential pool and auth store - Always return the direct provider match regardless of credential status -- let client init handle missing creds with a clear error rather than silently routing through the wrong provider Same philosophy as the provider-required fix: don't guess, don't silently reroute, error clearly when something is missing. Closes NousResearch#10300 * fix: word-wrap spinner, interruptable agent join, and delegate_task interrupt Three fixes: 1. Spinner widget clips long tool commands — prompt_toolkit Window had height=1 and wrap_lines=False. Now uses wrap_lines=True with dynamic height from text length / terminal width. Long commands wrap naturally. 2. agent_thread.join() blocked forever after interrupt — if the agent thread took time to clean up, the process_loop thread froze. Now polls with 0.2s timeout on the interrupt path, checking _should_exit so double Ctrl+C breaks out immediately. 3. Root cause of 5-hour CLI hang: delegate_task() used as_completed() with no interrupt check. When subagent children got stuck, the parent blocked forever inside the ThreadPoolExecutor. Now polls with wait(timeout=0.5) and checks parent_agent._interrupt_requested each iteration. Stuck children are reported as interrupted, and the parent returns immediately.
…nterrupt (NousResearch#10940) * fix: stop /model from silently rerouting direct providers to OpenRouter (NousResearch#10300) detect_provider_for_model() silently remapped models to OpenRouter when the direct provider's credentials weren't found via env vars. Three bugs: 1. Credential check only looked at env vars from PROVIDER_REGISTRY, missing credential pool entries, auth store, and OAuth tokens 2. When env var check failed, silently returned ('openrouter', slug) instead of the direct provider the model actually belongs to 3. Users with valid credentials via non-env-var mechanisms (pool, OAuth, Claude Code tokens) got silently rerouted Fix: - Expand credential check to also query credential pool and auth store - Always return the direct provider match regardless of credential status -- let client init handle missing creds with a clear error rather than silently routing through the wrong provider Same philosophy as the provider-required fix: don't guess, don't silently reroute, error clearly when something is missing. Closes NousResearch#10300 * fix: word-wrap spinner, interruptable agent join, and delegate_task interrupt Three fixes: 1. Spinner widget clips long tool commands — prompt_toolkit Window had height=1 and wrap_lines=False. Now uses wrap_lines=True with dynamic height from text length / terminal width. Long commands wrap naturally. 2. agent_thread.join() blocked forever after interrupt — if the agent thread took time to clean up, the process_loop thread froze. Now polls with 0.2s timeout on the interrupt path, checking _should_exit so double Ctrl+C breaks out immediately. 3. Root cause of 5-hour CLI hang: delegate_task() used as_completed() with no interrupt check. When subagent children got stuck, the parent blocked forever inside the ThreadPoolExecutor. Now polls with wait(timeout=0.5) and checks parent_agent._interrupt_requested each iteration. Stuck children are reported as interrupted, and the parent returns immediately.
Summary
Three fixes across
cli.pyandtools/delegate_tool.py(94 lines added, 17 removed).1. Spinner clips long tool commands (
cli.py)The prompt_toolkit spinner widget showed running tool commands in a
Window(height=1, wrap_lines=False). Text beyond terminal width was silently clipped.Fix:
wrap_lines=Trueon the Window, with_spinner_widget_height()computing dynamic height from text length / terminal width. Long commands wrap naturally across multiple lines.2. CLI hangs after interrupt (
cli.py)agent_thread.join()blocked indefinitely after an interrupt. If the agent thread took time to clean up, theprocess_loopthread froze.Fix: After an interrupt, poll with
join(timeout=0.2)checking_should_exiteach iteration so double Ctrl+C breaks out immediately. Normal completion uses a generousjoin(timeout=30).3. Root cause:
delegate_task()blocks forever on stuck children (tools/delegate_tool.py)This was the actual 5-hour hang. Found via
interrupt_debug.log— the last event before the freeze was "STOP RUNNING BROADER TASK SUITES" with 3 children. Zero agent.log entries for the next ~4 hours.delegate_task()usedas_completed(futures)with no interrupt check. When the parent agent received an interrupt, it propagated to children viachild.interrupt(), but then waited indefinitely for all futures to complete. If even one child thread got stuck (hung API call, stalled connection),as_completed()never yielded and the parent froze inside theThreadPoolExecutor.Fix: Replace
as_completed()withwait(pending, timeout=0.5, return_when=FIRST_COMPLETED)in a loop that checksparent_agent._interrupt_requestedeach iteration. When interrupted, collects completed results and reports remaining tasks as interrupted. Parent returns immediately.Test plan