Skip to content

release(41.0.0): persist v55.0.0 + verify v20.1.0 — one holding claim, key grants as rows, typed refusals that park or end - #868

Merged
emooreatx merged 51 commits into
mainfrom
release/41.0.0
Oct 13, 2026
Merged

emooreatx merged 51 commits into
mainfrom
release/41.0.0

Conversation

@emooreatx

Copy link
Copy Markdown
Contributor

Release v41.0.0 (MAJOR): persist v55.0.0 + CIRISVerify v20.1.0

This branch is local/persist-v55-adopt (55b73aa) with origin/main (244baf6, #865) merged in. The merge is tree-identical, because #865 squashed local/main-next, which the adopt branch already carries. On top of that is one release commit:

Ladder: edge v41.0.0 · persist v55.0.0 · verify v20.1.0 (leviculum v0.30.0+ciris.1).

persist v55 adoption check (#753 and slices B–D)

persist item edge
registered_family_held_shut on federation_dimension_rejected terminal, in typed_terminal_verdict (persist's const)
ledger:*: federation_signer_not_acting_for (retryable) the attestation door's refuse sends it to SignatureVerdict::of, which parks it on the held acts-for signer; new witness
ledger:*: ledger_l1_binding_violation terminal; new witness
settlement:*: federation_payment_processor_identifier terminal
matcher answers (key_grant / membership / ledger / settlement / holds_bytes) edge calls persist's match_family / authority_for directly and keeps no table of its own
rc6 carriers the integrated v55 deletes RC6_RETIRED_CARRIERS_ADMITTED_UNTIL_989. A retired carrier is attestation_type_unregistered (terminal), and is_holds_bytes_type matches the whole retired stem
MalformedRow, the purge_unauthorised retryable reasons, takedown_content_unbound terminal, retryable per is_retryable, and transient, respectively
verify 20.1 ciris.accord_invoke.v2 edge never canonicalizes or verifies accord invoke rows. The only accord:invoke row in edge is a test seed

No token edge maps by name was missing.

Local gate (head 0451d16; the tree was the same except the notes-only durable_queue_depth line)

Every step ran one at a time at nice -n 19 ionice -c3 with CARGO_BUILD_JOBS=2.

  • cargo check --all-targets (transport-http transport-reticulum pyo3 test-anchor): pass
  • cargo fmt --all -- --check: pass
  • cargo clippy --lib --profile=test -D warnings, three feature sets (…lxmf mesh-harness test-anchor / …ffi-uniffi / …test-anchor): pass
  • cargo clippy --all-targets -D warnings, set 1: pass
  • lib shards, 0 failed in each:
    • transport::: 501 passed
    • replication::: 500 passed
    • the rest: 1025 passed
    • this includes evidence_tsv_matches_emitted and the new ledger witness
  • integration, 0 failed in every file:
    • blob_federation_e2e: 43
    • chat_message_federates: 30
    • cohort_scope_refusal: 16
    • public_group_reach_762: 1
    • file_publish_stream_744: 3
    • file_range_read_737: 1
    • delivery_receipts_738: 4
    • first_contact_ladder_659: 2
    • first_contact_opaque_683: 9
    • av_spine_live_node_805: 2
  • the pre-push hook (clippy) passed

Do not merge or tag from this PR without the lead.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8

emooreatx and others added 30 commits October 9, 2026 13:16
…bundle field list, both bindings, round/permit histograms, PyO3 links (#848)

The release line (v40.0.7 → v40.0.12) shipped the P0 telemetry; main had
only #810's attach-at-build. This ports it onto main's bundle.

- `EDGE_METRICS_BUNDLE_FIELDS` from `edge_metrics_bundle_fields!`, which
  also destructures `EdgeMetricsBundle` exhaustively, so a field missing
  from the list fails the build. `EdgeMetricsBundle::flatten()` projects
  every field (key `f` always present, detail `f.<label>`), and the UniFFI
  `metrics_snapshot()` is now that projection. The hand-written UniFFI
  projections it replaces are gone: `insert_park_ledger` (#858), the A/V
  ledger loop (#805), `durable.enqueued_total.*` (#845),
  `transport.packets_dropped_total` / `transport.known_destination_evictions`
  (#809) and `blob.serve_legacy_dag_walks_total` (#771). Those values are
  now under their bundle field names. The three legacy reachability and
  low-trust keys and the resident `durable.queue_depth` gauge stay.
- PyO3 `metrics_snapshot()` projects the bundle through the free function
  `metrics_bundle_to_pydict`, which a parity test can drive. Main's PyO3
  dict omitted 22 bundle fields: the #819 pool gauges, the #853 link
  gauges, links-closed-by-reason, the transport counters, the DAG phase
  clocks, and the new histograms and responder metrics.
- `replication_round_duration_seconds` (by kind, recorded in
  `GatedRound::run` around `round_and_report`) and
  `sweep_permit_wait_seconds` (recorded inside `SweepGate::enter` for
  bounded acquires), with the OTel name constants. FixedHistogram was
  already on main (#859).
- The bundle gains the #853 responder telemetry (`responder_rounds_total`,
  `responder_link_up_seconds`, `responder_link_up_total`),
  `recovered_links_total` and `unclaimed_ship_refused_total`. Main
  counted these but reported them only through `EdgeMetrics` getters.
- PyO3 `link_count()` / `link_list()` (including #853's `direction`).
- UniFFI `queue_depth()` was already the resident depth (#845); this adds
  its first test.

Names follow main where both lines have a field. Exception: main's
`durable_enqueued_total` (#845's deliberate rename) is the release line's
`durable_queue_depth`. On main, `durable_queue_depth` means the resident
depth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5FDihqVKTSXyNYs2CLnrK
Points both ciris-persist entries at persist's local v54.1-admission
candidate (02c2b27d = v54.0.0 + #1042 (with the Edge follow-ups:
SignerDoesNotActFor, SignatureInvalid terminal, typed licensure/nonce
refusals, empty signer malformed) + #1044 + #1041 + #1047, Cargo version
still 54.0.0) via git+file:// on its worktree. Never merge this commit
as-is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5FDihqVKTSXyNYs2CLnrK
…es (#858 fix 7)

persist v54.1.0 (CIRISPersist#1042, #1044, #1047) types an unknown signer
apart from a bad signature, repairs a stored Key row that fails
verification, and moves accord nonces to the proposer. The mapping:

- Error::AttesterKeyUnknown { attesting_key_id, .. } and the key plane's
  KeyRefusalReason::AttesterKeyUnknown: transient, parked on the NAMED
  signer (ApplyOutcome::Refused gains `awaits_signer`), released by that
  Key's admit. The directory-absence read stays beside it (FSD I5). An
  empty key id names nothing that can land, so it is not a park.
- Error::FederationTierUnverified and the key plane's
  unverifiable_signature: terminal. Ends the self-park of an invalid
  self-scrubbed Key.
- Bare SignatureInvalid stays transient at this pin: persist's
  check_signer_acts_for still uses it for the #776 ordering gap. Pinned by
  a witness so the later flip is visible.
- ReplicatedKeyOutcome::InvalidReplaced: admitted; releases rows parked on
  that key.
- licensure_delegator_not_authority: authority_acts_by_quorum and
  accord_proposal_nonce_reused: terminal (prefix-only verdicts on generic
  variants; persist exports no constant for either).
- The key_grant door reads the signature axis too (its carrier rides the
  attestation plane).

Witnesses: replication::typed_refusal_858_tests (5) and
bridge::tests::a_quorum_authority_licence_and_a_reused_accord_nonce_are_terminal,
each red with its mapping reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5FDihqVKTSXyNYs2CLnrK
…gnatureInvalid terminal, typed licensure/nonce (#858 fix 7)

persist's #1042 follow-ups (02c2b27d) type the remaining fused arms:

- Error::SignerDoesNotActFor { attesting_key_id, .. }: retryable. The
  outcome carries AwaitedSigner::ActsFor(signer) and the row goes on the
  #858 held-signer index on any door that asks acts-for (occurrence, its
  revocation, route, touch claim). Released by the binding or occurrence
  that makes the signer act; from the second refusal on the terminal
  schedule, which bounds a signer that is never bound (2 offers ~20 s
  apart, then 30 min doubling to the 6 h cap).
- Bare SignatureInvalid: terminal (persist: terminal on every door).
- LicensureDelegatorNotAuthority { reason: authority_acts_by_quorum } and
  AccordProposalNonceReused: terminal, matched on the typed variants; the
  prose prefixes and their local constants are gone.
- The empty-signer guard stays as defence; persist now refuses an empty id
  as InvalidArgument at the source. park_fresh_858 pins what O5 means now.
- ApplyOutcome::Refused.awaits_signer is Option<AwaitedSigner> (Key |
  ActsFor); Session notes a missing Key only for Key.

Witnesses (typed_refusal_858_tests): (5) SignatureInvalid terminal, (6)
SignerDoesNotActFor indexed and released by the binding, (7) a never-bound
signer on the route door settles on the terminal schedule. The park_fresh
log-capture witnesses get a process-wide defer-to-scoped subscriber: the
new witnesses emit the same park lines from other threads, and a callsite
cached `never` there emptied the capture intermittently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5FDihqVKTSXyNYs2CLnrK
… row repairs are terminal (#858 fix 7)

Per persist release-54.1.0 (e165a2ac): named_edge_not_license_delegation,
named_edge_not_onto_emitter, authority_acts_by_quorum and
not_authority_at_receipt are terminal. named_edge_absent and
not_on_live_chain_at_asserted_at turn on rows still replicating and keep the
transient rule with its re-offer cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5FDihqVKTSXyNYs2CLnrK
Points both ciris-persist entries at persist's v54.1.0 release candidate
e1591219 (72f432ca + CHANGELOG and an evidence claim-ID rename, no code;
release-54.1.0: #1042 + follow-ups, #1044, #1041, #1047, #1046 steward
purge, #1052/#1054; Cargo version still 54.0.0, the tag commit 0ea1cf0f
adds only the version, Cargo.lock and evidence) via git+file:// on
/home/emoore/CIRISPersist-wt-541. Never merge this commit as-is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5FDihqVKTSXyNYs2CLnrK
…l, purge_unauthorised excepted (#1046)

Error::PurgeRefused { token, .. } maps to a terminal refusal named by its
token: purge_tombstoned (a purged record, refused at every admission door
for good) and trace_run_kind_mock (a federation-tier trace signing
run_kind = mock) among them. purge_unauthorised stays on the transient
rule: a replicated bundle is re-verified against this node's state, so it
can mean the claimant root's community row or a cosigner's Key is not
held yet, until CIRISPersist#1055 types the two apart.

Witness: bridge::tests::unrepairable_licences_a_reused_accord_nonce_and_a_purge_are_terminal
gains the purge_tombstoned and trace_run_kind_mock rows (terminal) and the
purge_unauthorised row (transient). Reverting the arm turns the
tombstoned row red; narrowing it to TOMBSTONED turns the mock row red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5FDihqVKTSXyNYs2CLnrK
Re-points both ciris-persist entries at persist's v54.1.0 RC 012aa94f
(PR #1057 head; was e1591219 / 0ea1cf0f), on the release-54.1.0 line via
git+file:///home/emoore/CIRISPersist-wt-541. Additive over e1591219:
purge_put_execution replaces purge_put_tombstone on FederationDirectory,
the purge_execution_incomplete token, PurgeState::Incomplete and the
`voided` report fields, the trace-purge rollup recompute, accountability
rows kept when tombstoned, and the manifest-form run_kind check. Edge
implements no FederationDirectory and matches no PurgeState: no compile
change. Cargo version is now 54.1.0 (edge's `version = "54"` still
matches). Never merge this commit as-is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
persist raises Error::TraceDimensionInvalid from
admission::check_trace_dimension_admission at the attestation put door
the replicated apply goes through: the attester absent from
subject_key_ids, a missing trace_id / agent_id_hash, not exactly one of
the inline and manifest forms, or (v54.1.0, PR #1057) a run_kind outside
the closed vocabulary in either form. Every arm judges the signed row, so
re-offering the same bytes never admits. typed_terminal_verdict now maps
it terminal, named by persist's own kind() (federation_trace_dimension_invalid);
before, it fell to the unclassified transient rule and took about eight
refusals and three capped windows to settle.

Witness: a Terminal row in
unrepairable_licences_a_reused_accord_nonce_and_a_purge_are_terminal with
persist's manifest-form run_kind refusal. Removing the arm turns it RED
(Transient != Terminal).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
Re-points both ciris-persist entries at persist's v54.1.0 RC ae1dadad
(PR #1057 head after Codex round 2; was 012aa94f), via
git+file:///home/emoore/CIRISPersist-wt-541. Seven commits over 012aa94f:
now/now_iso dropped from the purge surfaces (edge calls none), a
receiver's purge_human_subject_pending_notice_row for an unbound blob
(terminal through edge's PurgeRefused arm, as before), a purged sealed
manifest evicting its unshared DAG chunks, derivatives withheld with a
withheld parent transitively, and debug_canonicalize hashing the real
preimage. No edge compile or behaviour change. Never merge this commit
as-is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
Re-points both ciris-persist entries at persist's v54.1.0 round-3 head
ea7f6e56 (6 commits on ae1dadad, Codex round 3 on PR #1057), via
git+file:///home/emoore/CIRISPersist-wt-541. Adopter surface:
BlobError::Purged { sha256_hex, notice_id } (blob_purged, mapped in the
next commit), get_detection_events excluding tombstoned rows and V189's
accord_proposal_nonce_claim (edge uses neither). Never merge this commit
as-is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
persist v54.1.0 (Codex round 3 on CIRISPersist#1057) adds
BlobError::Purged { sha256_hex, notice_id } (kind blob_purged): a steward
purge tombstoned the bytes, or every manifest a sealed-DAG chunk belongs
to. Persist's contract is Withdrawn's: never NotHeld, because every holder
rooted at the same trust root is bound by the same purge. It arrives from
the serve disposition, the viewer reads and the sealed-manifest open.

Before this, the serve door's wildcard answered PolicyDenied ("this node
holds the bytes and declines"), which demotes the peer and walks the
fetcher to the next holder, and the viewer reads mapped it to Substrate.
Now, everywhere Withdrawn is handled:

- blob_swarm::serve_result_to_chunk: Purged -> ChunkSourceRefusal::Withdrawn,
  so the wire word is MissReason::Withdrawn and both fetchers abort
  (SwarmScheduler -> GoneFederationWide -> PullOutcome::Refused; the DAG
  SwarmFetch stops without trying another holder).
- blob_swarm::persist_source (the manifest serve and the stream listing):
  Purged -> DagMembership::Withdrawn.
- group_content: a typed GroupContentError::Purged { sha256_hex, notice_id },
  which chat::UnopenedReason::from_store_error (the one reader mapping, used
  by chat and files) reads as Withdrawn.

Witnesses: blob_swarm::tests::a_purged_blob_from_the_persist_door_is_withdrawn_on_the_wire
(arm removed: PolicyDenied != Withdrawn), the Purged row in
every_persist_serve_refusal_is_mapped, and
group_content::persist_store::tests::a_purged_blob_is_a_typed_answer_and_reads_as_withdrawn
(arm removed: Substrate("... is withheld: steward purge ...")).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…minally

The whole-blob SwarmScheduler has always ended on GoneFederationWide as
PullOutcome::Refused. The DAG fetcher (SwarmFetch) stopped asking holders
on the same wire word (MissReason::Withdrawn / Revoked; persist v54.1.0's
BlobError::Purged reaches the wire as Withdrawn), but returned the stop as
a String that fetch_verified read as DagFetchStop::Transport, so the pull
ended FetchFailed and booked a retry: a retracted DAG was re-asked up the
retry ladder though every holder is bound by the same verdict.

- DagFetchError { Transport, Gone } (re-exported beside DagByteFetch), and
  a DEFAULT DagByteFetch::fetch_typed that reads every fetch error as
  Transport, so the required fetch and every existing impl are unchanged.
- SwarmFetch::fetch_typed returns Gone for a gone miss; fetch delegates.
- DagFetchStop::Gone, and dag_gone(): PullOutcome::Refused with no
  book_retry, at all five DagFetchStop sites (pull_inline_with, the sealed
  manifest and lane chunk, the plaintext manifest and chunk).
- edge.rs ContentResult::ContentMiss doc: edge does not retry; Withdrawn
  and Revoked mean stop (the host decides), NotHeld / PolicyDenied /
  DiskPressure may go elsewhere.

Witness: blob_swarm::dag_gone_terminal_tests (a gone manifest and a gone
chunk each end Refused with no retry booked; the control pull stores).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
clippy -D warnings (--lib --profile=test, all three feature sets) refused
d899e70's `dag_gone(&self, ..)`: the helper reads nothing of the puller.
Moved beside DagFetchStop as a free fn; the five call sites are unchanged
otherwise. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
Re-points both ciris-persist entries at persist's v54.1.0 round-4 head
9daa25a2 (7 commits on ea7f6e56, Codex round 4 on PR #1057), via
git+file:///home/emoore/CIRISPersist-wt-541. Adopter surface: a new
`plane` value "purge_tombstone_subject" on the existing retryable
AttesterKeyUnknown (edge keys on the variant: transient, parked on the
named key), Purged byte doors unchanged since round 3 (edge keys on the
variant, never notice_id), put_detection_event / `voided` /
FederationDirectory::purge_tombstone_notices (edge uses none, implements
no FederationDirectory). No edge code change. Gated together with the
dag-gone-terminal commits (d899e70, 286137d) fast-forwarded onto this
branch. Never merge this commit as-is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
Both ciris-persist entries move from the LOCAL TEST PIN (git+file on
persist's worktree at 9daa25a2) to the released tag: git =
"https://github.com/CIRISAI/CIRISPersist", tag = "v54.1.0", version = "54"
(the main-line form). Cargo.lock resolves to
c03aae67b4a017e9d6fcfbf1a434786274dcb556, the merge of CIRISPersist#1057;
9daa25a2 (the head gated on 78fb786) is its ancestor and the tree diff
between them is empty, so the code is the code that was gated.
pyproject's `ciris-persist>=54,<55` already admits 54.1.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
… tag v55.0.0 before merge

Both ciris-persist entries point at persist's release-55.0.0 candidate
95e36ac6 (21f72044 + one test-only Python file) via git+file:// on
/home/emoore/CIRISPersist-wt-550. Its Cargo version is still 54.1.1 until
persist's bump commit, so `version = "54"` stays until the tag (then 55).
Cargo.lock's persist line was hand-edited (no dependency changes between
c03aae67 and 95e36ac6); `cargo update -p ciris-persist` re-confirms it at
the first build. Never merge this commit as-is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…t rows

- is_holds_bytes_type matches persist's whole retired stem
  (registry::RETIRED_HOLDING_CLAIM_STEM), and the possession fixture is
  rebuilt on holding_claim_row
- store_gate asks cohort_scope::is_structurally_invisible
- key_grant_emitter counts emitted ROWS (KeyGrantEmission.attestation_ids)
- test carriers read key grants as rows: is_key_grant_row,
  SignedKeyGrantRow, KeyGrantRow::from_attestation(..).axis, and count an
  admission by its projection

Still red on 95e36ac6 (a persist defect, reported): chunk_grants_779 799
and 797. persist's dirty_axes (key_grant.rs:1807) builds a self/family
Content axis with the node key as its owner. The pending loop then
withholds every recipient and clears the ledger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…fusal dispositions

Key grants:
- a row routes to the key-grant door when it is_key_grant_row, or names a
  retired KeyGrant set type
- apply_key_grant pre-parses with KeyGrantRow::from_attestation (an Err is
  terminal) and admits a SignedKeyGrantRow
- the subject-pull carve is unchanged: grants travel by may_receive only

typed_terminal_verdict:
- MalformedRow and TraceDimensionInvalid: terminal
- AttestationTypeUnregistered and PaymentProcessorIdentifier: terminal
- DimensionRejected: terminal only for registered_family_held_shut and
  ledger_l1_binding_violation
- PurgeRefused: purge_unauthorised is terminal unless
  unauthorised_reason::is_retryable; takedown_content_unbound is transient;
  every other token is terminal
- KeyGrantRefused: terminal for malformed, set_retired,
  wrap_algorithm_not_v2 and signer_not_{self,minter,author,stream_owner};
  the audience, membership and attestation reasons stay transient. The
  table points to CIRISPersist#1064 and gives way to persist's predicate
  once it ships.

Witness v55_typed_refusals_follow_persists_dispositions walks
KeyGrantRefusalReason::ALL with an exhaustive match.

#776 and #858 re-shapes: persist v55 refuses a tampered occurrence (a
device_class, a diverging asserted_at, a row naming no signer) as
federation_malformed_row. Edge holds that terminal from the first refusal,
so none of those rows parks. occurrence_before_binding_776 asserts the
terminal verdict and no park; park_fresh_858 now expects three parks (O1
on P, O2 on A, O3 on U) where it expected four.

Fix 6's witness moves to a RefusalBackoff unit test
(a_dependency_less_transient_moves_to_the_terminal_schedule_at_refusal_8)
because no occurrence row reaches that path any more. O5 (no signer) was
the only dependency-less transient row in park_fresh_858, and it is now a
terminal malformed row. The schedule fix 6 bounds still exists for any
unclassified transient, so the bound is now witnessed at the backoff
itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
- The manifest is adopted LocalOnly. The gate's disposition is carried to
  promote_adopted_manifest_to_dag, and Stored reports
  DagPromotion.announced (CIRISPersist#951).
- key_grant_admitted wakes a parked pull only for a projected, non-pending
  row.
- R1: a v4 community DAG whose chunks carry an epoch is a stream-keyed
  room. Its chunks are adopted with no community epoch on the provenance,
  and it waits on chunk-key readiness as an invisible DAG does
  (is_stream_keyed_room, chunk_provenance_of, needs_chunk_key_readiness).
  The epoch clear is a workaround until persist ships
  BlobProvenance::for_stream_keyed_chunk.
- DurabilitySweep::already_here: since v55 the write doors file the holding
  claim as they store. A pass that reaches a file it holds whole, with a
  live and fresh `here`, now says so; before, that file showed up nowhere.
  The durability_763 witnesses assert it, plus the `Here` verdict right
  after publish. A repair now reports the claim that exists
  (announced: true).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
PersistGroupContentStore::spawn_holding_claim_reack starts
Engine::spawn_reack_sweeper and returns a HoldingClaimReack handle:
is_running, async stop(), and a Drop that signals stop. edge_node keeps
the handle in its Occurrence and stops it after the role returns. A
custody:ack `here` lives 72 h, and the sweeper renews it about daily
(CC 3.1.3.3 rule 5).

Witness: the_hybrid_store_runs_the_reack_sweeper_and_stops_it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
Each self and family test asserts that:
- a non-member never receives the custody:ack claim, and its list_holders
  and live_holders are empty;
- a co-member sees the claim, and live_holders names the writer;
- no retired holds_bytes row exists.
Where a test had no non-member node, one is added (node_b in
a_file_over_the_inline_bound).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
EdgeConfig::holds_bytes_ttl_seconds and DEFAULT_HOLDS_BYTES_TTL_SECONDS
stay, documented as deprecated and INERT: a holding claim's freshness is
its own row's expires_at. The release-notes draft for the MAJOR goes at the
top of docs/RELEASE_NOTES.md. There is no version bump yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
… room DAGs are v4

The (pos) leg of f0de7ca was red. The serve helper built a bridge with no
self provider, so the writer advertised none of its own SelfOwn rows,
whatever their audience. The helper now wires the bridge as production
does: the node's own key, plus the identity and engine for the family
tier. A fresh bridge per call means no earlier round's per-peer watermark
can hide a row from the listing.

a_community_chunk_dag_is_pulled_holder_to_holder_under_the_rooms_dek:
since persist #977 a room stream is stream-keyed. Its manifest names five
data chunks and epoch 0's terminator, and C keeps the terminator beside
the chunks it kept. This is R1's path end to end: B and C pull a
stream-keyed room DAG through edge's pull, and D's pull of the old file
still parks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…still files `here`

already_here now reports real state that production uses. Each outcome of
a pass is booked into a new EdgeMetrics family, durability_custody, and
shows up in the snapshot, the UniFFI counters and the PyO3 dict:
- filed_here
- already_here
- filed_none
- report_refused
- repair

The four durability_763 rewrites follow v55's file-on-write, so a first
pass finds the claim fresh. That leaves the pass's own filing job
unwitnessed, so this adds
the_pass_files_here_when_the_claim_is_retracted_stale_or_lapsed_763. A
claim retracted by a `none`, one 25 h old (past the daily refresh) and one
73 h old (past the 72 h liveness) are each filed afresh. live_holders
names A again, and each filing is booked. The pass reads its clock through
durability_sweep_at(now), so a test can stand a day or three days on.
Fail-before: with the filing arm removed, the retracted leg is red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…es as retired

Docs only. Every mention that described `holds_bytes` as a live
mechanism now describes the v55 holding claim instead: a `custody:ack:v1`
`here` row at the content's own scope, filed by persist's write doors
(and, for self/family, by edge's `report_here` after a pull), live for
72 h, renewed by the re-ack sweeper, and retracted by its signer. That
covers src/, tests/, FSD/ and docs/. Historical mentions are kept and
labelled as retired in v55: dated decision records, witnessed results
from before v55, the legacy-row possession tests, and the CEG §10.1.2
TTL helper. docs/RELEASE_NOTES.md history is unchanged.

FSD/CONTENT_TRANSFER.md §7 invariant 1 is restated for v55. A self/family
claim is observable only inside its cohort. Its witness is
assert_holding_claim_confined in three integration files, and its mutant
is edge's audience_withholds opened. The section's duplicated heading is
fixed.

A few runtime strings say "holding claim" now where they said
holds_bytes: two revocation log lines, MeaningRefusal::PossessionIsNotMeaning's
Display, the edge_node key-mismatch error, and test assertion messages. No
test matches on them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
Notes the new EdgeMetrics family and its five outcome tags, plus a merge note:
when this branch meets #820 (local/820-p1c), the catalogue completeness gate
needs a spec for it (a counter with a closed `outcome` label from an enum).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…m receipts; per-recipient key carriers

Since persist v55 (#989), `Audience::discoverable()` is true only for a
Commons placement: a room's holding claims stay in the room. Edge derived
`With::is_structurally_invisible` as `!discoverable()`, so a room started
reading as invisible. It now asks persist's own predicate,
`cohort_scope::is_structurally_invisible` (self and family only), which is
what chat_message_federates' `with_answers_both_questions_from_persist`
asserts. Before the fix, `only_self_and_family_are_structurally_invisible`
and `with_answers_both_questions_from_persist` were both red.

Tests:
- chat_message_federates
  - A widened room row's `replicates.discoverable` is now false, because
    its claims stay in the room.
  - `carry_key_grants` hands each row only to its recipient's node, and
    only rows that node does not already hold, as may_receive's refers-to
    delivery and the want-diff do. Re-offering carol's own old-epoch row
    after her removal is refused `recipient_not_in_audience`.
- delivery_receipts_738: a room stream is stream-keyed (#977), so its tree
  includes epoch 0's terminator. K is therefore 6. The wrong-epoch and
  past-the-tree legs take K from the published tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…5 disposition witness

- TraceDimensionInvalid and MalformedRow share one arm (`|`). Both are
  Some(err.kind()), so they had the same disposition anyway.
- v55_typed_refusals_follow_persists_dispositions is split by family into
  three tests over one helper, assert_v55_dispositions: row shape, purge
  and takedown, and the KeyGrantRefusalReason walk. The split replaces an
  allow, and every row is unchanged.
- The re-ack test's binding `none` is renamed `unsigned`.
- late_device_768 passes is_key_grant_row by path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
emooreatx and others added 21 commits October 12, 2026 12:24
Edge vendors the same Constitution as the persist it pins. persist v55
vendors CC 1.0-rc7, and edge was still on rc6 (651140a). The rc6
`holds_bytes:sha256:{prefix}` family, retired in rc7, made
cc_namespace_match_vectors_replay report 6 divergences against persist
v55's matcher.

Both files are copied byte-for-byte from the local CIRISConstitution tag:
`git show v1.0-rc7:manifests/namespace_registry.json` and
`git show v1.0-rc7:manifests/namespace_match_vectors.json`.
- tag v1.0-rc7 -> commit 536b8cbb931da1f4549425284af24cd227587c36
- namespace_registry.json sha256
  6a14cb027ac65b8515f2004274278e73f4bcc4d3dfe233e9870c27c7d02d3c84
- namespace_match_vectors.json sha256
  8fc35ba167f1ab0d2cb5b4ea742865dff3fd92d1bd751105ddbb7fc191a73331
  Both equal persist v55's VENDORED_*_FILE_SHA256.
- _meta.registry_sha256
  796f609981434b838edab1165462484f1be67889eed7f60c26b210ede5984464
- vectors: 968 -> 1103

cc_namespace consts moved with the bytes: VENDORED_CC_COMMIT,
VENDORED_REGISTRY_SHA256, VENDORED_N_VECTORS = 1103, and cc_version
1.0-rc7. The replay now runs 1103/1103 with 0 divergences.
vendored_grammar_hashes_to_its_pin and the relay-delivery leaf test pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…och), interim; replace with tag v55.0.0 before merge

Both ciris-persist entries now point at persist branch
v55-fix-provenance-epoch @ 8c2d88f7 (on c577accd, v55.0.0), via
git+file:// on /home/emoore/CIRISPersist-wt-550. That commit carries
BlobProvenance::for_stream_keyed_chunk and the
StreamKeyedChunkCommunityEpoch refusal (CIRISPersist#977). The crate is
55.0.0, so the requirement moves to version = "55".

This is an interim pin. The final head follows persist's key_grant owner
fix (dirty_axes, key_grant.rs:1807).

pyproject moves its floor with the MAJOR: ciris-persist>=55,<56. None of
the four ABI versions moved (DIRECTORY 7, the other three 1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…edy a community-bound chunk at promote

persist 8c2d88f7 (CIRISPersist#977) fixes the provenance epoch on
stream-keyed room chunks. Edge's side:

- A stream-keyed room DAG is a community_dek DAG whose v4 manifest names
  per-chunk epochs. Its chunks are adopted under
  `row_provenance.for_stream_keyed_chunk()`, which drops the community
  epoch and the minter. This replaces the clear-.epoch workaround.
- At the chunk adopt, persist's StreamKeyedChunkCommunityEpoch refuses the
  whole batch. That is terminal, logged at ERROR as an edge bug, and
  reported as DagPullRefusal::StreamKeyedChunkCommunityEpoch { stage:
  "adopt" } (tag dag_stream_keyed_chunk_community_epoch).
- At the promote, the same refusal names a chunk bound before the stream's
  key rows landed. The pull applies persist's remedy, ONCE per attempt:
  every chunk the manifest names that a community epoch binds is evicted
  (Engine::evict_blob), re-fetched, verified, re-adopted stream-keyed and
  booked blob_dag_chunks.remediated. Then it promotes again. A second
  refusal is terminal { stage: "promote" } and is an edge bug.
- GroupContentError gains the typed StreamKeyedChunkCommunityEpoch, mapped
  from persist. Chat's read mapping treats it as a substrate fault.

Witnesses:
- pull.rs `a_stream_keyed_room_dag_waits_for_stream_keys_and_a_legacy_one_does_not`
  (R1). The chunk provenance equals persist's constructor: no epoch, no
  minter, the room kept. A non-room provenance is refused. A legacy room
  DAG keeps the row's provenance.
- blob_federation_e2e
  `a_room_chunk_bound_to_the_community_epoch_is_refused_at_adopt_and_remedied_at_promote_977`:
  - the gap: a chunk under the row's provenance lands bound before the
    stream's rows;
  - the adopt refusal: after the rows, the same provenance is refused,
    nothing written;
  - the remedy: the pull is Stored, with exactly 1 chunk remedied, no
    community binding left, and the file byte-identical.
  Fail-befores, both observed:
  - no remediation: the pull ends DagRefused { stage: "promote" };
  - the row's provenance reused for chunks: DagRefused { stage: "adopt" },
    and the room DAG holder-to-holder test goes red as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…wner fix); superseded by the final v55 head

Both ciris-persist entries point at v55-fix-provenance-epoch @ ff72d7ce
(8c2d88f7 + the key_grant dirty-loop fix: self/family content rows are
emitted to the owner's devices; a mis-built axis stays dirty). For the
targeted chunk_grants_779 check only; the full re-gate runs on the final
head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…t pending, and addressed to this node

The key_grant_admitted guard was `projected && !pending`. It now also
requires `recipient_key_id == local_key_id`: on a shared backend a sibling
occurrence's wrap can be projected, and it must not re-pull this node's
DAG. PullSink carries the puller's local_key_id, set in `start`.

The chunk_grants_779 wake tests went red only with the wake removed
entirely. Dropping either half of the guard kept them green, so the
guard's precision was unwitnessed. A spurious wake costs one re-pull.

Witness: a_parked_dag_wakes_only_on_a_projected_grant_to_this_node. A sha
parked on its content key:
- is NOT woken by a pending row (whether or not it is reported
  projected), by an unprojected row, or by a projected row for another
  node's key;
- IS woken by the projected, non-pending row to this node.
Fail-befores, all observed RED, each at its own leg:
- `|| pending` dropped
- `!projected` dropped
- the recipient check dropped
With the wake removed entirely, 797 and the two #779 ladder tests are
red (checked on ff72d7ce).

Why `recipient_key_id == local_key_id` cannot miss a wake: it is the ONE
key the puller can open with.
- Every persist unwrap looks the wrap up BY the viewer key:
  - self/family: `get_at_rest_grant(sha, viewer_key_id)`, in
    at_rest_cascade.rs `recover_blob_dek_for_viewer`;
  - stream epochs: `writers_own_epoch` / `stream_grant_sealer(..,
    viewer_key_id)` in chunk_dag_cascade.rs;
  - community epochs: `community_dek_member_grant_wrap(.., viewer_key_id)`
    in community_dek.rs.
- The puller asks every one of those doors as `self.local_key_id`:
  open_sealed_manifest_as, sealed_dag_readiness and
  promote_adopted_manifest_to_dag in pull.rs.
So a wrap to any other recipient changes nothing the parked pull can read,
and a wake for it re-pulls only to park again.

A persist wrap's recipient is the occurrence_key_id (resolve_recipients).
Several occurrences can share one backend's content-KEM identity. That
happens on a shared backend: one KEM row, `id = 0`. Each engine there
still has one derived key. The puller is given that key: edge_node
refuses to start unless engine.local_derived_key_id() == cfg.node_id,
which is the key it passes to BlobPuller::spawn.

#541's use_node_identity selects the TRANSPORT signer
(pyo3.rs `node_transport_identity`), not the content engine's key, and the
PyO3 host spawns no puller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…6 (release-55.0.0, PR #1065); replace with tag v55.0.0 before merge

Both ciris-persist entries point at 11e48c56 via git+file:// on
/home/emoore/CIRISPersist-wt-550. It descends from c577accd, 8c2d88f7,
ff72d7ce and a6c36c7d (all verified with merge-base --is-ancestor).
Since a6c36c7d it adds: register_stream_epoch_dek keeps the chunk door's
stream-name rule (a 64-hex name is refused at register), V192 re-dirties
the content axes a v54 node cleared, and docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
… r2 fix head); replace with tag v55.0.0 before merge

Both ciris-persist entries point at 9e1d6dda (descends from 11e48c56,
verified). Lockfile-only (`cargo update -p ciris-persist`); not compiled
yet: the delta re-gate runs after persist's certify.

Docs: meaning.rs said `registry::RETIRED_CARRIER_STEMS` is "a prefix
match". At 9e1d6dda each entry carries a `RetiredMatch`: the holds_bytes:
entry is `Stem`, the retired key-grant SET entries are `Exact` (whole
types), so `key_grant:*:v10` / `v1beta` / `v1x` are not retired and are
served. Worded from persist's registry.rs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…e`, never a silent StoreFailed

persist 9e1d6dda runs `would_hold` on the chunk doors too, so a pull of
content held for others can be refused DiskPressureProxyRefused at the
manifest adopt, the chunk adopt (and its #977 re-adopt), the plaintext
DAG door and the whole-blob doors. Each such refusal ended as an unnamed
StoreFailed — a swallowed refusal at a secondary door. Now every store door
books persist's DiskPressureProxyRefused under the new
`blob_pull_refusals` tag `disk_pressure` (`PULL_REFUSAL_DISK_PRESSURE`,
`store_refusal_tag`) and logs it — ONCE per refused pull: the chunk walk
keeps each refused item's tag beside its message and books only the
refusal that becomes the pull's outcome (the first stop), never one per
chunk or per adopt batch; single-call doors book their one refusal. The outcome is unchanged: StoreFailed, not retried (persist: it clears
only when the host disk recovers).

Witnesses (UNBUILT here — no cargo during persist's certify; they run in
the 9e1d6dda delta gate):
- lib pull::tests::a_disk_pressure_store_refusal_is_booked_by_name — the
  mapping, and the puller's hook books exactly one `disk_pressure`.
- blob_federation_e2e::a_pull_under_disk_pressure_books_its_store_refusal_by_name
  — a real refusal: bob's puller over an engine at the Stop pressure tier,
  pulling alice's room file; the manifest adopt, then (manifest pre-held)
  the chunk adopt, each StoreFailed AND booked `disk_pressure`; the count is
  exactly 1 then exactly 2 (leg 2's walk refuses every in-flight chunk,
  one count for the pull).
Fail-before (to run with the gate): the booking removed → both red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…unks to refuse

So the once-per-pull count bites: the per-item-booking mutant (run with
the delta gate) must book more than 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…booked `withheld`, nothing filed, no repair

persist 65b1d081 (Codex r3 on #1065) refuses a `here` about bytes a
takedown or steward purge withholds (`custody_ack_here_withheld`), and
`emit_holding_claim` refuses them `Purged`. A counter-notice takedown keeps
the bytes on disk through its window, and the file row stays in our self /
family listing, so the durability pass met the blob every pass: a refused
`here` booked `report_refused` + WARN each time, and when a chunk was
missing it queued a Repair that pulled the taken-down content back.

Now the pass asks persist's own predicate
(`steward_purge::purge_withheld`, the one its doors ask) right after the
audience check: a withheld blob is booked `withheld` (new
DURABILITY_WITHHELD tag in `durability_custody`; `DurabilitySweep.withheld`)
once per pass at DEBUG, nothing is filed and no repair is queued.

Witness (UNBUILT — no cargo during persist's certify):
durability_763 `a_durability_pass_leaves_withheld_bytes_alone` — a real
counter-notice takedown (as persist's I778: the claimant is the signed
subject of the content's establishing row, files a signed takedown_notice;
the node records it), then a lost chunk: withheld == [sha], no repair, no
report filed, `withheld` booked once. Fail-before (to run): the check
removed → a repair is queued and a report filed.

Release notes: the `withheld` tag, and the `disk_pressure` pull-refusal
tag from adbd7a8 (it had no line).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
… r3 head); replace with tag v55.0.0 before merge

Lockfile-only (descends from 9e1d6dda, verified); not compiled — the r3
delta gate runs after persist's certify. Edge names none of
KeyGrantProjection (new exhaustive NotAuthor), ReackReport (new
`withheld`) or emit_holding_claim (may return Purged) directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…many_lines); the witness doc says what its fail-before shows

`withheld_in_pass` holds the purge_withheld check (durability_of_file was
116/100 lines under -D warnings, clippy ×3). The witness's fail-before
(check removed) is red because nothing is booked `withheld`; without the
check the pass ALSO files nothing and repairs nothing in this fixture,
because persist's readiness door refuses a withheld manifest and the pass
stops at "holding unreadable" — silently. The doc now says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…opt call

`dag_adopt_batch_chunks: 1` makes the refused walk span several adopt
calls (asserted: blob_dag_chunks.adopt_batches >= 2), so the exact count
tells once-per-pull booking (2) from once-per-call (7) and once-per-chunk
(7) booking. At the default batch size all six chunks fit one call and
per-call booking read 2 too. Mutants on this fixture: per-chunk RED at 7,
per-call RED at 7; the real code green at 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
… v55 candidate); replace with tag v55.0.0 before merge

Lockfile-only (descends from 65b1d081, verified); not compiled — the gate
runs after persist's certify. Edge constructs neither DurabilityDeficit nor
BlobCustody by struct literal (grep src + tests), so the new `withheld`
fields need no edit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…eld `here` books `withheld`; the guard reads persist's deficit

persist 5ee68307 refuses a `here` about bytes a takedown withholds as
`federation::Error::PurgeRefused { token: steward_purge::token::BLOB_PURGED,
body: {sha256_hex, notice_id, retryable: false} }`.

- `durability::file_custody` returns `CustodyFileError` (Retracted /
  Unreadable / Persist(federation::Error)) instead of a String;
  `withheld_notice()` matches PurgeRefused with persist's BLOB_PURGED const
  (imported, no literal).
- The pass's report booking (`book_custody_report`) books that refusal
  `withheld` at DEBUG and leaves the file (no repair) — a takedown landing
  between the guard and the report — never `report_refused` + WARN. The
  post-pull report_here logs it at DEBUG too.
- The guard reads `DurabilityDeficit.withheld`: edge's PassCustody already
  computes each file's deficit through persist's `deficit_over`, which
  fills `withheld` from `steward_purge::purge_withheld` — the same
  predicate, the same read, so the separate purge_withheld walk is gone.

Witness: durability_763
`a_withheld_here_refused_by_persist_books_withheld_not_report_refused` —
the guard bypassed (file_custody called directly), a real counter-notice
takedown: persist refuses TYPED naming the notice, and the booking reads it
`withheld` (1), no `report_refused`. Fail-before (gate): the
`withheld_notice` match removed → `report_refused`.

UNBUILT: no cargo until persist's certify finishes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
… r4 head); replace with tag v55.0.0 before merge

Lockfile-only (descends from 5ee68307, verified); not compiled — the r4
delta runs after persist's certify. r4: the re-ack sweeper's first pass
runs after HOLDING_CLAIM_REACK_START_DELAY (90 s); room-stream retention
keeps chunks a retained DAG manifest still links. Edge keeps
Engine::spawn_reack_sweeper() (via PersistGroupContentStore::
spawn_holding_claim_reack) for production; it exposes no delay knob, and no
edge test that counts claims spawns the sweeper (grep, src + tests + mesh +
tools + python), so nothing is pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
Both ciris-persist entries pin tag v55.0.0 (commit ca6bdf60, tree
b9f53dbd — identical to the a28aa640 candidate this branch was gated on).
Cargo.lock resolves git+https://github.com/CIRISAI/CIRISPersist?tag=v55.0.0
#ca6bdf6004577b4738cf3fd82d196c040f4cf7c3. pyproject's floor is already
ciris-persist>=55,<56. Supersedes this branch's interim LOCAL TEST PIN
commits (95e36ac6 … a28aa640).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…n blob_federation_e2e

`Option::is_none_or` is past the crate's MSRV (1.75; stable since 1.82):
`map_or(true, ..)`. A redundant closure in the #977 community test:
`flat_map(&recipients_of)`. No behaviour change; blob_federation_e2e 43/0.
(The gate's clippy ran --lib --profile=test, which does not lint tests/*.rs.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
Tree-identical: #865 squashed local/main-next, which this branch already carries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
…, key grants as rows, typed refusals that park or end, the P0 telemetry on main

MAJOR from v40.1.0. Pins persist v55.0.0 (through v54.0.0 / v54.1.0)
and CIRISVerify v20.1.0; leviculum v0.30.0+ciris.1 unchanged.

- Cargo.toml / Cargo.lock 40.1.0 -> 41.0.0; evidence/CIRISEdge.cc_impl.tsv
  regenerated (ciris-edge@v41.0.0; evidence_tsv_matches_emitted green).
- docs/RELEASE_NOTES.md: the v55 draft becomes v41.0.0, with sections for
  persist v54/v54.1 (#862, #858 fix 7, purged-as-withdrawn, dag-gone
  terminal), the v55 adoption items edge checked, verify 20.1, the #848
  P0 telemetry forward-port, everything else on main since v40.1.0, and
  the host-facing API changes (file_custody's CustodyFileError,
  durability_custody `withheld`, blob pull refusal `disk_pressure`,
  EdgeMetricsBundle.durable_queue_depth gone -> Edge::durable_queue_depth()).
- bridge.rs (test only): v55_ledger_refusals_follow_persists_dispositions
  pins persist v55 #753's ledger refusals: a ledger-plane
  SignerDoesNotActFor (federation_signer_not_acting_for) parks on the held
  signer, transient; ledger_l1_binding_violation is terminal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bzxmyd64qo6upHJaSLKdX8
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@emooreatx
emooreatx merged commit b89b769 into main Oct 13, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant