Repository navigation
Conversation
added 30 commits
September 9, 2026 23:52
…fresh retry The four generic founder refresh journeys expected one physical relay row after a committed-but-unacknowledged welcome POST and its exact retry. The relay is append-only: worker/src/index.ts assigns a new sequence per accepted POST and never deduplicates by message ID, so identical ciphertext occupies two rows. Receivers deduplicate by message ID (docs/QSP-v1.0.md). Assert one row per recorded attempt, one distinct ciphertext and message ID, no other posts after the anchor, and that the duplicate row cannot rotate keys, add an admission, decrypt as plaintext, or mark history missing on either the fresh TypeScript founder or the issuing Python member. Keep the pre-POST journal evidence checks, the original optional challenge, and the reply exchange. No relay, protocol, or client behavior changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The native callback acceptance asserted the close code that Node's bundled undici reported for the failed socket. Wire captures (CI 34524097914 artifact, plus a byte-logging proxy in front of the direct workerd fixture on Node 22.23.2/undici 6.28.0 and Node 24.20.0/undici 7.29.0) show the maintained client sends Close(4000, "receive callback failed") and the relay echoes Close(4000) before FIN, yet undici reports 1006 in a few percent of runs: the relay's frames are compressed, undici inflates them asynchronously, and the Close echo is still queued behind that inflate (parser writableLength 102, closingInfo null) when the socket close event fires. Record the Close frames the client actually sends through a recording WebSocket for this test and assert exactly one 4000 close before redelivery. Keep the observed close event, but only reject codes that would mean the relay ended the failed subscription. Retain sent and observed closes plus the Node/undici versions in native-callback-reconnect.json. This is fixture isolation of an undici reporting race, not a change to the relay or client. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…Socket Accepting 1006 in the native callback test left the relay's close handshake untested through undici. Add a separate raw TCP WebSocket regression: it requests no extensions, verifies the upgrade, waits for the ready frame, sends a masked Close, and parses the exact Close code and reason the relay returns and the FIN that follows, with a bounded timeout and the socket destroyed on settle. Red proof: with the Worker's webSocketClose echo removed the test times out with close null and no FIN; restored, it passes for 4000 "receive callback failed" and 1000 "client closed". Correct the native callback commentary: recording close() proves the client's invocation, not wire bytes; undici's 4000-or-1006 report is accepted as unexplained rather than attributed to the relay; the wire evidence was local and CI only observed the 1006. Create the subscription inside the try that installs the recording WebSocket so the global is always restored. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Latch a bounded private receipt (message ID, canonical ciphertext digest, source epoch, verified relay sequence, branch validity) in the same group checkpoint write when authenticated receive accepts the exact ciphertext of a saved control. Observed delivery stays separate from canonical effect: a competing-rekey rewind invalidates the losing rotation and every receipt on its descendants, and a challenged welcome replacement invalidates all prior receipts. An explicit invalidation overrides a duplicate marker the rewind left in the bounded seen cache; without a receipt only the retained marker counts and older evicted evidence stays unknown. No ACK, bare ID, target absence or expected-root inference is treated as proof. Retry review and resume bind the evidence: a fully receipted remove, rekey or send finishes locally as accepted_cleanup with zero POST after real cache eviction or a later legitimate rotation, while cache-only evidence keeps the exact expected-state requirement. Receipts are validated against the pending exact wires, conversation and cursor on every load and write, follow the operation through saveRetry/cleanup, and join the review fingerprint so proof that changes between prepare and commit needs a new review. Writer locking and operation CAS are unchanged. Also fix the a735bfb regression: prepare('refresh') built the generic expected checkpoint without the admission map while the sealed welcome carried it, so every fresh generic refresh with known admissions failed the signed-intent comparison and broke the native journey. Tests: real authenticated traffic pressure against the full legal seen bound, exact cleanup after restart with zero POST, later canonical rotation, losing source rekey left in seen, losing descendant, challenged welcome replacement, malformed/mismatched receipts, changed proof between prepare and commit, and an installed OpenClaw/Python/TypeScript journey through the existing native review path (staged inside an admitted agent turn via the test-only hook). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Latch exact authenticated rekey/remove/create evidence in a bounded private host checkpoint so retry can finish after real seen eviction without a duplicate POST, later legal rotation, or predicted-key install. Explicit invalidation on rewind and challenged welcome replacement overrides leftover seen markers. Missing legacy evidence stays unknown.
Retry a completed rekey after synthetic legal-bound cache pressure and a real signed eviction event, restart, later rotation, and the Retry button. Invalidation wins across duplicate receipt identities; backups reject them. Preserve the failed 8192-message receive experiment as a non-gate follow-up.
…groups While an ordinary-group operation is pending, inbound messages are correctly deferred and cannot start a turn, so after a crash nothing could reach the reviewed qntm_group retry. The pinned OpenClaw CLI already documents a Gateway-backed, session-scoped turn: `openclaw agent --channel qntm --to <binding>`. Verified against the real 2026.9.3 host, that turn lands in the group's own session (this plugin's outbound session route) inside the running Gateway process, marked owner-initiated, with a Gateway-resolved delivery route `qntm:<conversation>` but no platform-native conversation id, so the tool's scope check hid it. resolveGroupToolScope now derives the conversation only from runtime context: the native inbound id as before, or, solely when the run is marked owner initiated and addressed to this channel, the delivery route; both ids must agree when present. Host-scheduled turns carry neither signal. Everything else is unchanged: configured account and binding, action permissions, contact pins, in-process prepare/commit reviews with their fingerprint, the writer lock, relay replay, membership, removal, recovery and expiry guards. The route starts a normal agent turn and commits nothing itself; qntm_gateway scope is untouched. No new endpoint, nonce or approval protocol is added. Tests: scope units for the operator route and its rejections (owner bit or channel missing, other channel, unprefixed or unknown target, other account, missing session, disagreeing ids, foreign review, denied action); a fixture that leaves a rotation saved but never posted with the host stopped; a harness helper that runs the host CLI from a fresh process; and a native journey: host restart with a crash-before-POST state, an inbound Python message stays deferred, a wrong conversation target hides the tool, the operator turn commits the reviewed retry which publishes the exact control once, then the deferred message is released exactly once and delivery resumes. README documents the command, its trust boundary and transcript footprint without claiming a universal restart wake-up. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ership An accepted group_remove whose saved completing rekey expired, was never posted, or no longer fits the verified roster left the remover (including a sole surviving creator) stuck: retry raised "message has expired" and the pending journal blocked any new rotation. Retry now proves the original removal only from exact authenticated receive history (_control_accepted: history ID, canonical digest, source epoch, positive verified sequence and a still-valid branch binding; invalidated history outranks a stale seen entry), then saves a fresh current-roster rotation as a removal_rekey journal before any POST. The original remove ciphertext stays as flat bounded evidence under origin/superseded_operations with the existing revision and byte limits. Release rechecks the journal, the removal proof, source epoch, needsRekey, current authority and the roster under the receive lock; verified replay, not an acknowledgement, finishes the operation. Once any verified rotation left the removal's epoch (a helper's completion, a later readmission or another removal), retry finishes without posting and never re-removes. A removal that was never proven accepted is preserved with a precise error when it expires or its epoch is superseded (follow-up qntm-ra0e). New remove journals pin the target's full public key, canonical member record and admission incarnation; exact release refuses a later readmission of that identity, and journals saved before pinning refuse a target admitted at the current source epoch. Standalone rekey journals keep exact valid bytes, are replaced from current membership when expired, on a rewound branch or after a roster change at the same epoch, and finish without posting once a verified later rotation exists. Old controls are never republished into obsolete epochs and no predicted roots are installed. Tests: 41 new Python cases cover sole survivor and helper rosters, restart and lost ACK, helper completion before and during release, later and same-epoch readmission, losing replacement branches, lower-branch invalidation, authority races between staging and POST, repeated repairs with evidence bounds, proof after cache eviction (tampered history and real eviction pressure), unaccepted expired/superseded removals, standalone rotation variants and CLI/MCP surfaces. Nine receipts tests now expect a superseded standalone rotation to finish rather than stay blocked. A real relay journey runs the accepted removal, expired rekey, lost ACK and fresh CLI and MCP retries against two TypeScript peers, asserting the durable journal before POST, append-only relay rows, survivor rotation and target exclusion. Full Python suite: 817 passed, 1 skipped, 3 subtests. README, MCP docstring and generated CLI help updated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…bership An accepted group_remove whose saved completing rekey expired, was never posted, or no longer fit the verified roster left the browser remover stuck: Retry saved operation raised "The saved operation expired" and the pending journal blocked any new rotation, including for a sole surviving creator. Retry now proves the original removal only from the private control receipt (exact id and digest, source epoch, verified positive sequence, still-valid branch binding; invalidation outranks a leftover seen entry), then saves a fresh current-roster rotation as a removal_rekey journal before any POST. The original remove and rekey ciphertext stays as flat bounded evidence under the existing revision and byte limits, and the original receipt remains pinned while only the repair is in the journal's controls. Release under the group lock rechecks the journal, the removal proof, source epoch, needsRekey, current authority and the roster; verified replay finishes the operation, an acknowledgement never installs keys. Once any verified rotation left the removal's epoch (a helper's completion, a later readmission or another removal) retry finishes without posting and never re-removes. A removal that was never proven accepted stays preserved with a precise error when it expires or its epoch is superseded (qntm-ra0e). New remove journals pin the target's full public key, canonical roster record and admission provenance; exact release refuses a later readmission of that identity, and older journals refuse a target admitted at the current source epoch. Standalone rekey journals keep exact valid bytes, are replaced from current membership when expired, on another branch or after a same-epoch roster change, and finish without posting once a verified later rotation exists. Encrypted backups validate the pinned target, the removal repair's origin and epoch bindings and superseded rotation evidence, and still accept older journals without those fields. Opening a group link no longer re-feeds rows at or before the saved cursor into the receive path, which had raised a false missing-history barrier when a removed member who never observed its removal opened a readmission link. Tests: 40 new host units (sole survivor and helper rosters, lost ACK with encrypted-backup restart, helper completion before retry and during release, later and same-epoch readmission, pinned and legacy readmission refusal, losing replacement branch with recovery and proof invalidation, lower-branch invalidation, authority races immediately before POST, repeated repairs and evidence bounds, tampered and evicted receipts, real eviction by authenticated controls with receipt pinning, unaccepted removals, standalone rotation variants, backup corruption). Seven receipt units now expect a superseded standalone rotation to finish rather than stay blocked. A new Playwright journey runs the accepted removal, expired rekey, real dropped ACK, restart and retry against the relay with a fresh Python CLI peer and a TypeScript peer, asserting the durable pre-POST journal, one relay row, no repeated removal, exclusion and readable replies; it passes on the relay stub and the real Worker (12 passed, 1 skipped). UI units 232, build and integration typecheck clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mbership An accepted native removal whose completing rotation expired, or a standalone rotation that went stale, previously left the journal preserved forever and blocked the group. Port the reviewed Python design (e5cf409) to the host. Removal journals pin the target's key ID, full public key, canonical member record and admission incarnation, so an exact retry never publishes an old removal against a later readmission; older journals refuse a target admitted at the current epoch. The original removal is proven only by exact authenticated acceptance of its saved wire. When it is proven but its rotation expired, targets an obsolete roster or cannot apply, retry reviews a fresh rotation for the current remaining roster as phase removal_rekey with the original encrypted intent and pin as its origin, saved before POST; repeated repairs join the flat bounded superseded history. Receipts now bind to the current controls plus a repair's original removal, still at most two, so the removal receipt survives the journal transition and is invalidated by the same rewind and challenged-welcome rules; no sequence is invented from the cache. If any verified rotation already left the removal's epoch, the journal finishes locally with no POST; a same-epoch readmission stays in the repaired roster. Release rechecks proof, epoch, pending-rotation state, authority and the exact rotation immediately before POST. Unproven expired or superseded removals stay preserved with precise reasons. A standalone rotation is kept exact, fulfilled by any verified later rotation, or replaced after review. Tests: 28 store/tool units (sole survivor and helper, lost ACK and restart, helper completion before and between review and commit, later and same-epoch readmission, pinned and legacy same-epoch readmission refusals, losing repair branch and challenged welcome, authority races, repeated repairs and limits, real cache eviction with a tampered origin receipt, unaccepted removals, standalone rotation variants and barriers, action denial and mutated reviews); a native journey with real host restarts, deferred inbound Python traffic, a denied and then a reviewed fresh-CLI operator retry, an uncertain repair proven from replay, and both removed peers excluded. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…alls CI 34537110345 (Node 22.23.2, Linux) failed the idle-boundary regression with 69 of 160 POSTs: all 31 non-zero lanes in round 0 hit the 5 s abort although every one of those messages was stored (replay has seq 1 for all 32 lanes, no duplicates), and rounds 2 and 4 lost 38 POSTs to "other side closed" or ECONNRESET on sockets that had carried exactly one earlier exchange. The artifact records only send times, so it cannot say whether the relay was slow, the reused socket was stale at the relay's 5 s idle boundary, or the test process stalled past undici's 4 s native keep-alive timer. Tag each idle-boundary POST and subscribe to undici's diagnostics channels for the duration of the test: per POST, the socket id, whether it was reused, idle time since that socket's previous completed response, and the times headers were written and response headers/trailers arrived; plus connection opens/closes, undici transport errors, and event-loop stalls above 100 ms. Merged into idle-boundary.json rows and written to idle-boundary-transport.json. No client, relay or assertion change. Local baseline (macOS, Node 22.23.2): 160/160, round-0 latency 37-119 ms, 76 sockets, reuse only within a round with idle times under 40 ms, no stalls. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Round two of the native removal journey removed a TypeScript peer whose last keys dated from epoch 3, so failing to decrypt later traffic only showed staleness. Before the removal, the host's reviewed refresh now delivers a signed current-key renewal to the still-admitted peer, which opens it through the ordinary TypeScript flow (newest openable welcome, coverage from the signed anchor, replay) and reads a live survivor text. The staged removal pins that same incarnation. After expiry, the uncertain repair, host restart and the operator's accepted_cleanup turn, the peer replays the real removal row and repair rotation into its current session and is removed at the prior epoch, while every row published after the repair fails with those immediately prior keys and Python survivors and the host keep replying. Exact-row, no-repost, deferred-turn and permission assertions are unchanged, as is round one. Test fixtures: the remaining cache-fill loops in the removal eviction test and the accepted-rotation fixture use the linear counter from 6c28159 with the same 8192 bound and real authenticated eviction. The host contact pin is passed to the Python CLI in hex, since a base64url key starting with '-' is read as an option by its argument parser. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ence A saved group_remove that was never verified in replay and can no longer be retried exactly (expired, source epoch superseded, other branch, absent or readmitted target, or a legacy journal whose target was admitted at the current epoch) stayed preserved forever, and its pending journal blocked every send and membership change. Plain retry still never mints a replacement removal; instead `qntm group retry CONVERSATION --release-unproven` and MCP `group_retry(conversation, release_unproven=True)` give up local retry ownership of exactly that journal. After the full locked replay, under the receive lock and a canonical journal compare, release refuses when history is incomplete, when the original removal is verified (plain retry finishes or repairs it), when the exact bytes still apply to their pinned incarnation, or when the journal changed. It posts nothing, claims nothing was removed, and leaves received membership, removal, rotation and recovery state untouched. The uncertain ciphertext, target pin and prior origin/superseded evidence move into a flat private archive on the conversation record with the reason and time, without predicted expected roots. The archive shares the existing 256-entry and 4 MiB budget and is validated before growth; when the entry does not fit or the archive is malformed, release refuses with the journal and archive unchanged rather than dropping evidence. The archive survives receive commits, saves and a challenged welcome replacement. Release of a removal_rekey repair whose original proof was invalidated by a verified later replacement is included, since plain retry never republishes an unproven origin removal. The result reports released, reason, current epoch, members, removed and needs_rekey; a later `group remove` is a fresh current-epoch decision with its own pin. prepare_change gains an optional removal_ttl used only by the relay fixture. Tests: 23 new cases cover pinned and legacy journals, expired, superseded, other-branch, absent and readmitted targets, verified and still-exact refusals, incomplete history, other operation kinds, removed and needsRekey barriers, journal races, revision, byte and malformed archive limits, restart and welcome-replacement survival, late arrival of the released ciphertext, and CLI and MCP surfaces. Full Python suite: 840 passed, 1 skipped, 3 subtests. Generated CLI reference regenerated and drift-checked. Real relay journeys through fresh CLI (expired) and MCP (superseded by a peer's later admission) processes with TypeScript peers show zero posts during release, no false exclusion, then an explicit new removal that excludes the target with current keys while other peers keep reading. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…of prepare_change Root review of 551832b asked for two bounded corrections. The speculative proof_invalidated escape in the release classifier had no reachable production scenario: after an accepted removal the target is absent from the roster, and a same-epoch readmission changes the pin, so a repair origin whose bytes still exactly apply cannot occur. Remove it and its reason from RELEASE_REASONS; if the original unproven bytes still exactly apply at this epoch to their pinned incarnation, release refuses whatever journal kind holds them, while expired, superseded and other-branch removal_rekey origins remain releasable as tested. A focused regression calls the classifier with the same still-applying bytes carried as a repair origin and expects the refusal, without any fabricated acceptance. The prepare_change(removal_ttl=...) argument existed only for tests. Revert the production signature and docstring; the unit helper and the relay fixture now stage a short-lived removal journal from the same production factories the remove action uses (create_group_control_message with a lifetime, receive_group_event, create_rekey and the target pin) and the private journal builder, installing no expected keys and inventing no receive proof. Verification: test_group_removal_release.py 23 passed, removal recovery and control receipt suites unchanged and passing, both real relay release journeys (fresh CLI expired, fresh MCP superseded) passed with zero posts during release. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This branch has not been deployed
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.
Adding a verified contact now admits their full public key, rotates group keys, and sends a recipient-encrypted welcome through the existing relay. Python CLI/MCP, the browser, terminal and OpenClaw support the flow; recipients receive a public locator without keys and can open their welcomes in a different order. Current-key refresh and retry preserve removal boundaries, reconcile confirmed delivery, and retain uncertain ciphertext across restart.
Matching TypeScript and Python welcome/session APIs check replay coverage and authenticated membership before delivering plaintext. Host journals, encrypted browser backups and private receive checkpoints retain the state needed for current-key refresh, interrupted addition/removal recovery, and explicit local release of an unproven stale retry. README compatibility guidance, CLI help and metadata documentation describe these behaviors and their remaining boundaries.
Validation includes shared protocol fixtures and actual browser, Python CLI/MCP, terminal PTY and installed OpenClaw journeys with TypeScript peers and local relay/gateway Workers. Full Linux CI passed on 516935e after fixing a test race between receiving a reply and finalizing its sender journal. All 14 native contact journeys also pass locally. These journeys include current-key exclusion after removal and recovery through a fresh local agent turn after restart. This passing result does not approve the feature's unresolved design choices or the separate unfinished control branches.
This remains a draft, outside the v0.6.2 release, pending user design review. Further feature work is paused: privacy/usability tradeoffs require the user's decision before implementation. Python release support is included here; unfinished browser/terminal and native controls are preserved separately in WIP commits 0e47c85 and 08611f9 and are not merged into this PR (beads qntm-ra0e). Contact-group promotion into gateway governance and incomplete legacy roster migration remain explicitly unsupported; their follow-up is qntm-jg3x. No release tag or production deployment has been made from this branch.