soothe-client-python 1.0.13__tar.gz → 1.0.14__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 (34) hide show
  1. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/PKG-INFO +1 -1
  2. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/daemon_session.py +54 -12
  3. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/websocket.py +7 -2
  4. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/.gitignore +0 -0
  5. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/LICENSE +0 -0
  6. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/README.md +0 -0
  7. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/pyproject.toml +0 -0
  8. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/__init__.py +0 -0
  9. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/__init__.py +0 -0
  10. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/attachments.py +0 -0
  11. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/broadcaster.py +0 -0
  12. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/card_projection.py +0 -0
  13. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/chunk_filter.py +0 -0
  14. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/classifier.py +0 -0
  15. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/events.py +0 -0
  16. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/loop_session_store.py +0 -0
  17. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/managed_client.py +0 -0
  18. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/observability.py +0 -0
  19. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/pool.py +0 -0
  20. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/query_gate.py +0 -0
  21. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/thinking_step.py +0 -0
  22. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/turn.py +0 -0
  23. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/turn_boundary.py +0 -0
  24. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/turn_runner.py +0 -0
  25. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/command_client.py +0 -0
  26. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/errors.py +0 -0
  27. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/helpers.py +0 -0
  28. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/intent_hints.py +0 -0
  29. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/protocol_params.py +0 -0
  30. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/py.typed +0 -0
  31. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/schemas.py +0 -0
  32. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/session.py +0 -0
  33. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/stream_terminal.py +0 -0
  34. {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/turn_boundary.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-client-python
3
- Version: 1.0.13
3
+ Version: 1.0.14
4
4
  Summary: WebSocket client + appkit for soothe-daemon (Python)
5
5
  Project-URL: Homepage, https://github.com/mirasoth/soothe-client-python
6
6
  Project-URL: Documentation, https://soothe.readthedocs.io
@@ -396,6 +396,7 @@ class DaemonSession:
396
396
  self,
397
397
  *,
398
398
  max_wait_s: float | None = None,
399
+ idle_timeout_s: float | None = None,
399
400
  ) -> AsyncIterator[tuple[tuple[Any, ...], str, Any]]:
400
401
  """Yield ``(namespace, mode, data)`` stream chunks until the turn ends.
401
402
 
@@ -406,6 +407,13 @@ class DaemonSession:
406
407
  max_wait_s: Optional absolute deadline for the whole turn. When set,
407
408
  raises ``TimeoutError`` if the daemon never emits a turn-end
408
409
  signal in time.
410
+ idle_timeout_s: Optional hard cap on how long to wait for the *first*
411
+ progress event before giving up. When set and no progress is seen
412
+ within this many seconds, raises ``TimeoutError`` instead of the
413
+ default 30s-log-and-reset behavior. Used by attach-only reads
414
+ (``skip_daemon_send_turn``) so a stale ``live`` probe that turns
415
+ out to have no real follow-on turn does not spin the thinking
416
+ row for minutes.
409
417
  """
410
418
  self._ensure_turn_floor_attrs()
411
419
  self.turn_event_stats = self._new_turn_stats()
@@ -426,6 +434,12 @@ class DaemonSession:
426
434
  turn_read_started = time.monotonic()
427
435
  first_event_logged = False
428
436
  progress_seen = False
437
+ # Wall-clock of the last *real* progress (a yielded chunk). A stale
438
+ # ``status=running`` frame sets ``progress_seen`` but yields nothing;
439
+ # for the attach-only idle timeout we need a signal that advances when
440
+ # the daemon actually streams content, not when it merely re-announces
441
+ # a (possibly stale) running status.
442
+ last_real_progress_at = turn_read_started
429
443
  absolute_deadline = (
430
444
  time.monotonic() + max_wait_s if max_wait_s is not None and max_wait_s > 0 else None
431
445
  )
@@ -446,15 +460,31 @@ class DaemonSession:
446
460
  f"Turn timed out after {max_wait_s:.0f}s "
447
461
  f"(loop={expected_loop_id or '?'})"
448
462
  )
449
- if not progress_seen and time.monotonic() - turn_read_started > 30.0:
450
- logger.warning(
451
- "No daemon stream progress after %.0fs (loop=%s, "
452
- "query_started=%s); check daemon sender / WebSocket reader",
453
- time.monotonic() - turn_read_started,
454
- (expected_loop_id or "?")[:16],
455
- query_started,
463
+ if not progress_seen:
464
+ elapsed = time.monotonic() - turn_read_started
465
+ if elapsed > 30.0:
466
+ logger.warning(
467
+ "No daemon stream progress after %.0fs (loop=%s, "
468
+ "query_started=%s); check daemon sender / WebSocket reader",
469
+ elapsed,
470
+ (expected_loop_id or "?")[:16],
471
+ query_started,
472
+ )
473
+ turn_read_started = time.monotonic()
474
+ # Attach-only idle timeout: even after a stale
475
+ # ``status=running`` sets ``progress_seen``, no real content
476
+ # may ever arrive (the runner already exited). Bound the wait
477
+ # for the first yielded chunk so the TUI falls back instead
478
+ # of spinning the thinking row for minutes.
479
+ if (
480
+ idle_timeout_s is not None
481
+ and idle_timeout_s > 0
482
+ and time.monotonic() - last_real_progress_at > idle_timeout_s
483
+ ):
484
+ raise TimeoutError(
485
+ f"No daemon stream progress within {idle_timeout_s:.0f}s "
486
+ f"attach window (loop={expected_loop_id or '?'})"
456
487
  )
457
- turn_read_started = time.monotonic()
458
488
  event = await self._client.read_event()
459
489
  if event and not first_event_logged:
460
490
  first_event_logged = True
@@ -653,6 +683,7 @@ class DaemonSession:
653
683
  continue
654
684
 
655
685
  progress_seen = True
686
+ last_real_progress_at = time.monotonic()
656
687
  if is_turn_progress_chunk(mode, data):
657
688
  turn_progress_seen = True
658
689
  yield (namespace, mode, data)
@@ -695,7 +726,7 @@ class DaemonSession:
695
726
  """Return skill rows from the daemon catalog."""
696
727
  async with self._rpc_lock:
697
728
  await self._ensure_rpc_connected()
698
- response = await self._rpc_client.list_skills(timeout=15.0)
729
+ response = await self._rpc_client.list_skills(workspace=self._workspace, timeout=15.0)
699
730
  skills = response.get("skills", [])
700
731
  if not isinstance(skills, list):
701
732
  return []
@@ -844,9 +875,10 @@ class DaemonSession:
844
875
  """Fetch execution-progress snapshot (plan, step_index, iteration, status).
845
876
 
846
877
  Returns a namespace with ``plan``, ``step_index``, ``iteration``,
847
- ``status`` for the loop's bound checkpoint thread. On RPC failure the
848
- fields degrade gracefully (``step_index=0``, ``iteration=0``,
849
- ``status=None``, ``plan=None``) so a resume gate can still proceed.
878
+ ``status``, and ``active_runner`` for the loop's bound checkpoint
879
+ thread. On RPC failure the fields degrade gracefully (``step_index=0``,
880
+ ``iteration=0``, ``status=None``, ``plan=None``, ``active_runner=False``)
881
+ so a resume gate can still proceed.
850
882
  """
851
883
  lid = str(loop_id or "").strip()
852
884
  empty = SimpleNamespace(
@@ -854,6 +886,7 @@ class DaemonSession:
854
886
  step_index=0,
855
887
  iteration=0,
856
888
  status=None,
889
+ active_runner=False,
857
890
  loop_id=lid or None,
858
891
  success=False,
859
892
  )
@@ -881,12 +914,21 @@ class DaemonSession:
881
914
  iteration = iteration_raw if isinstance(iteration_raw, int) else 0
882
915
  status_raw = resp.get("status")
883
916
  status = status_raw if isinstance(status_raw, str) else None
917
+ # ``active_runner`` is absent on daemons older than this field; treat
918
+ # absence as unknown (None) so callers can fall back to status only.
919
+ active_runner_raw = resp.get("active_runner")
920
+ active_runner: bool | None
921
+ if isinstance(active_runner_raw, bool):
922
+ active_runner = active_runner_raw
923
+ else:
924
+ active_runner = None
884
925
  success = bool(resp.get("success", True))
885
926
  return SimpleNamespace(
886
927
  plan=plan,
887
928
  step_index=step_index,
888
929
  iteration=iteration,
889
930
  status=status,
931
+ active_runner=active_runner,
890
932
  loop_id=resp.get("loop_id", lid),
891
933
  success=success,
892
934
  )
@@ -1232,9 +1232,14 @@ class WebSocketClient:
1232
1232
  except Exception as exc:
1233
1233
  raise StaleLoopError(lid, exc) from exc
1234
1234
 
1235
- async def list_skills(self, *, timeout: float = 15.0) -> dict[str, Any]:
1235
+ async def list_skills(
1236
+ self, *, workspace: str | None = None, timeout: float = 15.0
1237
+ ) -> dict[str, Any]:
1236
1238
  """Request wire-safe skill metadata from the daemon."""
1237
- return await self.request("skills_list", {}, timeout=timeout)
1239
+ params: dict[str, Any] = {}
1240
+ if workspace:
1241
+ params["workspace"] = str(workspace).strip()
1242
+ return await self.request("skills_list", params, timeout=timeout)
1238
1243
 
1239
1244
  async def list_models(self, *, timeout: float = 15.0) -> dict[str, Any]:
1240
1245
  """Request model catalog rows from the daemon host ``SootheConfig``."""