soothe-client-python 0.9.8__tar.gz → 0.9.10__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 (33) hide show
  1. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/PKG-INFO +6 -3
  2. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/README.md +5 -2
  3. soothe_client_python-0.9.10/src/soothe_client/__init__.py +189 -0
  4. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/appkit/__init__.py +9 -44
  5. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/appkit/daemon_session.py +52 -12
  6. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/helpers.py +5 -1
  7. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/protocol_params.py +5 -0
  8. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/session.py +6 -9
  9. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/stream_terminal.py +3 -3
  10. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/turn_boundary.py +18 -3
  11. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/ws_command_client.py +121 -156
  12. soothe_client_python-0.9.8/src/soothe_client/__init__.py +0 -194
  13. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/.gitignore +0 -0
  14. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/LICENSE +0 -0
  15. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/pyproject.toml +0 -0
  16. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/appkit/attachments.py +0 -0
  17. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/appkit/broadcaster.py +0 -0
  18. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/appkit/chunk_filter.py +0 -0
  19. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/appkit/classifier.py +0 -0
  20. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/appkit/events.py +0 -0
  21. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/appkit/managed_client.py +0 -0
  22. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/appkit/observability.py +0 -0
  23. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/appkit/pool.py +0 -0
  24. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/appkit/query_gate.py +0 -0
  25. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/appkit/session_store.py +0 -0
  26. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/appkit/thinking_step.py +0 -0
  27. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/appkit/turn.py +0 -0
  28. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/appkit/turn_runner.py +0 -0
  29. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/errors.py +0 -0
  30. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/intent_hints.py +0 -0
  31. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/py.typed +0 -0
  32. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/schemas.py +0 -0
  33. {soothe_client_python-0.9.8 → soothe_client_python-0.9.10}/src/soothe_client/websocket.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.10
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
@@ -69,10 +69,13 @@ More patterns: [`examples/`](examples/) (hello → streaming → multi-turn →
69
69
 
70
70
  | Need | Use |
71
71
  |------|-----|
72
- | One conversation, stream replies | `DaemonSession` |
72
+ | One conversation, stream replies | `DaemonSession` (`soothe_client.appkit`) |
73
+ | Jobs / cron (async) | `AsyncCommandClient` (alias of `WsCommandClient`) |
74
+ | Jobs / cron (scripts / sync) | `CommandClient` (alias of `SyncWsCommandClient`) |
73
75
  | Raw WebSocket / custom RPCs | `WebSocketClient` |
74
76
  | Many users / HTTP backend | `ConnectionPool` + `TurnRunner` |
75
- | Jobs / autopilot / cron | `WsCommandClient` |
77
+
78
+ Wire request param models live in `soothe_client.protocol_params` (not at package root).
76
79
 
77
80
  ## Develop
78
81
 
@@ -36,10 +36,13 @@ More patterns: [`examples/`](examples/) (hello → streaming → multi-turn →
36
36
 
37
37
  | Need | Use |
38
38
  |------|-----|
39
- | One conversation, stream replies | `DaemonSession` |
39
+ | One conversation, stream replies | `DaemonSession` (`soothe_client.appkit`) |
40
+ | Jobs / cron (async) | `AsyncCommandClient` (alias of `WsCommandClient`) |
41
+ | Jobs / cron (scripts / sync) | `CommandClient` (alias of `SyncWsCommandClient`) |
40
42
  | Raw WebSocket / custom RPCs | `WebSocketClient` |
41
43
  | Many users / HTTP backend | `ConnectionPool` + `TurnRunner` |
42
- | Jobs / autopilot / cron | `WsCommandClient` |
44
+
45
+ Wire request param models live in `soothe_client.protocol_params` (not at package root).
43
46
 
44
47
  ## Develop
45
48
 
@@ -0,0 +1,189 @@
1
+ """Soothe WebSocket client for talking to a running soothe-daemon.
2
+
3
+ Public surface (community apps)::
4
+
5
+ from soothe_client import WebSocketClient, is_daemon_live
6
+ from soothe_client.appkit import DaemonSession
7
+ from soothe_client import AsyncCommandClient, CommandClient # preferred aliases
8
+
9
+ Advanced wire validation lives in ``soothe_client.protocol_params``.
10
+ Higher-level session / multi-user helpers live in ``soothe_client.appkit``.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import importlib.metadata
16
+ import warnings
17
+ from typing import Any
18
+
19
+ from soothe_sdk.wire.codec import ProtocolError
20
+
21
+ from soothe_client.errors import (
22
+ DaemonError,
23
+ DisconnectCause,
24
+ ReconnectError,
25
+ StaleLoopError,
26
+ disconnect_cause_name,
27
+ )
28
+ from soothe_client.helpers import (
29
+ check_daemon_status,
30
+ connected_websocket,
31
+ fetch_config_section,
32
+ fetch_loop_cards,
33
+ fetch_loop_history,
34
+ fetch_loop_messages,
35
+ fetch_skills_catalog,
36
+ is_daemon_live,
37
+ protocol1_rpc,
38
+ request_daemon_config_reload,
39
+ request_daemon_shutdown,
40
+ websocket_url_from_config,
41
+ )
42
+ from soothe_client.intent_hints import (
43
+ DEFAULT_DELIVERABLE_PHASES,
44
+ EMBED,
45
+ IMAGE_TO_TEXT,
46
+ OCR,
47
+ TEXT_COMPLETION,
48
+ validate_loop_input_intent_hint,
49
+ )
50
+ from soothe_client.session import (
51
+ bootstrap_loop_session,
52
+ connect_websocket_with_retries,
53
+ )
54
+ from soothe_client.websocket import WebSocketClient
55
+ from soothe_client.ws_command_client import (
56
+ AsyncCommandClient,
57
+ CommandClient,
58
+ SyncWsCommandClient,
59
+ WsCommandClient,
60
+ async_command_client_from_config,
61
+ async_ws_command_client_from_config,
62
+ command_client_from_config,
63
+ ws_command_client_from_config,
64
+ )
65
+
66
+ try:
67
+ __version__ = importlib.metadata.version("soothe-client-python")
68
+ except importlib.metadata.PackageNotFoundError:
69
+ __version__ = "0.0.0"
70
+
71
+ # Legacy ``*Params`` names formerly re-exported at package root.
72
+ _LEGACY_PARAM_NAMES = frozenset(
73
+ {
74
+ "AuthParams",
75
+ "AuthRefreshParams",
76
+ "AutopilotCancelAllParams",
77
+ "AutopilotCancelGoalParams",
78
+ "AutopilotDreamParams",
79
+ "AutopilotGetGoalParams",
80
+ "AutopilotGetJobParams",
81
+ "AutopilotListGoalsParams",
82
+ "AutopilotListJobsParams",
83
+ "AutopilotResumeParams",
84
+ "AutopilotStatusParams",
85
+ "AutopilotSubmitParams",
86
+ "AutopilotSubscribeParams",
87
+ "AutopilotWakeParams",
88
+ "ConfigGetParams",
89
+ "ConfigReloadParams",
90
+ "CronAddParams",
91
+ "CronCancelParams",
92
+ "CronListParams",
93
+ "CronShowParams",
94
+ "DaemonShutdownParams",
95
+ "DaemonStatusParams",
96
+ "DisconnectParams",
97
+ "InvokeSkillParams",
98
+ "JobCancelParams",
99
+ "JobCreateParams",
100
+ "JobDagParams",
101
+ "JobGuidanceParams",
102
+ "JobPauseParams",
103
+ "JobResumeParams",
104
+ "JobStatusParams",
105
+ "LoopCardsFetchParams",
106
+ "LoopDeleteParams",
107
+ "LoopDetachParams",
108
+ "LoopGetParams",
109
+ "LoopInputParams",
110
+ "LoopListParams",
111
+ "LoopMessagesParams",
112
+ "LoopNewParams",
113
+ "LoopPruneParams",
114
+ "LoopReattachParams",
115
+ "LoopStateGetParams",
116
+ "LoopStateUpdateParams",
117
+ "LoopTreeParams",
118
+ "McpStatusParams",
119
+ "ModelsListParams",
120
+ "RpcCommandParams",
121
+ "SkillsListParams",
122
+ "SlashCommandParams",
123
+ "SubscribeParams",
124
+ }
125
+ )
126
+
127
+ __all__ = [
128
+ "__version__",
129
+ # Transport
130
+ "WebSocketClient",
131
+ "ProtocolError",
132
+ # Errors
133
+ "DaemonError",
134
+ "DisconnectCause",
135
+ "ReconnectError",
136
+ "StaleLoopError",
137
+ "disconnect_cause_name",
138
+ # Command clients (canonical + preferred aliases)
139
+ "WsCommandClient",
140
+ "SyncWsCommandClient",
141
+ "AsyncCommandClient",
142
+ "CommandClient",
143
+ "ws_command_client_from_config",
144
+ "async_ws_command_client_from_config",
145
+ "command_client_from_config",
146
+ "async_command_client_from_config",
147
+ # Session bootstrap
148
+ "bootstrap_loop_session",
149
+ "connect_websocket_with_retries",
150
+ # Helpers
151
+ "websocket_url_from_config",
152
+ "connected_websocket",
153
+ "protocol1_rpc",
154
+ "check_daemon_status",
155
+ "is_daemon_live",
156
+ "request_daemon_config_reload",
157
+ "request_daemon_shutdown",
158
+ "fetch_skills_catalog",
159
+ "fetch_config_section",
160
+ "fetch_loop_history",
161
+ "fetch_loop_cards",
162
+ "fetch_loop_messages",
163
+ # Intent hints
164
+ "TEXT_COMPLETION",
165
+ "IMAGE_TO_TEXT",
166
+ "OCR",
167
+ "EMBED",
168
+ "DEFAULT_DELIVERABLE_PHASES",
169
+ "validate_loop_input_intent_hint",
170
+ ]
171
+
172
+
173
+ def __getattr__(name: str) -> Any:
174
+ """Lazy-load legacy ``*Params`` with a deprecation warning.
175
+
176
+ Prefer ``from soothe_client.protocol_params import LoopInputParams``.
177
+ """
178
+ if name in _LEGACY_PARAM_NAMES:
179
+ warnings.warn(
180
+ f"Importing {name!r} from soothe_client is deprecated; "
181
+ f"use soothe_client.protocol_params.{name} instead.",
182
+ DeprecationWarning,
183
+ stacklevel=2,
184
+ )
185
+ from soothe_client import protocol_params as _params
186
+
187
+ return getattr(_params, name)
188
+ msg = f"module {__name__!r} has no attribute {name!r}"
189
+ raise AttributeError(msg)
@@ -1,13 +1,16 @@
1
1
  """Application helpers built on WebSocketClient.
2
2
 
3
- Product-agnostic building blocks for agent UIs and backends:
3
+ Primary building blocks for agent UIs and backends:
4
4
 
5
5
  - ``DaemonSession`` — dual-socket loop session + streamed turns
6
6
  - ``ConnectionPool`` / ``TurnRunner`` — multi-session turn execution
7
7
  - ``QueryGate`` — one-in-flight query per session
8
- - ``EventClassifier`` / ``extract_thinking_step`` — stream → deliverable mapping
8
+ - ``EventClassifier`` — stream → deliverable mapping
9
9
  - ``SSEBroadcaster`` — drop-on-full fan-out to subscribers
10
10
  - ``SessionStore`` — persistence seam (Protocol)
11
+
12
+ Advanced stream/pipeline plumbing lives in submodules
13
+ (``events``, ``chunk_filter``, ``turn``, ``managed_client``).
11
14
  """
12
15
 
13
16
  from __future__ import annotations
@@ -18,7 +21,6 @@ from soothe_client.appkit.attachments import (
18
21
  compact_image_attachment,
19
22
  )
20
23
  from soothe_client.appkit.broadcaster import SSEBroadcaster, SSEEvent
21
- from soothe_client.appkit.chunk_filter import should_drop_stream_chunk_early
22
24
  from soothe_client.appkit.classifier import (
23
25
  EVENT_FINAL_REPORT,
24
26
  ChatEventResult,
@@ -26,17 +28,7 @@ from soothe_client.appkit.classifier import (
26
28
  ClassifierConfig,
27
29
  EventClassifier,
28
30
  )
29
- from soothe_client.appkit.daemon_session import DEFAULT_POST_IDLE_DRAIN_S, DaemonSession
30
- from soothe_client.appkit.events import is_loop_scoped_event, unwrap_next
31
- from soothe_client.appkit.managed_client import (
32
- BootstrapFunc,
33
- ClientFactory,
34
- ManagedClient,
35
- WebSocketManagedClient,
36
- default_bootstrap_func,
37
- default_client_factory,
38
- )
39
- from soothe_client.appkit.observability import TurnEventStats
31
+ from soothe_client.appkit.daemon_session import DaemonSession
40
32
  from soothe_client.appkit.pool import (
41
33
  ConnectionPool,
42
34
  ErrPoolExhausted,
@@ -50,15 +42,6 @@ from soothe_client.appkit.thinking_step import (
50
42
  DEFAULT_THINKING_STEP_EVENTS,
51
43
  extract_thinking_step,
52
44
  )
53
- from soothe_client.appkit.turn import (
54
- PRIORITY_CRITICAL,
55
- PRIORITY_HIGH,
56
- PRIORITY_LOW,
57
- PRIORITY_NORMAL,
58
- TurnApplyBatcher,
59
- TurnEventPipeline,
60
- run_turn_pipeline,
61
- )
62
45
  from soothe_client.appkit.turn_runner import (
63
46
  STREAM_CLOSE_FAIL,
64
47
  STREAM_CLOSE_SOFT_COMPLETE,
@@ -77,13 +60,13 @@ from soothe_client.appkit.turn_runner import (
77
60
  )
78
61
 
79
62
  __all__ = [
80
- "DEFAULT_POST_IDLE_DRAIN_S",
81
63
  "DEFAULT_THINKING_STEP_EVENTS",
82
64
  "STREAM_CLOSE_FAIL",
83
65
  "STREAM_CLOSE_SOFT_COMPLETE",
84
66
  "Attachment",
85
- "BootstrapFunc",
86
- "ClientFactory",
67
+ "ChatEventResult",
68
+ "ChatEventTerminal",
69
+ "ClassifierConfig",
87
70
  "CompactImageOptions",
88
71
  "ConnectionPool",
89
72
  "DaemonSession",
@@ -92,18 +75,10 @@ __all__ = [
92
75
  "ErrPoolExhausted",
93
76
  "ErrQueryBusy",
94
77
  "ErrQueryTimeout",
95
- "ChatEventResult",
96
- "ChatEventTerminal",
97
- "ClassifierConfig",
98
78
  "EventClassifier",
99
79
  "InputOpts",
100
- "ManagedClient",
101
80
  "OnComplete",
102
81
  "OnError",
103
- "PRIORITY_CRITICAL",
104
- "PRIORITY_HIGH",
105
- "PRIORITY_LOW",
106
- "PRIORITY_NORMAL",
107
82
  "PoolConfig",
108
83
  "PooledConn",
109
84
  "QueryGate",
@@ -114,22 +89,12 @@ __all__ = [
114
89
  "SessionStore",
115
90
  "StreamClosePolicy",
116
91
  "TimeoutPolicy",
117
- "TurnApplyBatcher",
118
92
  "TurnConfig",
119
- "TurnEventPipeline",
120
- "TurnEventStats",
121
93
  "TurnRunner",
122
- "WebSocketManagedClient",
123
94
  "compact_attachments",
124
95
  "compact_image_attachment",
125
- "default_bootstrap_func",
126
- "default_client_factory",
127
96
  "default_pool_config",
128
97
  "extract_thinking_step",
129
98
  "idle_timeout_for_turn",
130
99
  "input_message_for_loop",
131
- "is_loop_scoped_event",
132
- "run_turn_pipeline",
133
- "should_drop_stream_chunk_early",
134
- "unwrap_next",
135
100
  ]
@@ -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__)
@@ -37,7 +37,21 @@ StreamDeliveryResolver = Callable[[], str]
37
37
 
38
38
 
39
39
  class DaemonSession:
40
- """Daemon-backed loop session with stream + RPC sockets."""
40
+ """One conversation with a daemon loop: connect, send turns, stream chunks.
41
+
42
+ Owns a subscribed stream WebSocket plus an RPC sidecar so metadata calls
43
+ do not starve loop events. Typical usage::
44
+
45
+ session = DaemonSession("ws://127.0.0.1:8765")
46
+ await session.connect()
47
+ await session.send_turn("Hello")
48
+ async for _ns, mode, data in session.iter_turn_chunks():
49
+ ...
50
+ await session.close()
51
+
52
+ Advanced: ``early_drop_fn`` / ``stats_factory`` customize stream filtering
53
+ and metrics for product UIs; most apps leave the defaults.
54
+ """
41
55
 
42
56
  def __init__(
43
57
  self,
@@ -67,7 +81,7 @@ class DaemonSession:
67
81
  self.last_turn_end_state: str | None = None
68
82
  self.last_turn_cancellation_seen: bool = False
69
83
  self.last_turn_error_message: str | None = None
70
- # IG-659: last completed turn's seq floor (drop stale prior-turn frames).
84
+ # Last completed turn's seq floor (drop stale prior-turn frames).
71
85
  self._last_turn_end_seq: int = 0
72
86
  self._expected_turn_id: str | None = None
73
87
 
@@ -108,7 +122,11 @@ class DaemonSession:
108
122
  )
109
123
 
110
124
  async def connect(self, *, resume_loop_id: str | None = None) -> dict[str, Any]:
111
- """Connect and bootstrap a daemon loop session."""
125
+ """Open the stream socket and bootstrap (or resume) a loop session.
126
+
127
+ Returns:
128
+ Status event from the daemon (includes ``loop_id`` on success).
129
+ """
112
130
  await connect_websocket_with_retries(self._client)
113
131
  return await self._bootstrap_loop(resume_loop_id=resume_loop_id)
114
132
 
@@ -182,7 +200,7 @@ class DaemonSession:
182
200
  await self._bootstrap_loop(resume_loop_id=resume_loop_id)
183
201
 
184
202
  async def close(self, *, handshake_timeout: float = 2.0) -> None:
185
- """Close stream and RPC sockets (idempotent)."""
203
+ """Close stream and RPC sockets. Safe to call more than once."""
186
204
  if self._closed:
187
205
  return
188
206
  self._closed = True
@@ -219,7 +237,11 @@ class DaemonSession:
219
237
  clarification_answers: list[str] | None = None,
220
238
  intent_hint: str | None = None,
221
239
  ) -> None:
222
- """Send a new user turn to the daemon."""
240
+ """Send a user message on the active loop (start of a turn).
241
+
242
+ Call ``iter_turn_chunks`` afterward to consume the streamed reply.
243
+ Requires ``connect()`` (or another bootstrap) first.
244
+ """
223
245
  if not self._loop_id:
224
246
  raise RuntimeError("No active loop session")
225
247
  await self._client.send_input(
@@ -303,13 +325,15 @@ class DaemonSession:
303
325
  *,
304
326
  max_wait_s: float | None = None,
305
327
  ) -> AsyncIterator[tuple[tuple[Any, ...], str, Any]]:
306
- """Yield ``(namespace, mode, data)`` chunks for the active daemon turn.
328
+ """Yield ``(namespace, mode, data)`` stream chunks until the turn ends.
329
+
330
+ Handles idle timeout, post-idle drain, and turn boundary filtering.
331
+ Call after ``send_turn``.
307
332
 
308
333
  Args:
309
334
  max_wait_s: Optional absolute deadline for the whole turn. When set,
310
335
  raises ``TimeoutError`` if the daemon never emits a turn-end
311
- signal in time (idle after payload, stopped, stream.end, or
312
- strange_loop.completed).
336
+ signal in time.
313
337
  """
314
338
  self.turn_event_stats = self._new_turn_stats()
315
339
  self.last_turn_end_state = None
@@ -392,13 +416,19 @@ class DaemonSession:
392
416
  if ev_seq is not None and last_end_seq > 0 and ev_seq <= last_end_seq:
393
417
  continue
394
418
 
395
- # IG-659 phases 1–2: drop mismatched turn_id once bound.
419
+ # Drop mismatched turn_id once bound — except status=running,
420
+ # which may rebind to a newer generation (pre-admit early
421
+ # running can briefly carry a stale or empty turn_id).
396
422
  ev_turn_id = frame_turn_id(event)
423
+ is_running_status = (
424
+ event_type == "status" and str(event.get("state") or "") == "running"
425
+ )
397
426
  if (
398
427
  expected_turn_id
399
428
  and ev_turn_id
400
429
  and ev_turn_id != expected_turn_id
401
430
  and event_type in {"event", "status"}
431
+ and not is_running_status
402
432
  ):
403
433
  logger.debug(
404
434
  "Ignoring mismatched turn_id frame type=%s got=%s expected=%s",
@@ -426,8 +456,18 @@ class DaemonSession:
426
456
  progress_seen = True
427
457
  status_turn = frame_turn_id(event)
428
458
  if status_turn:
429
- expected_turn_id = status_turn
430
- self._expected_turn_id = status_turn
459
+ new_gen = parse_turn_generation(status_turn)
460
+ old_gen = parse_turn_generation(expected_turn_id)
461
+ if expected_turn_id is None or (
462
+ new_gen is not None and (old_gen is None or new_gen >= old_gen)
463
+ ):
464
+ if expected_turn_id and status_turn != expected_turn_id:
465
+ # Successor turn admitted while this reader
466
+ # still held a stale/early binding.
467
+ stream_payload_seen = False
468
+ turn_progress_seen = False
469
+ expected_turn_id = status_turn
470
+ self._expected_turn_id = status_turn
431
471
  elif query_started and state == "stopped":
432
472
  self.last_turn_end_state = state
433
473
  if ev_seq is not None:
@@ -1,4 +1,8 @@
1
- """WebSocket helper functions for daemon communication."""
1
+ """Small WebSocket helpers for CLI and scripts (status, fetch, one-shot RPC).
2
+
3
+ For interactive turns use ``DaemonSession``. For jobs/cron use
4
+ ``WsCommandClient`` / ``CommandClient``.
5
+ """
2
6
 
3
7
  from __future__ import annotations
4
8
 
@@ -32,6 +32,7 @@ __all__ = [
32
32
  "AutopilotListGoalsParams",
33
33
  "AutopilotGetGoalParams",
34
34
  "AutopilotCancelGoalParams",
35
+ "AutopilotCancelAllParams",
35
36
  "AutopilotWakeParams",
36
37
  "AutopilotDreamParams",
37
38
  "AutopilotResumeParams",
@@ -363,6 +364,10 @@ class AutopilotCancelGoalParams(ParamsBase):
363
364
  goal_id: str = Field(..., min_length=1)
364
365
 
365
366
 
367
+ class AutopilotCancelAllParams(EmptyParams):
368
+ """Params for ``method=autopilot_cancel_all`` — no required fields."""
369
+
370
+
366
371
  class AutopilotWakeParams(EmptyParams):
367
372
  """Params for ``method=autopilot_wake`` — no required fields."""
368
373
 
@@ -1,4 +1,8 @@
1
- """Shared WebSocket session bootstrap for CLI headless and TUI."""
1
+ """Session bootstrap helpers for CLI, TUI, and appkit.
2
+
3
+ Prefer ``DaemonSession`` for application code. These helpers are the lower-level
4
+ connect + loop-subscribe steps used when you own the ``WebSocketClient``.
5
+ """
2
6
 
3
7
  from __future__ import annotations
4
8
 
@@ -22,14 +26,7 @@ _LEGACY_LOOP_AUTOPILOT_MODE = "solo"
22
26
 
23
27
 
24
28
  async def connect_websocket_with_retries(client: Any) -> None:
25
- """Connect to the daemon with bounded retries for cold-start races.
26
-
27
- Args:
28
- client: WebSocketClient instance.
29
-
30
- Raises:
31
- ConnectionError: If connection fails after all retries.
32
- """
29
+ """Connect a ``WebSocketClient`` with retries (daemon cold-start safe)."""
33
30
  last_error: OSError | ConnectionError | TimeoutError | None = None
34
31
  for attempt in range(_CONNECT_RETRY_COUNT):
35
32
  try:
@@ -1,7 +1,7 @@
1
- """Shared stream/turn terminal frame helpers for WebSocket and DaemonSession.
1
+ """Internal stream/turn terminal frame helpers.
2
2
 
3
- Keeps peel-at-turn-start and turn-end detection on one vocabulary so leftover
4
- prior-goal terminals cannot blank the next TUI query.
3
+ Used by ``WebSocketClient`` and ``DaemonSession`` so leftover prior-turn
4
+ terminals cannot blank the next query. Not part of the community public API.
5
5
  """
6
6
 
7
7
  from __future__ import annotations
@@ -1,7 +1,8 @@
1
- """Turn / stream boundary helpers (IG-659).
1
+ """Internal turn / stream boundary helpers.
2
2
 
3
- ``turn_id`` correlates a user turn across status, events, and terminals.
4
- ``seq`` is a daemon-assigned monotonic counter per loop for drop-stale filtering.
3
+ Used by ``DaemonSession`` and ``WebSocketClient`` for ``turn_id`` / ``seq``
4
+ filtering. Not part of the community public API import only if you are
5
+ extending stream handling.
5
6
  """
6
7
 
7
8
  from __future__ import annotations
@@ -18,6 +19,19 @@ def format_turn_id(loop_id: str, generation: int) -> str:
18
19
  return f"{lid}:{gen}"
19
20
 
20
21
 
22
+ def parse_turn_generation(turn_id: str | None) -> int | None:
23
+ """Extract generation int from ``turn_id``, or None if malformed."""
24
+ raw = str(turn_id or "").strip()
25
+ if not raw or ":" not in raw:
26
+ return None
27
+ suffix = raw.rsplit(":", 1)[-1]
28
+ try:
29
+ gen = int(suffix)
30
+ except ValueError:
31
+ return None
32
+ return gen if gen > 0 else None
33
+
34
+
21
35
  def frame_turn_id(frame: dict[str, Any] | None) -> str | None:
22
36
  """Return ``turn_id`` from a status/event frame or nested custom data."""
23
37
  if not isinstance(frame, dict):
@@ -51,4 +65,5 @@ __all__ = [
51
65
  "format_turn_id",
52
66
  "frame_seq",
53
67
  "frame_turn_id",
68
+ "parse_turn_generation",
54
69
  ]