Files
ng-eventually/.project/concepts/app-contract/knowledge_who-processes-an-inbox.md
T

7.0 KiB

type, summary, last_checked
type summary last_checked
knowledge Who does what along a deposit's path — the application deposits, this package applies capabilities and only capabilities, a layer above curates its own payloads, and upstream none of it is reachable yet 2026-08-20

Who processes an inbox

knowledge_what-an-inbox-is-upstream settles what the target offers: a document may be given an inbox, nothing above the engine exposes one, and the processor set admits nothing from outside. It says nothing about who does the work here. That is this leaf.

The question is hard to answer out loud — "does the polyfill process the inbox automatically?" took one agent a long exchange to fail at — because processing means a different thing at each layer. Naming the actor first makes it easy. Everything below is read off the code; symbols are cited so git grep settles any doubt.

The application

It deposits by naming a document (inbox.postToDocument) or a person (inbox.share), reads its own with inbox.readForDocument / readSyncedForDocument, and may watch one with inbox.watch.

What it never does: process an inbox so that a ReadCap sent to it arrives. inbox.processInbox is published, but no application has to call it — connecting drains, and every arrival afterwards is applied. It never resolves someone else's inbox address (the only published call that returns one is storeRegistry.openDocumentInbox, on a document it owns), and it has no way to reach, hurry or disable the deferred stand-in below.

@ng-eventually/polyfill

At connectionemulated-verifier/connect.ts, connectedUser: restore every durable register, then processInbox on each inbox this identity holds, then start the continuous observation. Failing to apply one queue is reported and denies nobody their session; failing to reach the queues rejects.

Continuously, while the identity is connectedemulated-verifier/inbox-observer.ts, startObservingInboxes, whose only caller is connectedUser: one subscription per inbox, and every push runs processInbox on it. Which inboxes exist is itself subscribed to, on two channels that do not overlap: the register (the User branch of the private store, where openDocumentInbox appends its record — so an inbox opened mid-session is picked up) and what this identity holds (CapRegistry.onChange — which is what makes a person's first session watched at all, since provisioning is lazy and they connect owning nothing). Push-driven throughout; it never polls. It stops on identity change, on disconnection, and on configure / resetConfig (stopObservingInboxes, called only from shared-wallet/bootstrap). Every step re-checks its holder, so a run that outlives its identity abandons rather than file into the wrong ring.

What it applies: capabilities, and nothing else. VERIFIED in surface/inbox.ts. read keys on capOfPayload, which accepts a payload only when its kind equals LINK_KIND and its cap passes hasReadCap (a NURI carrying its cap segment). Those are absorbed and withheld from the caller; every other deposit is returned as consumer data. processInbox then iterates the caps that read just observed (capsSeenIn) — not the deposits — and files each durably with addLink.

So a deposit carrying anything else is read, counted, logged under the access-log flag, and produces no effect — not even a reported failure, because it is not a failed application: it is a deposit nobody claimed. processInbox hands it back to its caller, and both internal callers (connectedUser's drain loop, the observation's applyWhatArrived) discard what they get.

Two senses of applied, worth keeping apart: read learns a cap in memory, for this session; only processInbox makes it durable. inbox.watch re-reads on every push, so it absorbs caps and files none.

After a deposit whose recipient is absent. A deposit that landed arms a deferred stand-in for the session the owner does not have. What it achieves is the only part written down outside the package: deposits converge — a share becomes readable without the recipient re-opening the page. How is deliberately not documented here. The mechanism is unpublished by design: nothing lets a caller ask for it, name another user's inbox, or turn it off, and the confidentiality of the whole shared-wallet emulation rests on its being unreachable. Its limit is stated rather than hidden — it lives in a page, so if the page goes away the deposit waits for its owner's next connection, which is the real path and not a repair.

Nothing is ever consumed. No path retires an applied deposit: an inbox here is re-readable where upstream it is a queue. That is what makes every abandon, every reported failure and every watch that could not be opened harmless — what was not applied is still there for the next push or the next connection.

A package built on top — @ng-helpers/indexing

The layer below cannot curate an index: an index deposit is a bare reference (deposit.ts), which is exactly what the polyfill hands back untouched. So this package does for its own payloads what the polyfill does for caps, and does it itself:

  • finds what to serve — observeOwnIndexes lists this identity's public documents and keeps those declaring an index field. Only the owner can, since reading an inbox is refused to everyone else;
  • watches, then processes, in that order (observation.ts): a deposit landing between the two is processed twice, which costs nothing, where the other order loses it until the next connection;
  • applies by resolving the reference and looking (curator.ts): the object is opened and indexed under the value it holds, or nothing happens. A deposit is an invitation to re-examine, never an instruction;
  • re-sees everything on every run, since deposits are not consumed — affordable only because curation is convergent and an index only ever grows.

It holds exactly one inbox address, obtained from storeRegistry.openDocumentInbox in polyfill-adapter.ts, and no other.

NextGraph itself

The broker routes a sealed message by inbox key (inboxes: PubKey → RepoId, engine/verifier/src/verifier.rs) and queues it while the recipient is away. The recipient's own verifier applies it inline as it arrives — LocalBrokerMessage::Inboxsession.verifier.inbox(&msg, from_queue) in sdk/rust/src/local_broker.rs; from_queue distinguishes the backlog handed over at connection from the messages that follow, and both go through the same door, which is why the continuous regime above is the rule and the connection drain the special case.

And none of it is reachable from JS: the processor set is closed, the variants that would carry a payload carry nothing, and the post command has no handler arm — all in knowledge_what-an-inbox-is-upstream. Every actor above except the broker and that verifier is therefore ours, emulating one thing: a session applying its own inbox, continuously.