Model multi-occurrence self to multi-occurrence self sessions - #512
Model multi-occurrence self to multi-occurrence self sessions#512emooreatx wants to merge 1 commit into
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
f84a6c2 to
a54867b
Compare
A self is a fed identity plus the N nodes it stewards. An attestation addressed
to a fed id fans out to every one of them, because a fed id has no transport path
of its own. Exactly one occurrence may act on it.
It is a NAT table, not a leader election. A self is legitimately doing several
things at once on different nodes: two agents on their own nodes, me chatting on
a third, me on video on a fourth — four occurrences, four live sessions, two
communities, concurrently. So the claim is keyed by (community, session) and
resolves to the occurrence handling THAT session. "Which node is my self?" has no
answer; "which node is handling session z in community B?" does.
Two primitives: claim a thing, and clear a claim.
An unclaimed attestation is never acted on — not by a quorum, not by the lowest
id, and not by a single-node self. Being the only occurrence confers no authority
to act; it means there is one place where nobody is home. Attendance has no
"unattended" variant, so there is no value that could be promoted into a right to
act.
The agent's MODEL ports and its MECHANISM does not. Its occurrences share one
database ("enables multiple instances against same database"), so its claim is an
atomic DB operation. Mesh occurrences each hold their own persist DB, so there is
no compare-and-swap across them. Porting transfer_task_ownership verbatim would
import a race already latent upstream — it reads, checks __shared__, then upserts,
and two occurrences can both read, both pass, and both write. In one process that
window is narrow; under replication lag it is the normal case.
So concurrent claims are expected rather than prevented, and resolve() settles
them with a rule every occurrence computes identically: earliest claim wins, ties
on the lowest occurrence id. Convergent without coordination. Handling must still
be idempotent per attestation id.
The table replicates on two paths. Within a self, siblings learn claims on the
CEG self tier, which moves only by subject-initiated pull. Between selves, the
peer learns ownership from a community-scoped claim under the directed-consent
grant — which must carry SESSION_ATTESTATION_PREFIX or every claim replicates to
nobody, silently, exactly as chat rows did when a grant covered capacity:/trace:
only and the people you knew best were the ones you could not message.
Model and tests only; no callers yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gxnxsTWS8FkN3CT2JEz86
a54867b to
c7d3e0b
Compare
|
Closing unmerged in favour of CIRISAI/CIRISPersist#782, where this belongs. Checking which layer actually owns the primitives answered the question decisively — persist already has all three, and needs no new ones:
The decisive detail: It also found a place where persist is simply better than what I wrote here: And the merge rule — earliest claim wins, ties on lowest occurrence id — is the same class of fold persist already does for Merging a server-side home would have created exactly the defect I filed against edge this morning (CIRISEdge#548): two crates implementing one rule, where the copy forks silently and the test pinning it keeps passing. Doing that knowingly, hours later, in the same arc, would be hard to defend. Nothing is lost — this had no callers, which made now the cheapest possible moment to move it. The branch stays; persist#782 references commit Server keeps the half that is genuinely its own: attendance. "A human is present here" is not a storage fact and persist cannot know it — this node observes it and asserts it. |
Model + tests only. No callers yet — this is the shape we agreed on before anything is wired to it.
The model
A self is a fed identity plus the N nodes it stewards — its occurrences. An attestation addressed to a fed id fans out to every one of them, because a fed id has no transport path of its own (#510 /
key_convention). Exactly one occurrence may act on it.It is a NAT table, not a leader election. A self is legitimately doing several things at once on different nodes:
So the claim is keyed by
(community, session)and resolves to the occurrence handling that session. "Which node is my self?" has no answer; "which node is handling session z in community B?" does.Two primitives: claim a thing, and clear a claim.
An unclaimed attestation is never acted on
Not by a quorum, not by the lowest id, and not by a single-node self. Being the only occurrence confers no authority to act — it means there is one place where nobody is home.
Attendancehas onlyHumanandAgent. There is deliberately no "unattended" variant: an unattended occurrence is absent from the table rather than present with a weak claim, so no value exists that a careless comparison could promote into a right to act.The agent's model ports; its mechanism does not
CIRISAgent solved this inside one runtime, and the model is the one to take over: an owning
agent_occurrence_idon every row, a reserved__shared__partition for unclaimed work, andtransfer_task_ownershipto take it.The mechanism does not port. Those occurrences share one database — the config field says so outright, "enables multiple instances against same database" — so the claim is an atomic DB operation. Mesh occurrences each hold their own persist DB and reconcile by replication; there is no compare-and-swap across them.
Porting the transfer verbatim would also import a race already latent upstream:
transfer_task_ownershipreads the row, checks it still says__shared__, then upserts — two occurrences can both read, both pass, and both write. In one process that window is narrow; under replication lag it is the normal case.So concurrent claims are treated as expected rather than prevented, and
resolve()settles them with a rule every occurrence computes identically: earliest claim wins, ties on the lowest occurrence id. Convergent without coordination. Handling must still be idempotent per attestation id — determinism only bites once views agree, and lag means they transiently do not.Replication: two paths, not one
projection_formaps self/family toProjection::SelfOwn, advertised by nobody, so those rows move only by subject-initiated pull (pull_subject_testimony, fail-closed to the subject). My occurrences qualify; a peer never does.contacts_chatrecords what happened when one was missing: a peer already federated with held a grant coveringcapacity:/trace:only, sochat:rows stayed ineligible and "the people you know best were exactly the people you could not message" — with every call reporting success. A claim onSESSION_ATTESTATION_PREFIXnot in the grant replicates to nobody, silently, and the symptom is not an error but a peer who appears to be ignoring you. Any surface emitting claims must add the prefix throughpeer::ensure_contact_consent_covers, never assume it.SESSION_CLAIM_DIMENSION = "session:claim:v1"is versioned (persist refusesMissingVersionSegment) and deliberately not underchat:— claims govern any addressed session, so scoping them to chat would leave video, moderation, and claim flows with no ownership signal.Tests (8)
claimed_atNot in this PR
CLAIM_TTL_SECS = 900) is a first guess, deliberately generous: reclaiming from a live-but-quiet occurrence yields two handlers for one session, which is worse than a late reply.🤖 Generated with Claude Code
https://claude.ai/code/session_017gxnxsTWS8FkN3CT2JEz86