Skip to content

0.5.237: contact requests are visible — the people who added you, in your inbox (#818) - #819

Closed
emooreatx wants to merge 2 commits into
mainfrom
feat/incoming-contact-requests
Closed

emooreatx wants to merge 2 commits into
mainfrom
feat/incoming-contact-requests

Conversation

@emooreatx

Copy link
Copy Markdown
Contributor

Closes #818.

Gap: a contact is consent each side authors. GET /v1/contacts listed only the grants the owner signed, so being added was invisible until you added the person yourself (Eric × Francesco). The five-platform cross-platform chat stage now measures this.

Change:

  • GET /v1/contacts/requests lists the people who added the owner and whom the owner has not added back. A request is a live chat-covering consent:replication:v1 grant that:

    • names the owner's fedID or this node's keys;
    • is signed by someone else, resolved to a person (never infrastructure, never the owner's own devices);
    • is not withdrawn, recanted or expired;
    • is unanswered.

    It returns one entry per person, newest first, each with accept: POST /v1/contacts {key_id}.

  • GET /v1/self/invites gains contact_requests with the same entries, beside the existing family, community, affiliation and pair-room invites. That makes it one inbox for every kind of invitation.

  • One shared rule: "already a contact" is now a single function (contact_persons), shared with GET /v1/contacts, so the two lists cannot disagree.

  • Bounded read: persist's AttestationQuery::to(key).dimension(consent:replication:v1), not an every-row scan.

  • FSD: FSD/MEMBERSHIP_INVITES.md §8 specifies it, and names what is not done yet: declining, delivery held up by a transport fault (CIRISEdge#888), requests addressed to another device, and the opener-only pair invite.

Tests:

  • contacts_chat: a request lasts until you add back, and a withdrawn request is not offered. Both filters are mutation-checked.
  • community_crud, on a two-node mesh: Alice's request reaches Bob's own inbox, hers shows nothing, and Bob adding back clears it.
  • Locally green: lib (634), contacts_chat (38), community_crud (15), every gate, and clippy with -D warnings.

Opens 0.5.237; the evidence TSV is bumped.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QriZxPx5aJvHXCGaTZmvKF

…your inbox (#818)

A contact is consent each side authors: a live consent:replication:v1 grant
covering chat:. GET /v1/contacts listed only the grants the owner signed, and
nothing listed the grants other people signed naming the owner. So being added
was invisible until you added the person yourself, and both people had to add
each other blind (Eric x Francesco).

- contacts_chat::incoming_contact_requests lists live grants that:
  - name the owner's fedID or this node's keys;
  - are signed by someone else, resolved to a PERSON through edge's contact
    resolver (never infrastructure, never the owner's devices);
  - cover chat:;
  - are not withdrawn or recanted by their signer, and not expired;
  - are unanswered (the person is not already a contact).
  It returns one entry per person, newest first, each carrying its `accept`:
  POST /v1/contacts {key_id}. The read is persist's bounded
  AttestationQuery::to(key).dimension(consent:replication:v1).
- "Already a contact" is now one function, contacts_chat::contact_persons,
  extracted from GET /v1/contacts so the two lists cannot disagree.
- GET /v1/contacts/requests (the owner; a delegate may read).
- GET /v1/self/invites gains `contact_requests` beside the family, community,
  affiliation and pair-room `invites`, so there is one inbox.
- FSD/MEMBERSHIP_INVITES.md section 8 specifies it, and names what is not yet
  done: declining, link-dependent delivery (CIRISEdge#888), requests addressed
  to another device, and the opener-only pair invite.

Tests:
- tests/contacts_chat.rs: a request until you add back, and a withdrawn
  request is not offered. Both filters are mutation-checked.
- tests/community_crud.rs: on a two-node mesh, Alice's request reaches Bob's
  own inbox, hers shows nothing, and Bob adding back clears it.

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

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-25T20:38:36.116097Z
We couldn’t complete this review. Comment @codex review to try again.
073b38a PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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

Copy link
Copy Markdown
Contributor Author

Carried in #820 (the 0.5.237 live-chat release PR, now targeting main). This closes when #820 merges.

emooreatx added a commit that referenced this pull request Oct 26, 2026
…819, #821 carried) (#820)

* 0.5.237: contact requests are visible — the people who added you, in your inbox (#818)

A contact is consent each side authors: a live consent:replication:v1 grant
covering chat:. GET /v1/contacts listed only the grants the owner signed, and
nothing listed the grants other people signed naming the owner. So being added
was invisible until you added the person yourself, and both people had to add
each other blind (Eric x Francesco).

- contacts_chat::incoming_contact_requests lists live grants that:
  - name the owner's fedID or this node's keys;
  - are signed by someone else, resolved to a PERSON through edge's contact
    resolver (never infrastructure, never the owner's devices);
  - cover chat:;
  - are not withdrawn or recanted by their signer, and not expired;
  - are unanswered (the person is not already a contact).
  It returns one entry per person, newest first, each carrying its `accept`:
  POST /v1/contacts {key_id}. The read is persist's bounded
  AttestationQuery::to(key).dimension(consent:replication:v1).
- "Already a contact" is now one function, contacts_chat::contact_persons,
  extracted from GET /v1/contacts so the two lists cannot disagree.
- GET /v1/contacts/requests (the owner; a delegate may read).
- GET /v1/self/invites gains `contact_requests` beside the family, community,
  affiliation and pair-room `invites`, so there is one inbox.
- FSD/MEMBERSHIP_INVITES.md section 8 specifies it, and names what is not yet
  done: declining, link-dependent delivery (CIRISEdge#888), requests addressed
  to another device, and the opener-only pair invite.

Tests:
- tests/contacts_chat.rs: a request until you add back, and a withdrawn
  request is not offered. Both filters are mutation-checked.
- tests/community_crud.rs: on a two-node mesh, Alice's request reaches Bob's
  own inbox, hers shows nothing, and Bob adding back clears it.

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

* contact requests: sort_by_key (clippy unnecessary_sort_by)

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

* 0.5.237: ignore a contact request, privately and reversibly (FSD §8.1)

Per CC's read (CC 5.3.2.4.1). The steward's ruling on the name is pending.

- POST /v1/contacts/requests/{grant_attestation_id}/ignore writes the owner's
  local-tier `scores` row, dimension contact:request:ignored:v1, with
  references_attestation_id pointing at the request grant, no subject and
  cohort self. It goes through attestation_insert_local, unsigned, and is never
  promoted, so the requester learns nothing. A request that is not pending is a
  404 contacts.request_not_found.
- DELETE on the same path un-ignores: a local `withdraws` under the same
  dimension citing the ignore. Un-ignoring something not ignored is a 404
  contacts.request_not_ignored.
- A delegate may not answer: 403 contacts.delegate_may_not_answer.
- GET /v1/contacts/requests adds `ignored` (entries plus ignore_id). Each
  request carries `ignore: {method, path}` beside `accept`. The /v1/self/invites
  inbox lists pending only.
- Decline, where the requester learns, waits on persist's addressed placement
  (CIRISPersist#1121, after v60). It is not shipped at a wider audience in the
  meantime (FSD section 8.2).

Tests:
- ignoring hides the request privately: the row is local tier, not federation;
  un-ignoring restores it, and the fold is mutation-checked;
- ignoring something that is not a request is refused.

The three new refusal ids are queued in KNOWN_UNLOCALIZED; MAX_UNCOVERED goes
from 30 to 33.

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

* contact requests: requester_label, the grant's valid_until window, and `mutual` on contacts (CSD-109 review)

From the client's CSD-109 sign-off questions:
- requester_label: a fedID key is <label>-<fingerprint>, and the label is the
  person's own choice at mint. It is stated (their claim, never verified) so no
  client parses key ids. It is null for keys not in that form.
- A request lasts exactly as long as its grant. Besides withdrawal and row
  expiry, the grant's own valid_until policy window now ends it too
  (mutation-checked).
- GET /v1/contacts rows gain `mutual`: whether that person has added the owner
  back. The requester sees false (pending) until the other side accepts, then
  true. It is read with the same inbound reader the requests use, so the two
  agree.
- Refactor: inbound_chat_grants (every person who added the owner) feeds both
  the requests (minus contacts) and `mutual`.

Tests:
- a request lapses with its grant's window;
- the two-node mesh test now asserts requester_label, and the requester's
  mutual going false then true.

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

* contacts: a stalled key fetch says so — and the Pull is re-queued until the key lands

The five-platform cross-chat stage (2026-10-25): the linux node pasted the
Android owner's fedcode v3, got contacts.awaiting_key_body, and the key never
arrived. The canonical held the stranger's key:record:v1 row at federation tier
and both nodes held each other's trust-root acceptance, but every hop that
could explain the stall logs at DEBUG (edge's "recovery round: asked this
peer", the responder's "identifier lookup refused: no mutual trust root"), so
neither node's log said anything.

- The pending-contacts pass now re-queues the Key Pull every pass. Edge's
  recovery queue removes a name when its Pull is sent and nothing re-adds it
  if the reply is lost, so one dropped answer stranded the contact until the
  person pasted again.
- After 60 s it WARNs "KEY FETCH STALLED" (again every 5 min) with what this
  node knows: wait time, asks, whether the runtime exists, edge's recovery
  queue depth, peers in no-route backoff, and the five places to look next.
- COMPLETED now logs waited_secs and asked. The pending clock keeps the first
  paste, so waited_secs is the person's real wait.
- tests/contact_key_pull_e2e.rs drives the production path in process
  (request_key_body, send_recovery_pull, the responder's subject_holdings,
  apply) and proves the canonical answers: Summary(1), Diff, Deliver,
  admitted 1. The fault is upstream of the round, on the asking node.

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

* 0.5.237 headline: the key re-ask and the STALLED line (#821)

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

* log citation: the STALLED line's CIRISEdge#634 is history (closed)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@emooreatx

Copy link
Copy Markdown
Contributor Author

Carried by #820 (merged as 185790c), released in 0.5.237.

@emooreatx emooreatx closed this Oct 26, 2026
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.

Contact requests are invisible: no reader for grants others signed naming you

1 participant