soothe-client-python 0.9.8__tar.gz → 0.9.9__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 (32) hide show
  1. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/PKG-INFO +1 -1
  2. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/appkit/daemon_session.py +20 -4
  3. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/turn_boundary.py +14 -0
  4. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/.gitignore +0 -0
  5. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/LICENSE +0 -0
  6. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/README.md +0 -0
  7. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/pyproject.toml +0 -0
  8. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/__init__.py +0 -0
  9. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/appkit/__init__.py +0 -0
  10. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/appkit/attachments.py +0 -0
  11. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/appkit/broadcaster.py +0 -0
  12. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/appkit/chunk_filter.py +0 -0
  13. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/appkit/classifier.py +0 -0
  14. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/appkit/events.py +0 -0
  15. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/appkit/managed_client.py +0 -0
  16. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/appkit/observability.py +0 -0
  17. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/appkit/pool.py +0 -0
  18. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/appkit/query_gate.py +0 -0
  19. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/appkit/session_store.py +0 -0
  20. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/appkit/thinking_step.py +0 -0
  21. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/appkit/turn.py +0 -0
  22. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/appkit/turn_runner.py +0 -0
  23. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/errors.py +0 -0
  24. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/helpers.py +0 -0
  25. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/intent_hints.py +0 -0
  26. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/protocol_params.py +0 -0
  27. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/py.typed +0 -0
  28. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/schemas.py +0 -0
  29. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/session.py +0 -0
  30. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/stream_terminal.py +0 -0
  31. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/websocket.py +0 -0
  32. {soothe_client_python-0.9.8 → soothe_client_python-0.9.9}/src/soothe_client/ws_command_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-client-python
3
- Version: 0.9.8
3
+ Version: 0.9.9
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
@@ -23,7 +23,7 @@ from soothe_client.stream_terminal import (
23
23
  is_turn_end_custom_data,
24
24
  is_turn_progress_chunk,
25
25
  )
26
- from soothe_client.turn_boundary import frame_seq, frame_turn_id
26
+ from soothe_client.turn_boundary import frame_seq, frame_turn_id, parse_turn_generation
27
27
  from soothe_client.websocket import WebSocketClient
28
28
 
29
29
  logger = logging.getLogger(__name__)
@@ -392,13 +392,19 @@ class DaemonSession:
392
392
  if ev_seq is not None and last_end_seq > 0 and ev_seq <= last_end_seq:
393
393
  continue
394
394
 
395
- # IG-659 phases 1–2: drop mismatched turn_id once bound.
395
+ # Drop mismatched turn_id once bound — except status=running,
396
+ # which may rebind to a newer generation (pre-admit early
397
+ # running can briefly carry a stale or empty turn_id).
396
398
  ev_turn_id = frame_turn_id(event)
399
+ is_running_status = (
400
+ event_type == "status" and str(event.get("state") or "") == "running"
401
+ )
397
402
  if (
398
403
  expected_turn_id
399
404
  and ev_turn_id
400
405
  and ev_turn_id != expected_turn_id
401
406
  and event_type in {"event", "status"}
407
+ and not is_running_status
402
408
  ):
403
409
  logger.debug(
404
410
  "Ignoring mismatched turn_id frame type=%s got=%s expected=%s",
@@ -426,8 +432,18 @@ class DaemonSession:
426
432
  progress_seen = True
427
433
  status_turn = frame_turn_id(event)
428
434
  if status_turn:
429
- expected_turn_id = status_turn
430
- self._expected_turn_id = status_turn
435
+ new_gen = parse_turn_generation(status_turn)
436
+ old_gen = parse_turn_generation(expected_turn_id)
437
+ if expected_turn_id is None or (
438
+ new_gen is not None and (old_gen is None or new_gen >= old_gen)
439
+ ):
440
+ if expected_turn_id and status_turn != expected_turn_id:
441
+ # Successor turn admitted while this reader
442
+ # still held a stale/early binding.
443
+ stream_payload_seen = False
444
+ turn_progress_seen = False
445
+ expected_turn_id = status_turn
446
+ self._expected_turn_id = status_turn
431
447
  elif query_started and state == "stopped":
432
448
  self.last_turn_end_state = state
433
449
  if ev_seq is not None:
@@ -18,6 +18,19 @@ def format_turn_id(loop_id: str, generation: int) -> str:
18
18
  return f"{lid}:{gen}"
19
19
 
20
20
 
21
+ def parse_turn_generation(turn_id: str | None) -> int | None:
22
+ """Extract generation int from ``turn_id``, or None if malformed."""
23
+ raw = str(turn_id or "").strip()
24
+ if not raw or ":" not in raw:
25
+ return None
26
+ suffix = raw.rsplit(":", 1)[-1]
27
+ try:
28
+ gen = int(suffix)
29
+ except ValueError:
30
+ return None
31
+ return gen if gen > 0 else None
32
+
33
+
21
34
  def frame_turn_id(frame: dict[str, Any] | None) -> str | None:
22
35
  """Return ``turn_id`` from a status/event frame or nested custom data."""
23
36
  if not isinstance(frame, dict):
@@ -51,4 +64,5 @@ __all__ = [
51
64
  "format_turn_id",
52
65
  "frame_seq",
53
66
  "frame_turn_id",
67
+ "parse_turn_generation",
54
68
  ]