monkeybot-cli 0.12.0__tar.gz → 0.13.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/PKG-INFO +1 -1
  2. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/pyproject.toml +1 -1
  3. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/skills/monkeybot/SKILL.md +8 -4
  4. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/chat_session.py +101 -4
  5. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/doctor.py +13 -1
  6. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/image-generator/SKILL.md +1 -1
  7. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_chat_session.py +24 -0
  8. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_doctor.py +6 -2
  9. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/.gitignore +0 -0
  10. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/skills/monkeybot/references/config-sections.md +0 -0
  11. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/__init__.py +0 -0
  12. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/chat_file_index.py +0 -0
  13. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/chat_local_shell.py +0 -0
  14. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/chat_renderer.py +0 -0
  15. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/chat_status_bar.py +0 -0
  16. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/chat_theme.py +0 -0
  17. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/chat_tool_display.py +0 -0
  18. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/chat_tui.py +0 -0
  19. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/chat_tui_widgets.py +0 -0
  20. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/__init__.py +0 -0
  21. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/chat.py +0 -0
  22. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/loop.py +0 -0
  23. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/mcp_login.py +0 -0
  24. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/new.py +0 -0
  25. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/refresh.py +0 -0
  26. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/run_cmd.py +0 -0
  27. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/talk.py +0 -0
  28. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/trace.py +0 -0
  29. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/commands/validate.py +0 -0
  30. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/compat.py +0 -0
  31. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/config_resolve.py +0 -0
  32. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/exit_commands.py +0 -0
  33. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/extras_catalog.py +0 -0
  34. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/gateway_health.py +0 -0
  35. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/main.py +0 -0
  36. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/opensandbox_lifecycle.py +0 -0
  37. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/output.py +0 -0
  38. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/overrides.txt +0 -0
  39. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/process_tree.py +0 -0
  40. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/providers.py +0 -0
  41. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/realtime/__init__.py +0 -0
  42. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/realtime/audio_io.py +0 -0
  43. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/realtime/client.py +0 -0
  44. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/realtime/gateway_manager.py +0 -0
  45. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/realtime/push_to_talk.py +0 -0
  46. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/realtime/session.py +0 -0
  47. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/realtime/session_controller.py +0 -0
  48. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/realtime/talk_ui.py +0 -0
  49. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/realtime/wire_encode.py +0 -0
  50. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/runtime_python.py +0 -0
  51. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold.py +0 -0
  52. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/AGENT.md +0 -0
  53. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/Dockerfile +0 -0
  54. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/__init__.py +0 -0
  55. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/browser/SKILL.md +0 -0
  56. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/command_allowlist.yaml +0 -0
  57. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/dockerignore +0 -0
  58. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/env.example +0 -0
  59. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/goal/SKILL.md +0 -0
  60. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/image-generator/generate_image.py +0 -0
  61. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/loop/SKILL.md +0 -0
  62. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/mcp.json +0 -0
  63. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/monkeybot.example.yaml +0 -0
  64. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/opensandbox.docker.toml +0 -0
  65. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/otel-collector.example.yaml +0 -0
  66. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/permissions.yaml +0 -0
  67. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/templates/coding/AGENT.md +0 -0
  68. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/templates/coding/command_allowlist.yaml +0 -0
  69. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/templates/coding/monkeybot.overrides.yaml +0 -0
  70. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/scaffold_defaults/templates/coding/permissions.yaml +0 -0
  71. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/session_controller.py +0 -0
  72. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/src/monkeybot_cli/terminal_markdown.py +0 -0
  73. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_chat_continue.py +0 -0
  74. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_chat_e2e.py +0 -0
  75. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_chat_errors.py +0 -0
  76. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_chat_file_index.py +0 -0
  77. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_chat_grounding.py +0 -0
  78. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_chat_local_shell.py +0 -0
  79. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_chat_renderer_parity.py +0 -0
  80. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_chat_status_bar.py +0 -0
  81. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_chat_theme.py +0 -0
  82. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_chat_tool_display.py +0 -0
  83. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_chat_tui.py +0 -0
  84. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_cli.py +0 -0
  85. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_config_resolve.py +0 -0
  86. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_extras_catalog.py +0 -0
  87. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_gateway_health.py +0 -0
  88. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_opensandbox_lifecycle.py +0 -0
  89. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_realtime_session_controller.py +0 -0
  90. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_run_cmd.py +0 -0
  91. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_runtime_python.py +0 -0
  92. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_scaffold.py +0 -0
  93. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_terminal_markdown.py +0 -0
  94. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/tests/test_trace_cmd.py +0 -0
  95. {monkeybot_cli-0.12.0 → monkeybot_cli-0.13.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: monkeybot-cli
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
  Summary: CLI to create, configure, validate, and chat with monkeybot agents.
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: httpx>=0.27.0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "monkeybot-cli"
7
- version = "0.12.0"
7
+ version = "0.13.0"
8
8
  description = "CLI to create, configure, validate, and chat with monkeybot agents."
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
@@ -31,11 +31,15 @@ Tell the user what you're installing and why. On macOS/Linux you can run these y
31
31
  ### 2. Install the global CLI
32
32
 
33
33
  ```bash
34
- uv tool install monkeybot-cli
34
+ # browser-harness pins Pillow 12. Core 3.12 needs Pillow 13, and
35
+ # `uv tool install` ignores the package override unless this file is passed.
36
+ # Without it the resolver stays on core 3.10.0.
37
+ printf 'pillow>=13\n' > /tmp/monkeybot-overrides.txt
38
+ uv tool install --overrides /tmp/monkeybot-overrides.txt monkeybot-cli
35
39
  monkeybot --help
36
40
  ```
37
41
 
38
- Upgrade later with `uv tool upgrade monkeybot-cli`.
42
+ Upgrade later with `uv tool install --reinstall --overrides /tmp/monkeybot-overrides.txt monkeybot-cli`.
39
43
 
40
44
  **Do not** tell day-1 users to clone `human-plus-machine/monkeybot` unless they are contributing to the harness itself (see Contributing below).
41
45
 
@@ -47,7 +51,7 @@ If the user arrived via skills.sh:
47
51
  npx skills add human-plus-machine/monkeybot --skill monkeybot
48
52
  ```
49
53
 
50
- …they have **this skill only**. Walk them through Tier 0: install `uv` → `uv tool install monkeybot-cli` → verify `monkeybot --help`. Then continue to Tier 1.
54
+ …they have **this skill only**. Walk them through Tier 0: install `uv` → `uv tool install --overrides /tmp/monkeybot-overrides.txt monkeybot-cli` (write `pillow>=13` into that file first) → verify `monkeybot --help`. Then continue to Tier 1.
51
55
 
52
56
  ## How config works (tell the user this first)
53
57
 
@@ -115,7 +119,7 @@ uv sync
115
119
  | `ollama-local` | None required — `OLLAMA_BASE_URL` (default `http://localhost:11434`). Requests send `keep_alive` 24h so prefix KV cache survives idle. | `monkeybot[ollama]` |
116
120
  | `ollama` | Legacy auto-route (key + blank URL = cloud; explicit URL wins) | `monkeybot[ollama]` |
117
121
 
118
- **Agent-first dependencies.** The CLI is thin — it does **not** install provider/storage extras globally. `monkeybot new` scaffolds a `pyproject.toml` with the selected provider (and any `--with` extras). Run plain `uv sync` in the agent directory. `monkeybot run` / `chat` spawn the gateway from that project's interpreter (`.venv/bin/python`, else `uv run python`), and `doctor` checks extras in that same interpreter. For a config-only tree (just `monkeybot_config/`, no `pyproject.toml`) the gateway uses the CLI's interpreter when it already has MonkeyBot 3.x (and MemPalace if memory is on). If memory is enabled and that interpreter cannot import MemPalace, `run` / `chat` provision a cached CLI-managed venv under `~/.cache/monkeybot/runtimes/` holding `monkeybot[memory]` pinned to the running core (never rewrites a `pyproject.toml`). `doctor` reuses that cache when it is already present. Otherwise extras must be installed in the CLI env (`uv tool install --with 'monkeybot[<extra>]' monkeybot-cli`).
122
+ **Agent-first dependencies.** The CLI is thin — it does **not** install provider/storage extras globally. `monkeybot new` scaffolds a `pyproject.toml` with the selected provider (and any `--with` extras). Run plain `uv sync` in the agent directory. `monkeybot run` / `chat` spawn the gateway from that project's interpreter (`.venv/bin/python`, else `uv run python`), and `doctor` checks extras in that same interpreter. For a config-only tree (just `monkeybot_config/`, no `pyproject.toml`) the gateway uses the CLI's interpreter when it already has MonkeyBot 3.x (and MemPalace if memory is on). If memory is enabled and that interpreter cannot import MemPalace, `run` / `chat` provision a cached CLI-managed venv under `~/.cache/monkeybot/runtimes/` holding `monkeybot[memory]` pinned to the running core (never rewrites a `pyproject.toml`). `doctor` reuses that cache when it is already present. Otherwise extras must be installed in the CLI env (`uv tool install --reinstall --overrides /tmp/monkeybot-overrides.txt --with 'monkeybot[<extra>]' monkeybot-cli`, with `pillow>=13` in that file). A bare `uv tool install` resolves core 3.10.0.
119
123
 
120
124
  `doctor` is the source of truth for credentials and extras — when in doubt, run it and read the `remediation` field (add `monkeybot[<extra>]` to agent deps + `uv sync`).
121
125
 
@@ -241,6 +241,8 @@ class ChatSessionController:
241
241
  self._reconnecting = False
242
242
  self._last_event_id: int | None = None
243
243
  self._abandoned: deque[str] = deque(maxlen=64)
244
+ self._deferred: list[Any] = []
245
+ self._idle_task: asyncio.Task[None] | None = None
244
246
  self._turn_abort = asyncio.Event()
245
247
  self._active_request_id: str | None = None
246
248
  self._hitl_future: asyncio.Future[HitlAnswer] | None = None
@@ -292,6 +294,7 @@ class ChatSessionController:
292
294
  self._reconnecting = False
293
295
  self._last_event_id = None
294
296
  self._stream_task = asyncio.create_task(self._consume_stream())
297
+ self._start_idle_watch()
295
298
  self._emit("session_ready", session_id=self.session_id)
296
299
  self._emit("connection_state", state="connected")
297
300
  await self._emit_transcript_backfill()
@@ -504,6 +507,14 @@ class ChatSessionController:
504
507
  return
505
508
 
506
509
  request_id = str(uuid.uuid4())
510
+ if self._active_request_id is not None:
511
+ self._emit("session_busy")
512
+ return
513
+ await self._stop_idle_watch()
514
+ if self._active_request_id is not None:
515
+ self._emit("session_busy")
516
+ self._start_idle_watch()
517
+ return
507
518
  self._active_request_id = request_id
508
519
  self._last_cancel_ok = None
509
520
  self._turn_abort.clear()
@@ -513,15 +524,23 @@ class ChatSessionController:
513
524
  json={"request_id": request_id, "message": message},
514
525
  )
