muse-code-sdk 1.3.0__tar.gz → 1.3.1__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 (64) hide show
  1. muse_code_sdk-1.3.1/PKG-INFO +86 -0
  2. muse_code_sdk-1.3.1/README.md +67 -0
  3. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/pyproject.toml +8 -8
  4. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/__init__.py +16 -17
  5. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/connection/__init__.py +2 -2
  6. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/connection/connection.py +42 -48
  7. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/connection/discovery.py +13 -13
  8. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/connection/spawn.py +75 -86
  9. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/connection/validation.py +17 -19
  10. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/errors.py +18 -21
  11. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/facade/__init__.py +2 -3
  12. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/facade/approval.py +22 -23
  13. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/facade/client.py +27 -32
  14. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/facade/discarded.py +3 -4
  15. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/facade/gap_fill.py +20 -22
  16. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/facade/host_death.py +16 -17
  17. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/facade/session.py +54 -58
  18. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/facade/turn_handle.py +24 -26
  19. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/facade/turn_submit.py +19 -22
  20. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/fold/__init__.py +2 -3
  21. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/fold/item_store.py +17 -19
  22. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/fold/session_fold.py +45 -41
  23. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/fold/state_store.py +8 -9
  24. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/pending/__init__.py +1 -1
  25. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/pending/pending_command_set.py +57 -62
  26. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/replay.py +10 -10
  27. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/sync_facade.py +31 -36
  28. muse_code_sdk-1.3.1/src/muse_code_sdk.egg-info/PKG-INFO +86 -0
  29. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code_sdk.egg-info/SOURCES.txt +1 -0
  30. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_asyncio_cancellation.py +5 -6
  31. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_compatibility_row.py +47 -20
  32. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_connection.py +25 -28
  33. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_connection_error_arms.py +22 -23
  34. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_docs_generation.py +9 -10
  35. muse_code_sdk-1.3.1/tests/test_external_audience.py +676 -0
  36. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_facade_approval.py +20 -22
  37. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_facade_gap_fill.py +24 -26
  38. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_facade_persistent_approval.py +3 -3
  39. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_facade_turn.py +27 -29
  40. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_host_discovery.py +27 -28
  41. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_item_store.py +1 -2
  42. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_manifests.py +10 -11
  43. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_muse_serve_child.py +47 -48
  44. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_pending_command_corpus.py +3 -4
  45. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_pending_command_set.py +15 -15
  46. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_pending_command_wire_binding.py +5 -5
  47. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_publish_shape.py +34 -28
  48. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_session_fold.py +24 -26
  49. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_source_closure.py +7 -7
  50. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_spawn_handshake.py +9 -10
  51. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_state_store.py +5 -6
  52. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_sync_facade.py +28 -28
  53. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_tolerance_corpus.py +5 -6
  54. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_transcript_replay.py +14 -16
  55. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_typing_gates.py +7 -7
  56. muse_code_sdk-1.3.0/PKG-INFO +0 -47
  57. muse_code_sdk-1.3.0/README.md +0 -28
  58. muse_code_sdk-1.3.0/src/muse_code_sdk.egg-info/PKG-INFO +0 -47
  59. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/LICENSE +0 -0
  60. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/setup.cfg +0 -0
  61. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/py.typed +0 -0
  62. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code_sdk.egg-info/dependency_links.txt +0 -0
  63. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code_sdk.egg-info/requires.txt +0 -0
  64. {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code_sdk.egg-info/top_level.txt +0 -0
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: muse-code-sdk
3
+ Version: 1.3.1
4
+ Summary: Muse Code SDK for MSP: asyncio-first facade over the generated wire types (muse-code-msp), with a thin sync wrapper.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/meta-models/muse-code-sdk
7
+ Project-URL: Repository, https://github.com/meta-models/muse-code-sdk
8
+ Project-URL: Issues, https://github.com/meta-models/muse-code-sdk/issues
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: pydantic==2.13.5
18
+ Dynamic: license-file
19
+
20
+ # `muse-code-sdk`
21
+
22
+ The Python SDK for Muse Code: an asyncio-first client for driving a Muse
23
+ Code agent over the Muse Session Protocol (MSP), with a thin synchronous
24
+ wrapper for scripts and notebooks.
25
+
26
+ - Open and resume sessions against a spawned `muse serve` host, or attach to
27
+ one you already run.
28
+ - Stream a turn's items and text deltas as async iterators; await the turn's
29
+ server-authored outcome.
30
+ - Answer tool-approval requests with a handler you register.
31
+ - A client-side fold keeps a live, typed view of the session — items, session
32
+ state, pending commands — with gap recovery after reconnects, and safe
33
+ same-command retry that can never double-submit.
34
+ - `SyncMuseClient` mirrors the same surface with blocking verbs.
35
+
36
+ Status: developer preview. Experimental protocol surfaces may change.
37
+
38
+ The generated MSP wire types live in the companion package
39
+ [`muse-code-msp`](https://pypi.org/project/muse-code-msp/) and are never
40
+ restated here. One runtime dependency: `pydantic`, exact-pinned.
41
+ Python 3.10+. Fully typed (`py.typed`).
42
+
43
+ ## Install
44
+
45
+ ```sh
46
+ pip install muse-code-sdk muse-code-msp
47
+ ```
48
+
49
+ You also need the Muse Code CLI: a `muse` binary on `PATH`, or an explicit
50
+ path passed at spawn time.
51
+
52
+ ## Use
53
+
54
+ ```python
55
+ import asyncio
56
+
57
+ from muse_code import (
58
+ MuseClient, MuseClientSpawnOptions, SendUserTurnOptions, StartSessionOptions,
59
+ )
60
+
61
+ async def main() -> None:
62
+ client = await MuseClient.spawn(MuseClientSpawnOptions(
63
+ muse_bin="muse",
64
+ args=["serve"],
65
+ client_info={"name": "my-app", "version": "1.0.0"},
66
+ ))
67
+ session = await client.start_session(StartSessionOptions(workspace_root="."))
68
+ turn = await session.send_user_turn(SendUserTurnOptions(
69
+ input=[{"type": "text", "text": "Summarize this repository."}],
70
+ composer_input="Summarize this repository.",
71
+ ))
72
+ async for item in turn.items():
73
+ print(item)
74
+ await turn.completed
75
+ await client.close()
76
+
77
+ asyncio.run(main())
78
+ ```
79
+
80
+ ## Documentation
81
+
82
+ - Guides, cookbook recipes, and the protocol reference:
83
+ <https://meta-models.github.io/muse-code-sdk/>
84
+ - Python API reference:
85
+ <https://meta-models.github.io/muse-code-sdk/next/generated/python/>
86
+ - Source and issues: <https://github.com/meta-models/muse-code-sdk>
@@ -0,0 +1,67 @@
1
+ # `muse-code-sdk`
2
+
3
+ The Python SDK for Muse Code: an asyncio-first client for driving a Muse
4
+ Code agent over the Muse Session Protocol (MSP), with a thin synchronous
5
+ wrapper for scripts and notebooks.
6
+
7
+ - Open and resume sessions against a spawned `muse serve` host, or attach to
8
+ one you already run.
9
+ - Stream a turn's items and text deltas as async iterators; await the turn's
10
+ server-authored outcome.
11
+ - Answer tool-approval requests with a handler you register.
12
+ - A client-side fold keeps a live, typed view of the session — items, session
13
+ state, pending commands — with gap recovery after reconnects, and safe
14
+ same-command retry that can never double-submit.
15
+ - `SyncMuseClient` mirrors the same surface with blocking verbs.
16
+
17
+ Status: developer preview. Experimental protocol surfaces may change.
18
+
19
+ The generated MSP wire types live in the companion package
20
+ [`muse-code-msp`](https://pypi.org/project/muse-code-msp/) and are never
21
+ restated here. One runtime dependency: `pydantic`, exact-pinned.
22
+ Python 3.10+. Fully typed (`py.typed`).
23
+
24
+ ## Install
25
+
26
+ ```sh
27
+ pip install muse-code-sdk muse-code-msp
28
+ ```
29
+
30
+ You also need the Muse Code CLI: a `muse` binary on `PATH`, or an explicit
31
+ path passed at spawn time.
32
+
33
+ ## Use
34
+
35
+ ```python
36
+ import asyncio
37
+
38
+ from muse_code import (
39
+ MuseClient, MuseClientSpawnOptions, SendUserTurnOptions, StartSessionOptions,
40
+ )
41
+
42
+ async def main() -> None:
43
+ client = await MuseClient.spawn(MuseClientSpawnOptions(
44
+ muse_bin="muse",
45
+ args=["serve"],
46
+ client_info={"name": "my-app", "version": "1.0.0"},
47
+ ))
48
+ session = await client.start_session(StartSessionOptions(workspace_root="."))
49
+ turn = await session.send_user_turn(SendUserTurnOptions(
50
+ input=[{"type": "text", "text": "Summarize this repository."}],
51
+ composer_input="Summarize this repository.",
52
+ ))
53
+ async for item in turn.items():
54
+ print(item)
55
+ await turn.completed
56
+ await client.close()
57
+
58
+ asyncio.run(main())
59
+ ```
60
+
61
+ ## Documentation
62
+
63
+ - Guides, cookbook recipes, and the protocol reference:
64
+ <https://meta-models.github.io/muse-code-sdk/>
65
+ - Python API reference:
66
+ <https://meta-models.github.io/muse-code-sdk/next/generated/python/>
67
+ - Source and issues: <https://github.com/meta-models/muse-code-sdk>
@@ -5,23 +5,23 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "muse-code-sdk"
8
- version = "1.3.0"
8
+ version = "1.3.1"
9
9
  description = "Muse Code SDK for MSP: asyncio-first facade over the generated wire types (muse-code-msp), with a thin sync wrapper."
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"
12
12
  license = "MIT"
13
13
  license-files = ["LICENSE"]
14
- # Exactly ONE runtime dependency, exact-pinned: the ADR 638 D2 supply-chain
15
- # exception (#211 O-3 form). Any second dependency is an owner escalation;
16
- # bumps are deliberate reviewed PRs (see the ADR's review policy).
14
+ # Exactly ONE runtime dependency, exact-pinned: a deliberate, ruled
15
+ # supply-chain exception. Any second dependency is an owner escalation;
16
+ # bumps are deliberate reviewed PRs.
17
17
  # muse-code-msp is deliberately NOT declared: it is not on the registry yet,
18
18
  # and promoting it is the owner escalation decided with the first publish
19
19
  # (the @muse-code/msp devDependency precedent; test_publish_shape pins this).
20
20
  dependencies = ["pydantic==2.13.5"]
21
21
  classifiers = [
22
- # Alpha states the withheld promise: D-065 (ADR 29534 D1) permits the
23
- # publish, but D-013's stability promise and conformance claim stay
24
- # withheld — experimental surfaces may change (ADR 25304 D3).
22
+ # Alpha states the withheld promise: publication is permitted, but the
23
+ # stability promise and conformance claim stay withheld — experimental
24
+ # surfaces may change.
25
25
  "Development Status :: 3 - Alpha",
26
26
  "Intended Audience :: Developers",
27
27
  "Operating System :: OS Independent",
@@ -30,7 +30,7 @@ classifiers = [
30
30
  ]
31
31
 
32
32
  [project.urls]
33
- # The mirror is the one public venue (ADR 638 D6); a published manifest never
33
+ # The mirror is the one public venue; a published manifest never
34
34
  # names the private producer repository.
35
35
  Homepage = "https://github.com/meta-models/muse-code-sdk"
36
36
  Repository = "https://github.com/meta-models/muse-code-sdk"
@@ -1,18 +1,18 @@
1
1
  """Muse Code SDK for MSP: the asyncio-first Python facade.
2
2
 
3
- Owning spec: ``specs/638-muse-sdk-python`` (governing ADR
4
- ``docs/adr/638-muse-sdk-python.md``). Wire shapes are imported from the
5
- generated ``muse_code_msp`` package, never restated here (INV-638-01/02).
3
+ Wire shapes are imported from the generated ``muse_code_msp`` package,
4
+ never restated here.
6
5
 
7
- S1 (the transport-less core) is in: the SS4 fold (``muse_code.fold``), the
8
- SS4.13 pending set (``muse_code.pending``), the transcript replay runner
6
+ The transport-less core: the client fold (``muse_code.fold``), the pending
7
+ command set (``muse_code.pending``), the transcript replay runner
9
8
  (``muse_code.replay``), and the typed error family (``muse_code.errors``).
10
- S2 is in: the NDJSON connection machine, the owned-process spawn path, host
11
- discovery, and the strict fingerprint gate (``muse_code.connection``). S3 is in:
12
- the asyncio facade (``muse_code.facade`` — ``MuseClient``, ``Session``,
13
- ``Turn``) with the host-death discharge, the approval/user-input round trip,
14
- the SS4.8 gap splice-fill, and the sync loop-runner wrapper
15
- (``muse_code.sync_facade`` — ``SyncMuseClient``).
9
+ The connection plane: the NDJSON connection machine, the owned-process spawn
10
+ path, host discovery, and the strict fingerprint gate
11
+ (``muse_code.connection``). The surface: the asyncio facade
12
+ (``muse_code.facade`` — ``MuseClient``, ``Session``, ``Turn``) with the
13
+ host-death discharge, the approval/user-input round trip, the gap
14
+ splice-fill, and the sync loop-runner wrapper (``muse_code.sync_facade`` —
15
+ ``SyncMuseClient``).
16
16
  """
17
17
 
18
18
  from __future__ import annotations
@@ -77,20 +77,19 @@ __all__ = [
77
77
  ]
78
78
 
79
79
  EXPECTED_SCHEMA_FINGERPRINT: Final[str] = (
80
- "sha256:ab69549a7ebb423fce94068762da0b5ff3cdec1f8fc263dcc17248eda117f852"
80
+ "sha256:b1e6676d624e116e2c1b150fec3192200d2cbca8ed79898e44f8921759c7872f"
81
81
  )
82
82
  """The stable schema-bundle fingerprint this SDK was built against.
83
83
 
84
- The one deliberately duplicated fact (spec 638 FR-638-006, the 14990 FR-006
85
- pattern): ``clients/sdk-py/tests/test_manifests.py`` binds it to
84
+ The one deliberately duplicated fact: ``clients/sdk-py/tests/test_manifests.py`` binds it to
86
85
  ``schema/msp/stable/manifest.json``, so a schema advance that forgets the
87
86
  Python SDK reds this lane instead of drifting silently.
88
87
  """
89
88
 
90
- # D-063 lockstep (ADR 25304 D4): the release train bumps pyproject.toml, the
89
+ # The lockstep rule: the release train bumps pyproject.toml, the
91
90
  # distribution-metadata authority; this constant is aligned at publication
92
- # time (the publish itself is owner-run, permitted by D-065, ADR 29534 D1).
93
- __version__: Final[str] = "1.3.0"
91
+ # time.
92
+ __version__: Final[str] = "1.3.1"
94
93
 
95
94
  # The facade is imported last so the constants above are already bound: the
96
95
  # spawn path reads ``EXPECTED_SCHEMA_FINGERPRINT`` (lazily, at initialize
@@ -1,5 +1,5 @@
1
- """The MSP connection plane (spec 638 FR-638-011..018): the transport-less
2
- NDJSON connection machine, the owned-process spawn path, and host discovery.
1
+ """The MSP connection plane: the transport-less NDJSON connection machine,
2
+ the owned-process spawn path, and host discovery.
3
3
  """
4
4
 
5
5
  from __future__ import annotations
@@ -1,11 +1,10 @@
1
- """Duplex-agnostic MSP NDJSON connection (spec 638 FR-638-011..015).
1
+ """Duplex-agnostic MSP NDJSON connection.
2
2
 
3
- Faithful asyncio port of ``clients/sdk-ts/src/connection/connection.ts``
4
- (spec 14990 FR-012/014/015/016). The transport supplies decoded UTF-8 text
5
- chunks; chunks may be empty and may split anywhere — Python strings are
6
- whole code points, so the TS surrogate-pair byte reconciliation has no twin
7
- here (spec 638 Edge Cases): the running byte count is exact by construction
8
- and stays O(bytes) per chunk.
3
+ Faithful asyncio port of the TypeScript SDK's connection module. The
4
+ transport supplies decoded UTF-8 text chunks; chunks may be empty and may
5
+ split anywhere — Python strings are whole code points, so the TS
6
+ surrogate-pair byte reconciliation has no twin here: the running byte count
7
+ is exact by construction and stays O(bytes) per chunk.
9
8
 
10
9
  One deliberate adaptation, noted per the port rules: in TS "invoking
11
10
  ``write()`` IS submission" because the transport's synchronous prefix runs
@@ -74,7 +73,7 @@ RetryDelay = Callable[[int, "MspError"], Awaitable[None]]
74
73
 
75
74
 
76
75
  class MspError(Exception):
77
- """The one typed error family consumers branch on (INV-012).
76
+ """The one typed error family consumers branch on.
78
77
 
79
78
  Attributes:
80
79
  code: The JSON-RPC error code.
@@ -104,7 +103,7 @@ class ProtocolError(Exception):
104
103
 
105
104
  Attributes:
106
105
  line: The offending inbound line, preserved as evidence and bounded
107
- in UTF-8 bytes (FM-007), when one exists.
106
+ in UTF-8 bytes, when one exists.
108
107
  """
109
108
 
110
109
  def __init__(self, message: str, line: str | None = None) -> None:
@@ -122,7 +121,7 @@ class _UnencodableFrameError(ProtocolError):
122
121
 
123
122
  A caller defect, never transport death: it rejects only that frame's
124
123
  outcome, so ``notify()``'s finish-on-write-failure rule must not treat
125
- it as the pipe dying (PR #30094 review).
124
+ it as the pipe dying.
126
125
  """
127
126
 
128
127
 
@@ -147,12 +146,11 @@ def _utf8_len(text: str) -> int:
147
146
 
148
147
 
149
148
  def _truncate_to_utf8_bytes(text: str, limit_bytes: int) -> str:
150
- """FM-007: error evidence is bounded in UTF-8 BYTES, never characters.
149
+ """The evidence bound: error evidence is measured in UTF-8 BYTES, never characters.
151
150
 
152
151
  Encode once and back the cut up over continuation bytes (the
153
152
  ``StderrTail._trim_bytes`` pattern): a per-scalar walk re-encoded every
154
- character and blocked the event loop for seconds at the frame limit
155
- (PR #30094 review).
153
+ character and blocked the event loop for seconds at the frame limit.
156
154
  """
157
155
  data = text.encode("utf-8", "surrogatepass")
158
156
  if len(data) <= limit_bytes:
@@ -179,7 +177,7 @@ def _canonical(value: Any) -> str:
179
177
 
180
178
 
181
179
  def create_uuid_v7_mint() -> Callable[[], str]:
182
- """Creates the SDK-owned UUIDv7 command-id source (INV-013).
180
+ """Creates the SDK-owned UUIDv7 command-id source.
183
181
 
184
182
  Returns:
185
183
  A zero-argument callable minting RFC 9562 v7-shaped ids, strictly
@@ -216,7 +214,7 @@ def create_uuid_v7_mint() -> Callable[[], str]:
216
214
 
217
215
 
218
216
  async def _default_retry_delay(attempt: int, _error: MspError) -> None:
219
- """FM-006's full-jitter budget: uniform in 0..ceiling, 50 ms doubling, 2 s cap."""
217
+ """The retry backoff's full-jitter budget: uniform in 0..ceiling, 50 ms doubling, 2 s cap."""
220
218
  ceiling_ms = min(2_000, 50 * 2 ** max(0, attempt - 1))
221
219
  await asyncio.sleep(secrets.randbelow(ceiling_ms + 1) / 1000)
222
220
 
@@ -272,9 +270,8 @@ class Connection:
272
270
 
273
271
  Args:
274
272
  transport: The duplex text channel.
275
- frame_limit_bytes: Inbound frame-size sanity bound (SS1.1
276
- default when omitted).
277
- mint_command_id: Injectable command-id source (INV-013);
273
+ frame_limit_bytes: Inbound frame-size sanity bound.
274
+ mint_command_id: Injectable command-id source;
278
275
  defaults to the UUIDv7 mint.
279
276
  mint_request_id: Injectable for deterministic transcript
280
277
  clients; values must be unique in flight.
@@ -313,15 +310,14 @@ class Connection:
313
310
 
314
311
  A fresh shield per access: a consumer's ``wait_for`` timeout must
315
312
  cancel only its own wait, never the connection's one real future —
316
- a cancelled ``_closed`` would break ``close()`` for everyone
317
- (PR #30094 review; a TS Promise cannot be cancelled).
313
+ a cancelled ``_closed`` would break ``close()`` for everyone.
318
314
  """
319
315
  return asyncio.shield(self._closed)
320
316
 
321
317
  def _submission_tail(self) -> asyncio.Future[None]:
322
318
  """The friend seam a process-owning transport adopts as ``flushed``.
323
319
 
324
- Module-private, like the TS twin's Symbol fence (PR #30094 review):
320
+ Module-private, like the TS twin's Symbol fence:
325
321
  it lets a close routed around this class — the public
326
322
  ``child.close()`` — wait for accepted frames' SUBMISSION the same
327
323
  way :meth:`close` does.
@@ -332,8 +328,8 @@ class Connection:
332
328
  """Takes one id from THIS connection's single command-id minter.
333
329
 
334
330
  Exposed for callers that need the id BEFORE the ack: the facade's
335
- ``send_user_turn`` records its optimistic SS4.13 entry under it. A
336
- second facade-side minter is exactly what INV-013 forbids. Pair with
331
+ ``send_user_turn`` records its optimistic pending entry under it. A
332
+ second facade-side minter is exactly what the single-minter rule forbids. Pair with
337
333
  ``command(..., command_id=...)``, which reuses the id.
338
334
 
339
335
  Returns:
@@ -344,7 +340,7 @@ class Connection:
344
340
  def request(
345
341
  self, method: str, params: Params | None = None
346
342
  ) -> asyncio.Future[Params]:
347
- """Sends one request and correlates its response (SS1.3).
343
+ """Sends one request and correlates its response.
348
344
 
349
345
  Args:
350
346
  method: The MSP method name.
@@ -352,7 +348,7 @@ class Connection:
352
348
 
353
349
  Returns:
354
350
  A future resolving with the result object, or rejecting with an
355
- :class:`MspError` (typed, INV-012) or :class:`ProtocolError`.
351
+ :class:`MspError` or :class:`ProtocolError`.
356
352
  """
357
353
  outcome: asyncio.Future[Params] = self._loop.create_future()
358
354
  if self._finished:
@@ -393,7 +389,7 @@ class Connection:
393
389
  value, a lone surrogate) is the caller's defect, detected before
394
390
  anything reaches the transport — it raises :class:`ProtocolError`
395
391
  synchronously instead of being dropped or finishing a healthy
396
- connection (spec 638 FM-638-7).
392
+ connection.
397
393
 
398
394
  Raises:
399
395
  ProtocolError: The frame is not JSON/UTF-8 encodable.
@@ -441,9 +437,9 @@ class Connection:
441
437
  max_attempts: int | None = None,
442
438
  retry_delay: RetryDelay | None = None,
443
439
  ) -> asyncio.Future[Params]:
444
- """Sends one logical command with the INV-013 ``commandId`` contract.
440
+ """Sends one logical command with the same-``commandId`` retry contract.
445
441
 
446
- Same-id retries on the FM-006 retryable pair (``backpressured`` /
442
+ Same-id retries on the retryable pair (``backpressured`` /
447
443
  ``overloaded``) with the bounded full-jitter budget; a value-identical
448
444
  ack settles a replay; ``inputTooLarge`` (and every other error) is
449
445
  never auto-retried.
@@ -460,13 +456,12 @@ class Connection:
460
456
  A future for the ack object (eager: the first attempt is in
461
457
  flight when this returns) — or one already rejected, with NO
462
458
  attempt in flight, when the params are not JSON-serializable
463
- (:class:`ProtocolError`, FM-638-7 — the same settle-on-the-future
464
- posture as :meth:`request`) or the ``commandId`` is reused with a
459
+ or the ``commandId`` is reused with a
465
460
  different payload.
466
461
  """
467
462
  # The prelude and the FIRST attempt run synchronously so the frame
468
463
  # joins the submit tail NOW: a command() immediately followed by
469
- # close() must still write it (PR #30094 review — a deferred task
464
+ # close() must still write it (a prior review — a deferred task
470
465
  # enqueued nothing until the loop's next yield, silently dropping
471
466
  # the frame; request()/notify() already enqueue synchronously).
472
467
  cid = command_id if command_id is not None else self._mint_command_id()
@@ -475,11 +470,11 @@ class Connection:
475
470
  signature = _canonical({"method": method, "params": command_params})
476
471
  except (TypeError, ValueError):
477
472
  # A param json.dumps cannot serialize is the caller's defect
478
- # (FM-638-7), surfaced on the future this method promises — never
473
+ #, surfaced on the future this method promises — never
479
474
  # a raw synchronous TypeError. request() with the same params
480
475
  # rejects through _enqueue_write; the two surfaces must agree.
481
476
  # Nothing is stranded: no _commands entry, no request in flight
482
- # (PR #30094 review, round 18).
477
+ #.
483
478
  rejected: asyncio.Future[Params] = self._loop.create_future()
484
479
  rejected.set_exception(
485
480
  _UnencodableFrameError("outbound frame is not JSON/UTF-8 encodable")
@@ -519,7 +514,7 @@ class Connection:
519
514
  ack = await pending
520
515
  # session/start and session/resume results omit the redundant
521
516
  # commandId echo (contracts/sdk-surface.md); one that carries
522
- # it must echo exactly (TEST-018).
517
+ # it must echo exactly.
523
518
  if "commandId" in ack and ack["commandId"] != cid:
524
519
  raise ProtocolError(
525
520
  f"{method} ack commandId {ack['commandId']} did not echo {cid}"
@@ -547,7 +542,7 @@ class Connection:
547
542
  """Closes the connection without gating teardown on the peer.
548
543
 
549
544
  COMPLETION must not gate teardown — a peer that stopped reading
550
- never completes a write (#15943). SUBMISSION must: the submission
545
+ never completes a write. SUBMISSION must: the submission
551
546
  tail goes TO the transport's optional ``close``, which waits for it
552
547
  inside its own shutdown budget; this class knows nothing about
553
548
  processes, signals, or timeouts. A close-less transport supplies no
@@ -575,7 +570,7 @@ class Connection:
575
570
  # allow_nan=False so a NaN/Infinity float raises ValueError here
576
571
  # rather than emitting a bare `NaN`/`Infinity` token — invalid
577
572
  # JSON the Rust host cannot parse, which would leave the caller's
578
- # request hanging until EOF (FR-638-013; the TS twin coerces to
573
+ # request hanging until EOF (the governing rule; the TS twin coerces to
579
574
  # null). It joins the caller-defect settle below.
580
575
  line = json.dumps(
581
576
  frame, separators=(",", ":"), ensure_ascii=False, allow_nan=False
@@ -588,7 +583,7 @@ class Connection:
588
583
  # reject just this frame and leave the connection (and every
589
584
  # sibling in flight) alive. Detected here, the one outbound
590
585
  # boundary, so all three writers — request(), notify(), and the
591
- # server-request reply — get it (PR #30094 review; the TS twin
586
+ # server-request reply — get it (a prior review; the TS twin
592
587
  # survives the same input).
593
588
  outcome.set_exception(
594
589
  _UnencodableFrameError("outbound frame is not JSON/UTF-8 encodable")
@@ -655,7 +650,7 @@ class Connection:
655
650
  return
656
651
  self._segments.append(remaining)
657
652
  self._buffer_bytes += _utf8_len(remaining)
658
- # FR-638-011: the retained buffer is newline-free on entry (the drain
653
+ # The rule: the retained buffer is newline-free on entry (the drain
659
654
  # below runs to exhaustion), so only the fresh chunk can carry a
660
655
  # newline — ingest stays O(bytes), never a full-buffer rescan.
661
656
  if "\n" in remaining:
@@ -722,7 +717,7 @@ class Connection:
722
717
  # A Ctrl-C that cancels the run cancels this handler task;
723
718
  # `Future.exception()` would then raise CancelledError back
724
719
  # into the callback and log a spurious traceback in the
725
- # consumer's shutdown (PR #30094 review).
720
+ # consumer's shutdown.
726
721
  if done.cancelled():
727
722
  return
728
723
  error = done.exception()
@@ -749,9 +744,9 @@ class Connection:
749
744
  if pending.future.done():
750
745
  # The caller cancelled its await (or an asyncio.wait_for timed
751
746
  # out): the request is abandoned, not unknown — the late reply is
752
- # dropped and the CONNECTION stays healthy (spec 638 Edge Cases:
747
+ # dropped and the CONNECTION stays healthy (the owning spec Edge Cases:
753
748
  # cancellation is a consumer-side act and must not corrupt the
754
- # connection; PR #30094 review, thread 2 — settling a done future
749
+ # connection; a prior review — settling a done future
755
750
  # raised InvalidStateError inside the read loop and tore
756
751
  # everything down).
757
752
  del self._pending[_request_key(frame_id)]
@@ -759,8 +754,8 @@ class Connection:
759
754
  has_result = "result" in frame
760
755
  has_error = "error" in frame
761
756
  if has_result == has_error:
762
- # FR-638-013: the MATCHED caller must settle — with no local
763
- # timeout (INV-006) an unrejected caller would hang until EOF.
757
+ # The rule: the MATCHED caller must settle — with no local
758
+ # timeout an unrejected caller would hang until EOF.
764
759
  violation = ProtocolError(
765
760
  "response must carry exactly one of result or error", line
766
761
  )
@@ -809,7 +804,7 @@ class Connection:
809
804
  }
810
805
  )
811
806
  result = await self._server_request_handler(request)
812
- # FM-009: a failed reply write finishes the connection; it must
807
+ # The rule: a failed reply write finishes the connection; it must
813
808
  # not escape this fire-and-forget task as an unhandled exception.
814
809
  reply = self._enqueue_write(
815
810
  {"jsonrpc": "2.0", "id": request_id, "result": result}
@@ -845,10 +840,9 @@ class Connection:
845
840
  )
846
841
  reply.add_done_callback(
847
842
  lambda done: self._reply_done(request_id, done)
848
- ) # FM-009
849
-
843
+ )
850
844
  def _reply_done(self, request_id: object, done: asyncio.Future[None]) -> None:
851
- """FM-638-7 for reply writes, minus the caller-defect arm.
845
+ """The reply-write rule, minus the caller-defect arm.
852
846
 
853
847
  A reply frame that cannot serialize/encode (a handler returned or
854
848
  raised a non-JSON value — a set, a lone surrogate) is the HANDLER's
@@ -857,7 +851,7 @@ class Connection:
857
851
  connection (the third writer after request()/notify()). The fallback
858
852
  frame cannot re-enter this arm because ``_line`` admits a server
859
853
  request only with a positive-INTEGER id, so ``request_id`` is always
860
- an int and the fallback is ASCII by construction (spec 638 FM-638-7).
854
+ an int and the fallback is ASCII by construction.
861
855
  """
862
856
  error = done.exception()
863
857
  if error is None:
@@ -1,13 +1,13 @@
1
- """Host discovery for the UNBUNDLED wheel (spec 638 FR-638-020, INV-638-08).
2
-
3
- The wheel carries no host binary and no code path assumes one (#29216 fence).
4
- Exactly the two ruled tiers, first hit wins: an explicit ``muse_bin``
5
- argument, then a ``muse`` on ``PATH``. The LIBRARY reads no environment
6
- variable (TS parity: ``spawn.ts`` takes a required command; ``MUSE_BIN`` is
7
- a knob of the quickstart/cookbook HARNESSES, which read it themselves and
8
- pass the explicit parameter down — PR #29277 review, thread 6). No
9
- discoverable host fails BEFORE any process is spawned, with an exact error
10
- naming the parameter and the compatibility page.
1
+ """Host discovery for the UNBUNDLED wheel.
2
+
3
+ The wheel carries no host binary and no code path assumes one. Exactly the
4
+ two ruled tiers, first hit wins: an explicit ``muse_bin`` argument, then a
5
+ ``muse`` on ``PATH``. The LIBRARY reads no environment variable (TS parity:
6
+ the TS spawn module takes a required command; ``MUSE_BIN`` is a knob of the
7
+ quickstart/cookbook HARNESSES, which read it themselves and pass the
8
+ explicit parameter down). No discoverable host fails BEFORE any process is
9
+ spawned, with an exact error naming the parameter and the compatibility
10
+ page.
11
11
  """
12
12
 
13
13
  from __future__ import annotations
@@ -21,13 +21,13 @@ HOST_BINARY_NAME = "muse"
21
21
 
22
22
 
23
23
  def discover_muse_bin(muse_bin: str | None = None) -> str:
24
- """Resolves the MSP host binary to spawn (spec 638 FR-638-020).
24
+ """Resolves the MSP host binary to spawn.
25
25
 
26
- Exactly two ruled inputs (FR-638-020 / INV-638-08): an explicit
26
+ Exactly two ruled inputs: an explicit
27
27
  ``muse_bin``, else the process ``PATH``. There is deliberately no ``env``
28
28
  knob — no production caller passes one, the TS twin has none, and a
29
29
  harness wanting hermetic discovery exports ``PATH`` itself; a future S3
30
- consumer that needs the child's own env adds it there (PR #30094 review).
30
+ consumer that needs the child's own env adds it there.
31
31
 
32
32
  Args:
33
33
  muse_bin: An explicit host path; when given it wins outright and is