muse-code-sdk 1.3.0__py3-none-any.whl
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.
- muse_code/__init__.py +129 -0
- muse_code/connection/__init__.py +49 -0
- muse_code/connection/connection.py +905 -0
- muse_code/connection/discovery.py +55 -0
- muse_code/connection/spawn.py +1067 -0
- muse_code/connection/validation.py +149 -0
- muse_code/errors.py +199 -0
- muse_code/facade/__init__.py +89 -0
- muse_code/facade/approval.py +279 -0
- muse_code/facade/client.py +444 -0
- muse_code/facade/discarded.py +45 -0
- muse_code/facade/gap_fill.py +483 -0
- muse_code/facade/host_death.py +254 -0
- muse_code/facade/session.py +977 -0
- muse_code/facade/turn_handle.py +440 -0
- muse_code/facade/turn_submit.py +289 -0
- muse_code/fold/__init__.py +82 -0
- muse_code/fold/item_store.py +268 -0
- muse_code/fold/session_fold.py +785 -0
- muse_code/fold/state_store.py +141 -0
- muse_code/pending/__init__.py +53 -0
- muse_code/pending/pending_command_set.py +842 -0
- muse_code/py.typed +0 -0
- muse_code/replay.py +144 -0
- muse_code/sync_facade.py +493 -0
- muse_code_sdk-1.3.0.dist-info/METADATA +47 -0
- muse_code_sdk-1.3.0.dist-info/RECORD +30 -0
- muse_code_sdk-1.3.0.dist-info/WHEEL +5 -0
- muse_code_sdk-1.3.0.dist-info/licenses/LICENSE +21 -0
- muse_code_sdk-1.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"""``TurnHandle`` — one turn's view of the fold (spec 638 FR-638-019a,
|
|
2
|
+
INV-638-04 carrying spec 14990 INV-014; tdd SS3.1.4). Port of
|
|
3
|
+
``clients/sdk-ts/src/facade/turn-handle.ts``.
|
|
4
|
+
|
|
5
|
+
A handle owns two things: the SS3.1.4 turn-wait, and the async iterators that
|
|
6
|
+
ride the fold for this turn. It authors no wire traffic and holds no durable
|
|
7
|
+
state — :class:`~muse_code.facade.session.Session` feeds it folded events and
|
|
8
|
+
it fans them out.
|
|
9
|
+
|
|
10
|
+
INV-006 governs the wait: the SDK never locally times out, fails, or completes
|
|
11
|
+
a turn. Every settlement below traces to a server-authored fact, with one
|
|
12
|
+
sanctioned exception that is not a terminal at all — terminal-unknown after an
|
|
13
|
+
ephemeral host death, which SS2.13.3b makes a client MUST ("stop waiting for a
|
|
14
|
+
terminal") and which stays an annotation rather than a synthesized event.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import (
|
|
22
|
+
AsyncIterator,
|
|
23
|
+
Awaitable,
|
|
24
|
+
Callable,
|
|
25
|
+
Generic,
|
|
26
|
+
List,
|
|
27
|
+
Literal,
|
|
28
|
+
Protocol,
|
|
29
|
+
Set,
|
|
30
|
+
TypeVar,
|
|
31
|
+
Union,
|
|
32
|
+
runtime_checkable,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from muse_code_msp import (
|
|
36
|
+
TURN_ERROR_KIND_KNOWN_VALUES,
|
|
37
|
+
TURN_TERMINAL_KNOWN_VALUES,
|
|
38
|
+
Item,
|
|
39
|
+
ItemDeltaParams,
|
|
40
|
+
TurnCompletedParams,
|
|
41
|
+
TurnUnqueuedParams,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
_T = TypeVar("_T")
|
|
45
|
+
|
|
46
|
+
FoldedItem = Item
|
|
47
|
+
"""What the iterators yield: the fold's stored item.
|
|
48
|
+
|
|
49
|
+
The TS twin seals this with ``DeepReadonly<Item>`` so a consumer cannot write
|
|
50
|
+
``item["revision"] = 0`` through the read surface. Python has no structural
|
|
51
|
+
readonly; the fold's own read views already refuse mutation, and the items
|
|
52
|
+
yielded here are the same generated dicts the fold holds (INV-638-02).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
# Read from the generated KNOWN_VALUES so a typo is caught by the codegen
|
|
56
|
+
# contract rather than compiling green against an open ``str`` enum — the
|
|
57
|
+
# runtime twin of the TS closed-vocabulary ``Extract<…>`` bind.
|
|
58
|
+
_TERMINAL_FAILED = "failed"
|
|
59
|
+
_LAUNCH_ERROR = "launchError"
|
|
60
|
+
assert _TERMINAL_FAILED in TURN_TERMINAL_KNOWN_VALUES
|
|
61
|
+
assert _LAUNCH_ERROR in TURN_ERROR_KIND_KNOWN_VALUES
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class _Completed:
|
|
66
|
+
"""The ordinary terminal, AND the launch-failure exit.
|
|
67
|
+
|
|
68
|
+
``deferred_start_failed`` is terminal ``failed``, ``error.kind:
|
|
69
|
+
launchError``, no preceding ``turn/started`` — :func:`is_launch_failure`
|
|
70
|
+
reads that WIRE marker; ``observed_start`` does not discriminate them,
|
|
71
|
+
because a terminal-first fold is legitimate for turns that really ran.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
params: TurnCompletedParams
|
|
75
|
+
observed_start: bool
|
|
76
|
+
kind: Literal["completed"] = "completed"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class _Unqueued:
|
|
81
|
+
"""The reclaim (SS3.6). No ``turn/completed`` will ever carry this
|
|
82
|
+
``turnId``, so a wait folding only ``turn/completed`` hangs forever."""
|
|
83
|
+
|
|
84
|
+
params: TurnUnqueuedParams
|
|
85
|
+
kind: Literal["unqueued"] = "unqueued"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class _TerminalUnknown:
|
|
90
|
+
"""SS2.13.3b: an ephemeral host died, so the client stops waiting.
|
|
91
|
+
|
|
92
|
+
Carries no ``reason``: the arm has exactly one cause, so a single-valued
|
|
93
|
+
field would add nothing ``kind`` does not already say.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
kind: Literal["terminalUnknown"] = "terminalUnknown"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
TurnOutcome = Union[_Completed, _Unqueued, _TerminalUnknown]
|
|
100
|
+
"""How a turn ended, from a waiter's point of view (SS3.1.4's two resolutions,
|
|
101
|
+
plus the ephemeral-death annotation). ``turn/retracted`` and
|
|
102
|
+
``turn/retryScheduled`` are deliberately absent: a retracted turn still
|
|
103
|
+
reaches its own terminal, and a scheduled retry is explicitly non-terminal
|
|
104
|
+
(SS4.5.1)."""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def is_launch_failure(outcome: TurnOutcome) -> bool:
|
|
108
|
+
"""The ``deferred_start_failed`` exit (SS3.1.4/SS3.2).
|
|
109
|
+
|
|
110
|
+
A pre-minted turn whose launch errored at the terminal boundary, so the
|
|
111
|
+
runtime wrote its terminal directly and no ``turn/started`` was ever
|
|
112
|
+
emitted BY THE HOST. Read off the wire, never off local observation: a
|
|
113
|
+
client that had not been listening when the turn started is not looking at
|
|
114
|
+
a launch failure.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
outcome: The settled turn outcome.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
``True`` iff the terminal is ``failed`` with ``error.kind ==
|
|
121
|
+
launchError``.
|
|
122
|
+
"""
|
|
123
|
+
if not isinstance(outcome, _Completed):
|
|
124
|
+
return False
|
|
125
|
+
if outcome.params.get("terminal") != _TERMINAL_FAILED:
|
|
126
|
+
return False
|
|
127
|
+
error = outcome.params.get("error")
|
|
128
|
+
return isinstance(error, dict) and error.get("kind") == _LAUNCH_ERROR
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class _PushStream(Generic[_T]):
|
|
132
|
+
"""A single-consumer push stream (async iterator).
|
|
133
|
+
|
|
134
|
+
Buffers when nobody is awaiting and hands the buffer over before reporting
|
|
135
|
+
an end or a failure, so a consumer never loses events it was already owed.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(self, on_finished: Callable[[], None]) -> None:
|
|
139
|
+
self._buffer: List[_T] = []
|
|
140
|
+
# FIFO, not a single slot: ``items()``/``deltas()`` hand out a plain
|
|
141
|
+
# async iterator, so a consumer prefetching with two concurrent
|
|
142
|
+
# ``__anext__`` calls is reasonable, and a single slot would leave the
|
|
143
|
+
# first waiter never settled. A silently never-settling future is the
|
|
144
|
+
# hang class this file exists to prevent.
|
|
145
|
+
self._waiters: List[asyncio.Future[_T]] = []
|
|
146
|
+
self._on_finished = on_finished
|
|
147
|
+
self._ended = False
|
|
148
|
+
self._failure: BaseException | None = None
|
|
149
|
+
|
|
150
|
+
def push(self, value: _T) -> None:
|
|
151
|
+
"""Delivers one value (to the oldest waiter, else the buffer)."""
|
|
152
|
+
if self._ended:
|
|
153
|
+
return
|
|
154
|
+
while self._waiters:
|
|
155
|
+
waiter = self._waiters.pop(0)
|
|
156
|
+
if not waiter.done():
|
|
157
|
+
waiter.set_result(value)
|
|
158
|
+
return
|
|
159
|
+
self._buffer.append(value)
|
|
160
|
+
|
|
161
|
+
def end(self) -> None:
|
|
162
|
+
"""Reports end-of-stream to every queued waiter, not just the oldest."""
|
|
163
|
+
if self._ended:
|
|
164
|
+
return
|
|
165
|
+
self._ended = True
|
|
166
|
+
for waiter in self._waiters:
|
|
167
|
+
if not waiter.done():
|
|
168
|
+
waiter.set_exception(StopAsyncIteration())
|
|
169
|
+
self._waiters.clear()
|
|
170
|
+
|
|
171
|
+
def fail(self, error: BaseException) -> None:
|
|
172
|
+
"""Reports a failure to every queued waiter."""
|
|
173
|
+
if self._ended:
|
|
174
|
+
return
|
|
175
|
+
self._ended = True
|
|
176
|
+
self._failure = error
|
|
177
|
+
for waiter in self._waiters:
|
|
178
|
+
if not waiter.done():
|
|
179
|
+
waiter.set_exception(error)
|
|
180
|
+
self._waiters.clear()
|
|
181
|
+
|
|
182
|
+
def __aiter__(self) -> AsyncIterator[_T]:
|
|
183
|
+
return self
|
|
184
|
+
|
|
185
|
+
async def __anext__(self) -> _T:
|
|
186
|
+
# Buffered values come out first even after an end or a failure: the
|
|
187
|
+
# consumer was already owed them, and swallowing them here would be
|
|
188
|
+
# the silent hole the iterators exist to prevent.
|
|
189
|
+
if self._buffer:
|
|
190
|
+
return self._buffer.pop(0)
|
|
191
|
+
if self._failure is not None:
|
|
192
|
+
raise self._failure
|
|
193
|
+
if self._ended:
|
|
194
|
+
raise StopAsyncIteration()
|
|
195
|
+
waiter: asyncio.Future[_T] = asyncio.get_running_loop().create_future()
|
|
196
|
+
self._waiters.append(waiter)
|
|
197
|
+
return await waiter
|
|
198
|
+
|
|
199
|
+
async def aclose(self) -> None:
|
|
200
|
+
"""Drop the backlog and deregister this stream.
|
|
201
|
+
|
|
202
|
+
The TS twin deregisters on ``for await ... break`` via the iterator's
|
|
203
|
+
``return()``. Python's ``async for ... break`` does NOT call ``aclose``
|
|
204
|
+
on a manual async iterator, so a consumer that wants deterministic
|
|
205
|
+
deregistration wraps the iterator in ``contextlib.aclosing(...)`` (the
|
|
206
|
+
Python idiom) — this is the method it drives. Without it a dropped
|
|
207
|
+
iterator is reclaimed only when the turn settles (``end``/``fail``
|
|
208
|
+
clears every stream) or on GC; either way the fan-out never yields to a
|
|
209
|
+
stream nobody reads, because ``end`` and ``fail`` clear the sets.
|
|
210
|
+
"""
|
|
211
|
+
self._buffer.clear()
|
|
212
|
+
self.end()
|
|
213
|
+
self._on_finished()
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
ItemReplay = Callable[[], "tuple[FoldedItem, ...]"]
|
|
217
|
+
"""The turn's replayable backlog, supplied by ``Session`` from the fold."""
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class TurnHandle:
|
|
221
|
+
"""One turn's SS7.1 surface, and the Session-fed mutators behind it.
|
|
222
|
+
|
|
223
|
+
``Session.turn()`` returns this typed as :class:`Turn` (the consumer view)
|
|
224
|
+
so the ``settle_*``/``push_*``/``fail`` mutators are not public: an
|
|
225
|
+
embedder calling ``settle_completed`` would fabricate a terminal the
|
|
226
|
+
server never authored, which is exactly what INV-006 forbids. They stay
|
|
227
|
+
reachable only through ``Session.apply``, i.e. only from a server-authored
|
|
228
|
+
event.
|
|
229
|
+
|
|
230
|
+
Attributes:
|
|
231
|
+
turn_id: The wire turn id.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
def __init__(self, turn_id: str, replay_items: ItemReplay) -> None:
|
|
235
|
+
"""Builds the handle; ``replay_items`` reads this turn's folded items."""
|
|
236
|
+
self.turn_id = turn_id
|
|
237
|
+
self._replay_items = replay_items
|
|
238
|
+
self._item_streams: Set[_PushStream[FoldedItem]] = set()
|
|
239
|
+
self._delta_streams: Set[_PushStream[ItemDeltaParams]] = set()
|
|
240
|
+
self._observed_start = False
|
|
241
|
+
self._settled = False
|
|
242
|
+
self._outcome: TurnOutcome | None = None
|
|
243
|
+
# Retained so an iterator opened AFTER the failure reports it too:
|
|
244
|
+
# without it a late ``items()`` would end cleanly, handing a partial
|
|
245
|
+
# replay plus a tidy done — an unfinished turn rendered as finished,
|
|
246
|
+
# the invented completion INV-006 forbids.
|
|
247
|
+
self._failure: BaseException | None = None
|
|
248
|
+
self._waiters: List[asyncio.Future[TurnOutcome]] = []
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def completed(self) -> Awaitable[TurnOutcome]:
|
|
252
|
+
"""Settles on ``turn/completed``, ``turn/unqueued``, or the ephemeral
|
|
253
|
+
discard.
|
|
254
|
+
|
|
255
|
+
Each access returns a fresh awaitable resolved from the handle's
|
|
256
|
+
settled state (or a pending one that the next settlement resolves), so
|
|
257
|
+
any number of callers may await it and a late awaiter still learns the
|
|
258
|
+
outcome. It never settles on a local timeout (INV-006).
|
|
259
|
+
"""
|
|
260
|
+
future: asyncio.Future[TurnOutcome] = asyncio.get_running_loop().create_future()
|
|
261
|
+
if self._failure is not None:
|
|
262
|
+
future.set_exception(self._failure)
|
|
263
|
+
elif self._settled and self._outcome is not None:
|
|
264
|
+
future.set_result(self._outcome)
|
|
265
|
+
else:
|
|
266
|
+
self._waiters.append(future)
|
|
267
|
+
return future
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def live_stream_count(self) -> int:
|
|
271
|
+
"""How many iterators this handle is still fanning out to.
|
|
272
|
+
|
|
273
|
+
Exposed so the deregistration contract is assertable: an iterator a
|
|
274
|
+
consumer broke out of, or dropped, must leave this set rather than
|
|
275
|
+
accumulate for the rest of the turn.
|
|
276
|
+
"""
|
|
277
|
+
return len(self._item_streams) + len(self._delta_streams)
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def observed_start(self) -> bool:
|
|
281
|
+
"""Did a ``turn/started`` ever fold for this turn?
|
|
282
|
+
|
|
283
|
+
A session-local observation, NOT a launch-failure marker — the fold
|
|
284
|
+
accepts a terminal-first ``turn/completed`` for turns that really ran
|
|
285
|
+
(single-shot turns, gap fills, mid-stream attach). Branch on
|
|
286
|
+
:func:`is_launch_failure` for the launch failure.
|
|
287
|
+
"""
|
|
288
|
+
return self._observed_start
|
|
289
|
+
|
|
290
|
+
def items(self) -> AsyncIterator[FoldedItem]:
|
|
291
|
+
"""This turn's items: everything the fold already holds, at its
|
|
292
|
+
current revision and in first-opened order, then the live tail — one
|
|
293
|
+
yield per fold-observed revision CHANGE. A stale re-emission mutates
|
|
294
|
+
nothing (INV-003) and yields nothing.
|
|
295
|
+
|
|
296
|
+
The iterator ends when the turn settles; it never ends on a local
|
|
297
|
+
timeout.
|
|
298
|
+
"""
|
|
299
|
+
stream: _PushStream[FoldedItem] = _PushStream(
|
|
300
|
+
lambda: self._item_streams.discard(stream)
|
|
301
|
+
)
|
|
302
|
+
for item in self._replay_items():
|
|
303
|
+
stream.push(item)
|
|
304
|
+
self._admit(stream, self._item_streams)
|
|
305
|
+
return stream
|
|
306
|
+
|
|
307
|
+
def deltas(self) -> AsyncIterator[ItemDeltaParams]:
|
|
308
|
+
"""This turn's ``item/delta`` frames, live only.
|
|
309
|
+
|
|
310
|
+
Deliberately NOT replayed, and the asymmetry with :meth:`items` is a
|
|
311
|
+
fact about the fold: the store holds each item at its latest revision
|
|
312
|
+
plus the ACCUMULATED delta text per field path, never the delta event
|
|
313
|
+
sequence. A consumer attaching mid-turn reads the accumulated value
|
|
314
|
+
from the fold; there is no retained event list to replay, and
|
|
315
|
+
inventing one would be a second copy of state INV-002 must defend.
|
|
316
|
+
"""
|
|
317
|
+
stream: _PushStream[ItemDeltaParams] = _PushStream(
|
|
318
|
+
lambda: self._delta_streams.discard(stream)
|
|
319
|
+
)
|
|
320
|
+
self._admit(stream, self._delta_streams)
|
|
321
|
+
return stream
|
|
322
|
+
|
|
323
|
+
def _admit(
|
|
324
|
+
self, stream: _PushStream[_T], into: Set[_PushStream[_T]]
|
|
325
|
+
) -> None:
|
|
326
|
+
# The failure branch comes FIRST: a turn that failed is not a turn that
|
|
327
|
+
# ended. ``_PushStream`` drains the replay before reporting either, so
|
|
328
|
+
# a late consumer still receives everything it was owed.
|
|
329
|
+
if self._failure is not None:
|
|
330
|
+
stream.fail(self._failure)
|
|
331
|
+
elif self._settled:
|
|
332
|
+
stream.end()
|
|
333
|
+
else:
|
|
334
|
+
into.add(stream)
|
|
335
|
+
|
|
336
|
+
# ---- fed by Session -----------------------------------------------------
|
|
337
|
+
|
|
338
|
+
def mark_started(self) -> None:
|
|
339
|
+
"""A ``turn/started`` folded for this turn."""
|
|
340
|
+
self._observed_start = True
|
|
341
|
+
|
|
342
|
+
def push_item(self, item: FoldedItem) -> None:
|
|
343
|
+
"""Fan one item out to every live ``items()`` iterator."""
|
|
344
|
+
for stream in self._item_streams:
|
|
345
|
+
stream.push(item)
|
|
346
|
+
|
|
347
|
+
def push_delta(self, params: ItemDeltaParams) -> None:
|
|
348
|
+
"""Fan one delta out to every live ``deltas()`` iterator."""
|
|
349
|
+
for stream in self._delta_streams:
|
|
350
|
+
stream.push(params)
|
|
351
|
+
|
|
352
|
+
def settle_completed(self, params: TurnCompletedParams) -> None:
|
|
353
|
+
"""Settle on a ``turn/completed`` terminal."""
|
|
354
|
+
self._finish(_Completed(params=params, observed_start=self._observed_start))
|
|
355
|
+
|
|
356
|
+
def settle_unqueued(self, params: TurnUnqueuedParams) -> None:
|
|
357
|
+
"""Settle on a ``turn/unqueued`` reclaim."""
|
|
358
|
+
self._finish(_Unqueued(params=params))
|
|
359
|
+
|
|
360
|
+
def settle_terminal_unknown(self) -> None:
|
|
361
|
+
"""SS2.13.3b: an ephemeral host died; stop waiting for a terminal."""
|
|
362
|
+
self._finish(_TerminalUnknown())
|
|
363
|
+
|
|
364
|
+
def fail(self, error: BaseException) -> None:
|
|
365
|
+
"""A DURABLE host died abnormally.
|
|
366
|
+
|
|
367
|
+
The waiter gets no answer here — FM-001 puts the terminals on resume —
|
|
368
|
+
so it is rejected rather than resolved: a resolution would be the
|
|
369
|
+
invented terminal INV-006 forbids, and silence would be the SS3.1.4
|
|
370
|
+
hang.
|
|
371
|
+
"""
|
|
372
|
+
if self._settled:
|
|
373
|
+
return
|
|
374
|
+
self._settled = True
|
|
375
|
+
self._failure = error
|
|
376
|
+
for waiter in self._waiters:
|
|
377
|
+
if not waiter.done():
|
|
378
|
+
waiter.set_exception(error)
|
|
379
|
+
self._waiters.clear()
|
|
380
|
+
for item_stream in list(self._item_streams):
|
|
381
|
+
item_stream.fail(error)
|
|
382
|
+
for delta_stream in list(self._delta_streams):
|
|
383
|
+
delta_stream.fail(error)
|
|
384
|
+
self._item_streams.clear()
|
|
385
|
+
self._delta_streams.clear()
|
|
386
|
+
|
|
387
|
+
def _finish(self, outcome: TurnOutcome) -> None:
|
|
388
|
+
# First settlement wins: a duplicate terminal is a losing racer's echo.
|
|
389
|
+
if self._settled:
|
|
390
|
+
return
|
|
391
|
+
self._settled = True
|
|
392
|
+
self._outcome = outcome
|
|
393
|
+
for waiter in self._waiters:
|
|
394
|
+
if not waiter.done():
|
|
395
|
+
waiter.set_result(outcome)
|
|
396
|
+
self._waiters.clear()
|
|
397
|
+
for item_stream in list(self._item_streams):
|
|
398
|
+
item_stream.end()
|
|
399
|
+
for delta_stream in list(self._delta_streams):
|
|
400
|
+
delta_stream.end()
|
|
401
|
+
self._item_streams.clear()
|
|
402
|
+
self._delta_streams.clear()
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@runtime_checkable
|
|
406
|
+
class Turn(Protocol):
|
|
407
|
+
"""A turn, as a CONSUMER sees it — the SS7.1 turn surface and nothing else.
|
|
408
|
+
|
|
409
|
+
``Session.turn()`` and ``send_user_turn()`` advertise THIS rather than the
|
|
410
|
+
concrete :class:`TurnHandle` so the "fed by Session" mutators
|
|
411
|
+
(``settle_completed``, ``push_item``, ``fail``, …) are not part of the
|
|
412
|
+
consumer contract: an embedder calling one would fabricate a terminal the
|
|
413
|
+
server never authored (INV-006). Python cannot hide them at runtime the
|
|
414
|
+
way the TS ``Turn`` interface does, so the seal is the return annotation
|
|
415
|
+
plus this documented surface — the same convention the fold's read views
|
|
416
|
+
use.
|
|
417
|
+
"""
|
|
418
|
+
|
|
419
|
+
@property
|
|
420
|
+
def turn_id(self) -> str:
|
|
421
|
+
"""The wire turn id."""
|
|
422
|
+
...
|
|
423
|
+
|
|
424
|
+
@property
|
|
425
|
+
def observed_start(self) -> bool:
|
|
426
|
+
"""Did THIS session observe a ``turn/started``? Not a launch marker."""
|
|
427
|
+
...
|
|
428
|
+
|
|
429
|
+
@property
|
|
430
|
+
def completed(self) -> Awaitable[TurnOutcome]:
|
|
431
|
+
"""The SS3.1.4 turn-wait; settles on a server-authored fact."""
|
|
432
|
+
...
|
|
433
|
+
|
|
434
|
+
def items(self) -> AsyncIterator[FoldedItem]:
|
|
435
|
+
"""This turn's items: folded backlog, then the live tail."""
|
|
436
|
+
...
|
|
437
|
+
|
|
438
|
+
def deltas(self) -> AsyncIterator[ItemDeltaParams]:
|
|
439
|
+
"""This turn's ``item/delta`` frames, live only."""
|
|
440
|
+
...
|