515
526
  if reply.status_code == 409:
527
+ self._active_request_id = None
516
528
  self._emit("session_busy")
529
+ self._start_idle_watch()
517
530
  return
518
531
  reply.raise_for_status()
519
532
  except httpx.HTTPError as exc:
533
+ self._active_request_id = None
520
534
  self._emit("error", message=format_http_error("Reply", exc))
535
+ self._start_idle_watch()
521
536
  return
522
537
 
523
538
  self._emit("turn_started", request_id=request_id)
524
- await self._run_turn(request_id)
539
+ try:
540
+ await self._run_turn(request_id)
541
+ finally:
542
+ if self._active_request_id is None and not self._closed:
543
+ self._start_idle_watch()
525
544
 
526
545
  async def _dequeue_payload(self) -> str | None | Literal[False]:
527
546
  """Return payload, None for stream end, False on poll timeout."""
@@ -544,9 +563,13 @@ class ChatSessionController:
544
563
  self._warn(f"skipping unparseable event: {exc}")
545
564
  return None
546
565
 
547
- async def _run_turn(self, request_id: str) -> None:
566
+ async def _run_turn(self, request_id: str, *, prelude: list[Any] | None = None) -> None:
548
567
  assert self._client is not None and self.session_id is not None
549
568
  state = _TurnState()
