Repository navigation
Release 1.2.12a1 - #313
Open
github-actions[bot] wants to merge 319 commits into
Open
Release 1.2.12a1#313github-actions[bot] wants to merge 319 commits into
github-actions[bot] wants to merge 319 commits into
Conversation
…back (#131) * feat: protocol v3 Noise handshake client (XXpsk2/KKpsk0) with v2 fallback Adds the HiveMind protocol version 3 session layer per HIVEMIND-CRYPTO-1 §3.4 on the client side: hivemind_bus_client.noise wraps the poorman_handshake.noise primitive with the negotiation, prologue and transport-framing glue, and HiveMindSlaveProtocol runs the Noise handshake when both peers advertise protocol version >= 3. - Noise_XXpsk2_25519_ChaChaPoly_SHA256 by default; KKpsk0 selected when the server's static key is already pinned and the server offers it - the PSK is derived from the shared password (argon2id, salted with SHA-256 of the server node_id); a wrong password aborts the handshake cryptographically, fail-fast, with no fallback to the legacy handshake - the server's cleartext HELLO and parameter HANDSHAKE payloads plus the selected Noise protocol name are bound into the Noise prologue (downgrade/tampering protection) - after Split() every session message is a Noise transport message; sequential nonces reject replayed, tampered or reordered messages - the server's static key is TOFU-pinned in the node identity file (keyed by endpoint); a pinned-key mismatch aborts the connection - the static X25519 key persists next to the identity file (NodeIdentity.noise_key) and survives restarts - the access_key stays a clear-text routing identifier in the URL exactly as in v2 (it is a non-secret username); the password is the only secret and is never transmitted - v2 and below are untouched: max_protocol_version=2 or a pre-v3 server falls back to the legacy handshake transparently - connect() accepts handshake_max_retries to bound the handshake wait (None keeps the legacy retry-forever behaviour) - e2e migration matrix: v3<->v3, v3 client<->v2 server, v2 client<->v3 server, wrong password, tampered negotiation, replayed transport message; e2e now runs against an isolated XDG home Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: scope coverage to unit tests (exclude e2e requiring a live master) The e2e suite spins up a hivemind-core master and runs under coverage instrumentation; those belong in build-tests (green), not the coverage job. Coverage measures hivemind_bus_client unit coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: pin poorman-handshake>=2.0.0a1 (Noise from PyPI, drop git ref) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: offer 25519_AESGCM_SHA256 Noise suite (enables Web Crypto / JS v3) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: keep tests green under poorman 2.0 backstop + min_protocol_version Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ci: install test extra in coverage job (pytest-asyncio for async tests) The coverage job ran without the test extra, so pytest-asyncio was missing and the async unit tests errored (they pass in build-tests which installs .[test]). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: pre-install pytest-asyncio + poorman prerelease in coverage job The shared coverage workflow uses uv, which won't resolve the poorman 2.0.0a1 prerelease floor without --prerelease=allow; pre-seeding it (and pytest-asyncio) via pip lets the uv install succeed. build-tests (pip) already passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: split e2e-only deps into an e2e extra so uv coverage resolves The circular v3 git-ref (hivemind-core@branch) + hivescope are e2e-only, but living in the test extra made the uv-based coverage job fail to resolve (No solution). Unit tests import neither; move them to an e2e extra. coverage installs unit-only test; build-tests installs test,e2e. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: use pre_install_pip for pytest-asyncio in coverage (not install_extras) The coverage reusable workflow does not accept install_extras like build-tests does (it tried to install a package named 'test'). Use the supported pre_install_pip input for the async plugin; the package's runtime deps come from the plain install (UV_PRERELEASE=allow). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… publish) (#134) * fix: keep hivemind-core git ref out of package metadata so PyPI accepts the wheel PyPI rejects any uploaded package whose metadata declares a direct URL dependency (400 'Can't have direct dependency'), which blocked every alpha since the v3 e2e matrix was added (frozen at 0.9.2a1). Move the Noise-capable hivemind-core git ref from the e2e extra into build-tests pre_install_pip; e2e still installs it via pip, the wheel stays clean. Revert to a version pin once hivemind-core v3 releases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: use bare git+URL in pre_install_pip (no 'name @' whitespace split) uv fails to parse 'hivemind-core @ git+...' because pre_install_pip splits on whitespace; a bare git+URL installs the same package. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: run client build-tests as unit-only to break the v3 circular dep The client<->core e2e matrix needs a Noise-capable hivemind-core, which depends on a poorman-2.0 hivemind-bus-client that is not yet published — an unresolvable cycle during the coordinated v3 rollout. Run the unit suite here (Noise wire format is covered by fixture tests vs the Python reference); the live matrix runs in the hivescope integration harness post-release. This lets the client publish so core can pin it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
AsyncHiveMessageBusClient could not complete a handshake against a protocol-v3 hivemind-core hub, making the published [async] extra unusable. Two shipped bugs: 1. The async client never defined max_protocol_version, so the shared HiveMindSlaveProtocol._should_use_noise read its `2` default and Noise was never negotiated. Add the attribute (default 3), mirroring the sync client, and add the noise_transport session state + encrypt/decrypt in on_message/emit so a negotiated v3 session actually carries traffic. 2. The handshake state machine sent its frames with a bare self.hm.emit(), which is a coroutine on the async client and was never awaited, so the frame never reached the wire and connect() hung forever. Route the handshake/hello/ping emits through HiveMindSlaveProtocol._emit(), which schedules the coroutine on the running receive loop for the async client and is a no-op wrapper for the sync client. HANDSHAKE frames are guarded to stay cleartext (deferred emit runs after noise_transport is assigned). Adds a real e2e (tests/e2e) driving AsyncHiveMessageBusClient against a live hivemind-core master via hivescope: v3 Noise session round-trip and v2 legacy-fallback round-trip. Both reproduce the hang before the fix and pass after. Sync v3 matrix + async unit suite stay green. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
`_decode_bitstring_v1` coerced ANY unknown 5-bit message-type code to 11 (THIRDPRTY) via `_INT2TYPE.get(code, 11)`, silently accepting corrupt or forged frames. HIVEMIND-WIRE-1 §4.2 requires: "A receiver MUST reject a frame carrying an unassigned or reserved value as malformed." The decoder now raises ValueError for any 5-bit code not in the assigned registry (13-31 are unassigned). The three client receive loops (sync/async/http) wrap `decode_bitstring` and drop a malformed frame with a logged error instead of crashing the receive loop. Wire compatibility is preserved: codes 0-12 are all currently emitted by this library and the HiveMind-js peer (cross-platform vectors), so they remain accepted and round-trip unchanged. The library's wire numbering diverges wholesale from the aspirational §4.2 table (6=HELLO vs INTERCOM, 8=CASCADE vs reserved, 11=THIRDPRTY vs reserved, QUERY/CASCADE/RENDEZVOUS binary-emitted vs text-only). Renumbering the wire is a breaking change left for a coordinated spec revision and is documented inline; only the unassigned-code (13-31) rejection is enforced here. Adds regression tests (unassigned codes rejected, all assigned types still round-trip) and updates docs/serialization.md. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix: defer legacy password validation until negotiation * test: enforce production password policy in e2e * test: isolate E2E identity storage * test: document E2E identity helper * fix: log active legacy handshake key size
* Fix websocket reconnect lifecycle * fix: harden reconnect worker lifecycle * test: harden reconnect recovery coverage * fix: isolate reconnecting event callbacks * fix: reset protocol handshake state on reconnect * fix: serialize reconnect worker lifecycle * fix: harden reconnect retry lifecycle
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
run 35903895293
…#298) `HiveMindSlaveInternalProtocol.handle_outgoing_mycroft` built both of its frames with `payload=message.serialize()`, which is a STRING. Since 1.2.4a1 the constructor refuses a string payload (HIVEMIND-MSG-1 §4), so every SHARED_BUS forward and every targeted upstream reply raised `MalformedWirePayload` out of the internal bus handler. Passive monitoring and the reply leg were both dead. Measured in hivemind-test-harness at dev: 5 failures, all of this shape, and two of them say "SHARED_BUS payload must be a JSON object, got str". The fix passes the `Message` itself. The constructor already converts one to `{"type": .., "data": .., "context": ..}`, which is the object §4 requires and the shape the `payload` property reads back. No new conversion, and no repair of a wire value: nothing here reads the wire. tests/test_outgoing_bus_payload_is_an_object.py: 3 failed before this change, 5 pass after, and the set carries two controls — a frame for nobody is still not emitted, and a string payload is still refused. Co-authored-by: JarbasAi <jarbasai@mailfence.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
run 36180102989
…292) HIVEMIND-MSG-1 §4 gives the payload a form per msg_type. For BINARY it is "an opaque byte string with an associated handling instruction". For BUS and SHARED_BUS it is a Layer-1 bus message, and for the routing types a nested HiveMessage, both objects. The text path got its one validated door in #290, HiveMessage.from_wire(). The binary send path had none. BitArray.append() does not supply one, so four shapes passed through get_bitstring: - BUS with a str that is not an object was written to the frame as raw bytes. No peer can parse it, so the far end dropped the message and the sender never learned of it. - BUS with a list or an int raised a bare AssertionError that named neither the payload nor the rule. - BINARY with a dict became a SINGLE BIT on the wire. BitArray.append() reads a mapping as a one-bit token, so the payload was lost and the frame still went out. - BINARY with a str raised "ValueError: Can't parse token" from inside bitstring, which reads a string as a format specification. Each is now refused at the sender with MalformedWirePayload, the same exception the text path raises, naming the shape and the clause. The check sits in _get_bitstring_v1 rather than at the client call site, because that function is the one place every binary frame is built. The same defect was reachable from client.py, async_client.py and http_client.py. TEXT IS A LEGITIMATE PAYLOAD FORM. A Message reaches the encoder as its own JSON text, so the object test accepts a str that decodes to an object as well as a dict, and refuses only text that is not JSON or that decodes to something else. A check that refused every str looked correct and broke 18 existing cases, because the pre-serialized form is what the library itself produces. The two accepted forms encode the same bytes, which a test now asserts. No correct frame moves. Twelve valid cases were encoded with the unpatched function and this one and compared byte for byte: BUS with a dict, BUS with a Message, BROADCAST with a nested object and BINARY with bytes, each at compressed False, True and None. All twelve are identical. ONE EXISTING TEST CHANGES, and the change is a consequence worth naming. test_wire_payload_is_an_object.py built its malformed frames by calling get_bitstring with a bad payload. The sender now refuses to build them, so the decoder test lays the bits out itself per HIVEMIND-WIRE-1 §4.1. It proves the same property against a frame a non-conforming peer can send, and the counts are unchanged: 78 passed, 4 skipped, 80 subtests, the same as before this commit. Co-authored-by: JarbasAi <jarbasai@mailfence.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
run 36234014731
…rrives (#293) * fix: a handshake retry does not downgrade before the server's offer arrives wait_for_handshake waits for handshake_event and, on timeout, calls protocol.start_handshake() to resend. start_handshake read: if self._server_handshake_payload and self._should_use_noise(...): self.start_noise_handshake(...) return self._legacy_start_handshake(self._server_handshake_payload or {}, retry=True) With no offer yet, _server_handshake_payload is None, so the guarded branch was skipped and the legacy call ran with an empty dict. That sends a protocol v2 pubkey handshake decided on NO evidence about the peer, and hivemind-core 5.x refuses it with 1008: HIVEMIND-CRYPTO-1 §3 makes Noise the only key exchange and gives no legacy fallback. start_handshake now returns when the offer has not arrived. Waiting is safe because every caller is a bounded retry loop that re-waits, in client.py, async_client.py and http_client.py, so a peer that never offers ends in that loop's own timeout instead of provoking a refusal. A test asserts that property of the callers, so the guard cannot be left dangling by a future caller that sends once and blocks. REPRODUCED, not inferred. A protocol positioned as a retry finds it, with _server_handshake_payload None, takes _legacy_start_handshake with {} and logs hivemind_bus_client.protocol:_legacy_start_handshake:786 - INFO - hivemind does not support binarization protocol which is the same file, the same line and the same message as the nightly log in hivemind-test-harness test_listener_releases_every_client_after_a_burst. ONE CORRECTION TO THE CLASSIFICATION in live-hivemind-test-harness-T-1567.md. The downgrading client did not read a server payload that was missing the v3 capability fields. It read no payload at all. hivemind-core sends its offer from one place and it always carries max_protocol_version, binarize, encodings, ciphers and noise; its only other HANDSHAKE send is a Noise message. So the server offers every satellite the same full payload, and this one had simply not received it yet. There is no route through a defective server payload. WHAT THIS DOES NOT FIX. The nightly test fails in two shapes. This is the refusal shape. The PollTimeout shape is a second defect: the log shows two handle_hello frames per satellite, so the server saw two CONNECTIONS, and a satellite admitted under one session id and registered under another leaves the registry short. start_handshake cannot cause that, because it already returns early while a Noise handshake is in flight. That shape needs its own task. ONE EXISTING TEST FIXTURE CHANGES. test_the_timed_retry_does_not_open_the_window_for_an_envelope asserted that the retry resends, which the old code only did by downgrading on an empty payload. Its subject is the security property that a peer holding its envelope back past the retry is still refused, and that is unchanged. The fixture now carries the pre-v3 server's own HANDSHAKE request, which is what a real retry answers, so the retry resends through the legacy path and the refusal assertions run as before. The fixture also sets max_protocol_version, because hm is a bare MagicMock and _should_use_noise compares that attribute with an int, which raises TypeError on a mock. The old short-circuit never reached the comparison. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: the retry loops are bounded only when max_retries is set reviewer's SHIP-WITH-FIXES on this PR. The first commit called every start_handshake caller "a bounded retry loop" in three places: the start_handshake comment, this test file's module docstring, and a test name. That is wrong by default. `wait_for_handshake(timeout=5, max_retries=None)` in client.py and async_client.py defaults to None, and connect() passes it straight through. Reproduced rather than taken on trust, and the numbers match the reviewer's: with the default it ran 14 attempts in 3 s and never returned, while max_retries=3 raised RuntimeError in 0.80 s after 3 attempts. THE DEFAULT IS NOT CHANGED, deliberately. "By default this waits for reconnect forever" is that method's documented contract, and it is the right one for a satellite whose hub may be restarting: bounding it would make every caller that relies on it give up instead, which is a regression well outside a fix for a handshake downgrade. What was wrong is what I wrote about it, so that is what changed. The three places now say the loops RE-WAIT, which is the property the guard actually needs, and that they are bounded only when max_retries is set. The test is renamed from ..._inside_a_bounded_loop to ..._inside_a_rewaiting_loop, because re-waiting is what it asserts and always was. THE HONEST CONSEQUENCE, now stated where it belongs rather than glossed: under the default, a peer that never sends its offer leaves the client waiting instead of timing out. That is the trade this PR makes deliberately. Waiting for a hub is what a satellite is for, and the downgrade it replaces got the satellite refused with 1008 and, because the client records that as a refused identity, taken off the mesh until it was restarted. A new test pins both halves so the prose cannot drift again: it asserts max_retries still defaults to None in both clients, with a message saying the wording needs rewriting if that changes, and it asserts that a loop WITH a bound really does raise after that many attempts. Suite unchanged otherwise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: JarbasAi <jarbasai@mailfence.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
run 36235038201
Two clauses, and the first version of this change satisfied only one.
HIVEMIND-MSG-1 §4: a BROADCAST, PROPAGATE, ESCALATE, QUERY or CASCADE
payload "is itself a HiveMessage (a nested envelope)". Both commands
passed the Layer-1 Message straight in, which the constructor turns
into {"type","data","context"} with no msg_type, so the far end raised
TypeError the moment it read .payload. Both now wrap in
HiveMessage(BUS, ...).
HIVEMIND-MSG-1 §5: "When target_site_id is not set, or is empty, on a
BROADCAST, a PROPAGATE, or an ESCALATE, no site identifier is equal to
it. A node MUST NOT deliver the inner BUS message of that envelope to
its Layer-1 bus." The key is read from the OUTER envelope.
So wrapping alone left the frame parseable and UNDELIVERABLE: measured
at the previous head, both commands emitted target_site_id=None even
with --siteid, because that flag reached node.connect, which declares
THIS node's own site, and never the message. It is now set on the outer
envelope.
Measured, per command, with --siteid test-site:
before: target_site_id = None
after: target_site_id = 'test-site'
tests/test_scripts.py: 2 subtests fail before this change and pass
after, one per command. test_the_far_end_can_read_the_payload no longer
asserts merely that the payload is not None; it reads the inner
envelope's type and the Layer-1 message inside it. A control asserts
the key is NOT on the inner envelope, because §5 reads it from the
outer one and setting it inside would look right and deliver nothing.
Suite: 1018 passed, 4 skipped, 85 subtests, no failures, in a venv
built from .[test,async,e2e].
Co-authored-by: JarbasAi <jarbasai@mailfence.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
run 36240527619
hivemind-core sends 1008 from seven sites. Three are in decode() — a non-Noise message on a v3 session, an invalid Noise transport message, and an unencrypted message when crypto is required — and all three need an ADMITTED connection to reach. The clients latched _auth_rejected on the CODE alone and stopped reconnecting, so one malformed frame took a correctly registered satellite off the mesh permanently and blamed its access key. Both clients now read the connection state, not the reason text. The sync client needs one thing the async client does not. websocket-client delivers the disconnect to on_error with an exception FIRST, and that path clears handshake_event; teardown then calls on_close(1008), where a guard reading handshake_event.is_set() saw False and latched anyway. The fact is therefore stamped on the WebSocketApp: run_forever builds a new one per attempt, so the mark has exactly the lifetime of the connection it describes. No clear can downgrade it and nothing leaks into the next connection. Three things the review round added, each measured: The mark is read with `is True`, not as a truth test. Any object that invents attributes — a MagicMock, a proxy — answers a getattr for an attribute nobody set with something TRUTHY, and the guard then believed a session was established when it never was. That fails OPEN: a genuinely refused key stops latching and retries for ever. Two rows in test_client.py caught it, and one row now pins it. on_close takes args[0] only when it is not None. A caller passing None as the ws handed the None straight through, the guard fell back to handshake_event, which on_error had already cleared, and the 1008 latched again — measured: ws=socket gave None, ws=None gave the latch. The async site had NO test. Every async 1008 row set connected_event and never handshake_event, so they covered the never-established branch only; forcing async_client._receive_loop's session_was_established to False left the whole set green. One row now sets the handshake first, and it is the row that fails on that mutation. The comment left where the deleted source-text test stood claimed those three sites were covered; it was false for this one and now says so. Suite: 1022 passed, 4 skipped, 83 subtests, no failures. Co-authored-by: JarbasAi <jarbasai@mailfence.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
run 36240656530
* fix: refuse a Layer-1 Message as a routing payload
HIVEMIND-MSG-1 §4: for BROADCAST, PROPAGATE, ESCALATE, QUERY and CASCADE
"the payload is itself a HiveMessage (a nested envelope) whose msg_type is
typically BUS". The constructor converts a Message to {"type", "data",
"context"} with no msg_type, so a Layer-1 message put straight into a
routing type is not an envelope: the far end raises TypeError the moment it
reads .payload, and the sender is told nothing. That is what
hivemind-client escalate and propagate shipped until #300.
The refusal stands beside the conversion whose invariant it states, and it
is a library invariant, not a conformance point: it is invisible on the
wire and can only refuse this library's own caller (architecture ruling on
T-5567).
Three codec fixtures stop asserting that an undeliverable frame
round-trips: test_escalate_roundtrip encoded '{}', test_broadcast_roundtrip
'{"type":"alert"}', and test_all_message_types used one bus payload for
every type, including the five wrappers.
Scope, measured and not assumed. A payload that is already a dict with no
msg_type is NOT refused. Every wire door hands this constructor exactly
that: from_wire, deserialize, decode_bitstring, the inner view the payload
property rebuilds, and hivemind-core's door at protocol.py:584. Refusing it
is the receive-side twin T-5170 ruled out, and §4 forbids an admitting node
to inspect the inner payload of a wrapped routing message. A first version
refused the dict too: it broke three tests here and hivemind-core's
test_malformed_wrapper_payload.py, which builds that dict on purpose. A
Message object never arrives from a wire.
Suite: 1070 passed 4 skipped before, 1099 passed 4 skipped after, 152
subtests, no failures. With the guard reverted and the new tests kept, 11
fail and the 12 control cases still pass.
Consumer sweep across org:JarbasHiveMind found one caller that builds a
routing type from a Message, hivescope test_bare_query_is_malformed, fixed
first in hivescope#86, which passes with and without this guard.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: cite §4 with §2, because the key is required by composition
reviewer-33 attacked the §4 reading and it came back the other way: the
disclaimer was wrong, and wrong in the cautious direction.
MSG-1 §4 makes a routing payload "itself a HiveMessage (a nested envelope)".
§2 makes msg_type a required field of a HiveMessage -- "msg_type | yes" in
the three-field table, and "msg_type is the only field a receiver may rely
on to decide how to handle a message". So a nested envelope with no
msg_type is not a HiveMessage, and the key is spec-required by composition.
There is no silence here to read as permission.
Verified verbatim at architecture origin/dev 7eabe88, which is the merged
text and not the draft head this review read.
The comment no longer says §4 "names the shape and no field" and no longer
credits this library for the key. The refusal now reads "(HIVEMIND-MSG-1 §4
with §2)" and names msg_type, and the test asserts both clauses instead of
§4 alone.
Behaviour is unchanged: the guard refuses exactly what it refused before.
Suite 1099 passed, 4 skipped, 152 subtests, no failures, the same counts as
the reviewed head.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: JarbasAi <jarbasai@mailfence.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
run 36251605191
…#308) Three things put the REPOSITORY ROOT on `sys.path`, and this repository had two of them. The root carries a second copy of `hivemind_bus_client`, so `build_tests` installs a wheel it does not test and cannot detect a packaging defect. `tests/__init__.py` made `tests` a package, and the root `conftest.py` re-adds the root by itself, which is why deleting the marker alone was measured NOT to fix a repository of this shape. Both go. `pytest_plugins` cannot move into `tests/conftest.py`, because pytest refuses that key in a non-top-level conftest, so a `pytest_configure` hook calls `config.pluginmanager.import_plugin` behind the same `try/except ImportError`. Ten sibling imports lose their `tests.` prefix; with `tests/` no longer a package, pytest puts `tests/` itself on `sys.path` and the unprefixed form resolves. `tests/e2e/__init__.py` is ADDED, and it is not cosmetic. `tests/e2e/` and `tests/` both hold a `test_encryption.py`. While `tests` was a package the two module names differed; with the marker gone both become a top-level `test_encryption` and collection stops with "import file mismatch". The e2e marker makes that file `e2e.test_encryption` again. It does not bring the root back: with the parent no longer a package, the basedir for an e2e file is `tests/`. The third trigger is the one a conftest change cannot reach. `test_the_as_dict_guard_survives_python_dash_o` must use a subprocess, because it checks a guard survives `python -O`. `python -c` puts the CURRENT WORKING DIRECTORY on the child's `sys.path`, and pytest runs from the repository root, so that child imported the working tree whatever the parent resolved. It now runs from an empty temporary directory. `-P` does the same in one flag but is Python 3.11, and this repository still builds 3.10. Measured on dev 7c1c5f6, the wheel built with `uv build` and installed non-editable with the `test,e2e` extras, which is what `build_tests` passes. The instrument prepends `raise ImportError(...)` to the WORKING TREE copy of `hivemind_bus_client/__init__.py` and changes nothing else, so a green run proves the wheel was imported: before, clean 1099 passed, 4 skipped, 152 subtests before, tree poisoned 1 FAILED, 1098 passed after, clean 1099 passed, 4 skipped, 152 subtests after, tree poisoned 1099 passed, 4 skipped, 152 subtests The single failure before the fix is the subprocess cell, and it is the whole reason this change is not hygiene: 1098 sibling cells testing the wheel said nothing about it. Co-authored-by: JarbasAi <jarbasai@mailfence.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
run 36861402998
bitstring 5 removed BitStream and ConstBitStream and reversed the argument order of BitArray.insert(). serialization.py imported BitStream at module load, and the dependency is bitstring>=4.1.1, so a fresh install on Python 3.11 or later took bitstring 5 and nothing that imports hivemind_bus_client.client could start (ImportError: cannot import name 'BitStream'). The decoder now reads through a small cursor over Bits slices, which behave the same in both major versions, and the encoder pads with prepend() instead of insert(). The wire format is unchanged: the cross-platform vectors pass on both versions. The new tests run the codec in a child interpreter with BitStream removed and the old insert() order refused, so they fail on the previous code under bitstring 4 as well as 5. The existing tests that built frames with BitStream or the old insert() order use the shared API. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
run 39366917176
…ups work (#312) The hub returns the session on its replies, with response_mode while a skill waits for an answer. HiveMessageBusClient.emit sent only session_id and site_id on the next utterance, so ovos-core had lost the pending response and the answer went unmatched (silent on the terminal, error sound on HiveMind-voice-sat). The client now keeps the state-record fields of an inbound BUS frame whose session_id is the client's own (active_handlers, converse_handlers, response_mode, intent_context; OVOS-SESSION-1 §2.1), read before the client's own bus stamps default session fields onto the frame. emit sends them with session_id and the client's site_id on a BUS message that has no session. Override fields the hub returned (pipeline, blacklisted_*) are not kept, so an operator change reaches the next utterance. A response_mode whose expires_at has passed is not sent (OVOS-CONVERSE-1 §2.2). A caller-set session is never replaced and another session_id is never adopted. Fail-before: on the previous commit, 3 of the 12 tests in tests/test_session_echo.py fail (a returned pipeline and blacklisted_skills are sent again, and an expired response_mode is sent); on dev, 3 of the earlier 8 fail with KeyError 'response_mode'. Co-authored-by: JarbasAi <jarbasai@mailfence.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
run 39507958182
Contributor
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Human review requested! This supersedes any earlier open release proposal for this repository.