569
+ for evt in prelude or []:
570
+ if state.done or self._turn_abort.is_set():
571
+ break
572
+ await self._dispatch_turn_event(evt, request_id, state)
550
573
  while not state.done and not self._turn_abort.is_set():
551
574
  payload = await self._dequeue_payload()
552
575
  if payload is False:
@@ -577,6 +600,71 @@ class ChatSessionController:
577
600
  if not self._stream_alive and not self._reconnecting:
578
601
  self._emit("stream_ended")
579
602
 
603
+ def _start_idle_watch(self) -> None:
604
+ """Render turns the gateway starts while the prompt is idle."""
605
+ if self._closed or self._active_request_id is not None:
606
+ return
607
+ task = self._idle_task
608
+ if task is not None and not task.done():
609
+ return
610
+ self._idle_task = asyncio.create_task(self._watch_idle(), name="chat-idle-watch")
611
+
612
+ async def _stop_idle_watch(self) -> None:
613
+ task = self._idle_task
614
+ if task is None or task.done():
615
+ self._idle_task = None
616
+ return
617
+ if self._active_request_id is not None:
618
+ return
619
+ task.cancel()
620
+ with contextlib.suppress(asyncio.CancelledError):
621
+ await task
622
+ self._idle_task = None
623
+
624
+ async def _watch_idle(self) -> None:
625
+ """Show a background-subagent wake (or any other turn) the user did not send."""
626
+ try:
627
+ while not self._closed and self._active_request_id is None:
628
+ await self._play_deferred()
629
+ if self._closed or self._active_request_id is not None:
630
+ return
631
+ payload = await self._dequeue_payload()
632
+ if payload is False:
633
+ continue
634
+ if payload is None:
635
+ return
636
+ if self._active_request_id is not None:
637
+ self._event_queue.put_nowait(payload)
638
+ return
639
+ evt = self._decode_event(payload)
640
+ if evt is None:
641
+ continue
642
+ rid = getattr(evt, "request_id", "") or ""
643
+ if not rid or rid in self._abandoned:
644
+ continue
645
+ self._deferred.append(evt)
646
+ except asyncio.CancelledError:
647
+ raise
648
+
649
+ async def _play_deferred(self) -> None:
650
+ while self._deferred and not self._closed and self._active_request_id is None:
651
+ first = self._deferred.pop(0)
652
+ rid = getattr(first, "request_id", "") or ""
653
+ if not rid or rid in self._abandoned:
654
+ continue
655
+ batch = [first]
656
+ rest: list[Any] = []
657
+ for evt in self._deferred:
658
+ if getattr(evt, "request_id", "") == rid:
659
+ batch.append(evt)
660
+ else:
661
+ rest.append(evt)
662
+ self._deferred = rest
663
+ self._active_request_id = rid
664
+ self._turn_abort.clear()
665
+ self._emit("turn_started", request_id=rid)
666
+ await self._run_turn(rid, prelude=batch)
667
+
580
668
  async def _dispatch_turn_event(self, evt: Any, request_id: str, state: _TurnState) -> None:
581
669
  evt_rid = getattr(evt, "request_id", "") or ""
582
670
 
@@ -589,8 +677,9 @@ class ChatSessionController:
589
677
  self._emit("thinking", text="thinking…")
590
678
  return
591
679
 
592
- if evt_rid != request_id:
593
- self._maybe_thinking_trace(evt, request_id)
680
+ if evt_rid and evt_rid != request_id:
681
+ if evt_rid not in self._abandoned:
682
+ self._deferred.append(evt)
594
683
  return
595
684
 
596
685
  if isinstance(evt, GroundingEvent):
@@ -885,6 +974,7 @@ class ChatSessionController:
885
974
  if self._active_request_id is not None:
886
975
  self._abandoned.append(self._active_request_id)
887
976
  self._active_request_id = None
977
+ await self._stop_idle_watch()
888
978
 
889
979
  if self._stream_task is not None:
890
980
  self._stream_task.cancel()
@@ -913,6 +1003,7 @@ class ChatSessionController:
913
1003
  await self._teardown_stream()
914
1004
  await self._create_session()
915
1005
  self._stream_task = asyncio.create_task(self._consume_stream())
1006
+ self._start_idle_watch()
916
1007
  self._emit("session_ready", session_id=self.session_id)
917
1008
  self._emit("connection_state", state="connected")
918
1009
 
@@ -926,6 +1017,7 @@ class ChatSessionController:
926
1017
  await self._teardown_stream()
927
1018
  await self._attach_or_create_session(sid)
928
1019
  self._stream_task = asyncio.create_task(self._consume_stream())
1020
+ self._start_idle_watch()
929
1021
  self._emit("session_ready", session_id=self.session_id)
930
1022
  self._emit("connection_state", state="connected")
931
1023
  await self._emit_transcript_backfill()
@@ -942,6 +1034,11 @@ class ChatSessionController:
942
1034
  if self._closed:
943
1035
  return
944
1036
  self._closed = True
1037
+ if self._idle_task is not None:
1038
+ self._idle_task.cancel()
1039
+ with contextlib.suppress(asyncio.CancelledError, asyncio.TimeoutError, TimeoutError):
1040
+ await asyncio.wait_for(self._idle_task, timeout=0.5)
1041
+ self._idle_task = None
945
1042
  if self._stream_task is not None:
946
1043
  self._stream_task.cancel()
947
1044
  with contextlib.suppress(asyncio.CancelledError, asyncio.TimeoutError, TimeoutError):
@@ -70,6 +70,16 @@ def _agent_defines_project_extra(agent_root: Path, extra: str) -> bool:
70
70
  return isinstance(optional, dict) and extra in optional
71
71
 
72
72
 
73
+ def _cli_overrides_file() -> Path:
74
+ """Pillow floor shipped next to this package.
75
+
76
+ ``uv tool install`` does not read ``[tool.uv] override-dependencies``.
77
+ browser-harness pins Pillow 12, so a bare install resolves core 3.10
78
+ instead of a core that requires Pillow 13.
79
+ """
80
+ return Path(__file__).resolve().parents[1] / "overrides.txt"
81
+
82
+
73
83
  def _extra_remediation(extra: str, agent_root: Path, runtime) -> str:
74
84
  """Remediation text pointing at the agent project, not the CLI env."""
75
85
  if runtime.source in {"cli", MANAGED_RUNTIME_SOURCE}:
@@ -78,9 +88,11 @@ def _extra_remediation(extra: str, agent_root: Path, runtime) -> str:
78
88
  if runtime.source == MANAGED_RUNTIME_SOURCE
79
89
  else ""
80
90
  )
91
+ overrides = _cli_overrides_file()
81
92
  return (
82
93
  "Config-only tree: install in the CLI env — "
83
- f"uv tool install --with 'monkeybot[{extra}]' monkeybot-cli"
94
+ f"uv tool install --reinstall --overrides {overrides} "
95
+ f"--with 'monkeybot[{extra}]' monkeybot-cli"
84
96
  f"{refresh}"
85
97
  )
86
98
  if _agent_defines_project_extra(agent_root, extra):
@@ -39,7 +39,7 @@ workspace-relative.
39
39
  }
40
40
  ```
41
41
 
42
- Replace `<skills_path>` with the absolute `skills_path` returned by `list_skills`.
42
+ Replace `<skills_path>` with the `skills_path` returned by `list_skills` (`skills`).
43
43
 
44
44
  Supported `--aspect-ratio` values: `1:1`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `16:9`, `21:9`.
45
45
 
@@ -448,3 +448,27 @@ def test_close_deletes_session_and_captures_report_dir() -> None:
448
448
  assert client.delete.await_count == 1
449
449
 
450
450
  asyncio.run(_run())
451
+
452
+
453
+ def test_idle_watch_shows_a_turn_the_user_did_not_send() -> None:
454
+ async def _run() -> None:
455
+ events: list[ChatUiEvent] = []
456
+ controller = ChatSessionController(base="http://localhost:8080", emit=events.append)
457
+ controller.session_id = "s"
458
+ controller._client = object() # type: ignore[assignment]
459
+ controller._event_queue.put_nowait(
460
+ '{"type":"AssistantDelta","request_id":"wake-1","delta":"survey"}'
461
+ )
462
+ controller._event_queue.put_nowait('{"type":"TurnComplete","request_id":"wake-1"}')
463
+ controller._start_idle_watch()
464
+ for _ in range(40):
465
+ if any(event.kind == "turn_complete" for event in events):
466
+ break
467
+ await asyncio.sleep(0.05)
468
+ await controller._stop_idle_watch()
469
+ deltas = [event.payload.get("delta") for event in events if event.kind == "assistant_delta"]
470
+ assert deltas == ["survey"]
471
+ assert any(event.kind == "turn_complete" for event in events)
472
+ assert any(event.kind == "turn_started" for event in events)
473
+
474
+ asyncio.run(_run())
@@ -46,14 +46,18 @@ def test_extra_remediation_keeps_extra_for_project_optionals(tmp_path: Path) ->
46
46
  def test_extra_remediation_config_only_cli_env(tmp_path: Path) -> None:
47
47
  runtime = SimpleNamespace(source="cli")
48
48
  text = _extra_remediation("bedrock", tmp_path, runtime)
49
- assert "uv tool install --with 'monkeybot[bedrock]' monkeybot-cli" in text
49
+ assert "--overrides " in text
50
+ assert "overrides.txt" in text
51
+ assert "--with 'monkeybot[bedrock]' monkeybot-cli" in text
50
52
  assert "refresh the managed runtime" not in text
51
53
 
52
54
 
53
55
  def test_extra_remediation_config_only_managed_runtime(tmp_path: Path) -> None:
54
56
  runtime = SimpleNamespace(source="cli-managed")
55
57
  text = _extra_remediation("openai", tmp_path, runtime)
56
- assert "uv tool install --with 'monkeybot[openai]' monkeybot-cli" in text
58
+ assert "--overrides " in text
59
+ assert "overrides.txt" in text
60
+ assert "--with 'monkeybot[openai]' monkeybot-cli" in text
57
61
  assert "refresh the managed runtime" in text
58
62
 
59
63
 
File without changes