Parent: #45
Blocked by: P4 above
Implement core/data upstream (wire-mesh#34 territory for the shard-distribution piece) rather than accept a per-agent local-history regression. The larger job, but unlocks mailboxes, offline delivery, and the handle registry as side effects, not just message history.
Investigated (2026-09-14), confirming this genuinely is "the biggest unknown" rather than overstated -- three real, largely independent pieces of missing work, not one. Checked directly against the current code, not assumed:
- Content format (mostly designed, partly built):
room-notice (spec/room.cddl's noticeboard extension, Section C of this plan) is exactly the right envelope for a catch-up-able message -- self-certifying, embeds the poster's own room:member token, already covers the cross-author-ordering tiebreak and posting-time-valid revocation semantics this needs. ts/packages/core/src/domain/room.ts (wire-mesh#126, merged) implements verifyRoomNotice/compareRoomNotices -- but read-side only. There is no createRoomNotice/mint function anywhere; nothing in either language can actually produce a signed room-notice yet.
- Sync protocol (spec-only, zero implementation):
core/data's three frames (data-have/data-request/data-entries, spec/data-domain.cddl) have generated Zod types from CDDL codegen but no domain module implements any of them -- no oplog storage abstraction, no gap-rejection logic (data-entries-frame's own documented "reject a frame whose from-seq would leave a gap" obligation), no send/receive wiring, in either wire-mesh or agent-comms. This is the actual "biggest unknown" -- a real peer-to-peer log-sync protocol, not a small addition.
- agent-comms' own integration (not started):
messages/dms are still plain in-memory Maps populated by the legacy broadcastPatch/full-replication mechanism (per P3.5's own remaining note). Migrating onto (1)+(2) means: each sender's own outgoing room.send optionally also gets minted as a room-notice and appended to that sender's own oplog (Section C's already-designed "delivery and durable catch-up are the same artifact, opt-in per message" pattern); a new local persistence layer for each peer's own oplog-per-device; and the actual data-have/data-request/data-entries exchange on reconnect/join to backfill what was missed while offline.
Recommended sequencing, not yet started on any piece: (1) mint-side createRoomNotice in wire-mesh-core is small, self-contained, and unblocks nothing else being blocked on it -- reasonable first PR. (2) core/data's sync protocol is the real multi-week core of this phase and should get its own dedicated design pass (mirroring the rigor Sections A-C already got) before implementation starts, given its scope: peers' own log storage/persistence port, the have/request/entries state machine, gap handling, and how it composes with discovery.cddl's mailbox fan-out for offline durability (already designed in Section C, not yet built either). (3) agent-comms' own integration is the last piece, genuinely blocked on (1) and (2) existing first.
Piece 2 (the sync protocol itself, "the actual biggest unknown") implemented (2026-09-14, wire-mesh#138): appendOwnEntry, handleDataHave, handleDataRequest, handleDataEntries. Built directly on the existing KeyValueStorage port (which already named "data-domain oplog entries" as an intended use in its own comment -- no new storage abstraction needed, only a small type-precision fix to the port's own declared byte-value types). Gap-rejection (data-domain.cddl's own documented obligation) is enforced exactly: a data-entries response whose from-seq doesn't match this store's own current head is refused, covering both a real gap and a stale/replayed response identically. Deliberately scoped to the primitive alone -- proactive broadcast timing, which peers' logs to track, and fan-out across several candidate holders (mailboxes, other replicas) all stay out of scope as application policy, matching this codebase's own established primitive-vs-policy split (mesh-session.ts's frame handling vs. room-protocol.ts's dispatch).
Two of three pieces now done (mint-side createRoomNotice, wire-mesh#137; the sync protocol, wire-mesh#138). Remaining, the last piece: agent-comms' own integration -- replacing its in-memory messages/dms Maps and the legacy broadcastPatch-based replication with (a) each sender's own outgoing message optionally also minted as a room-notice and appended via appendOwnEntry, (b) wiring handleDataHave/handleDataRequest/handleDataEntries into agent-comms' own WireMeshTransport/session frame handling, and (c) an actual catch-up policy (when to send data-have, which peers' logs to track) -- this last piece is real application-level design work of its own, not yet started.
Durable Node storage unblocked (2026-09-14, wire-mesh#139): createNodeFsStorage. No Node filesystem-backed KeyValueStorage adapter existed anywhere -- only in-memory (tests) and browser IndexedDB -- despite the port itself naming "data-domain oplog entries" as an intended use. Mirrors indexeddb-storage.ts's own contract exactly: every key is treated as a relative path (creating nested directories as needed), matching how every key this codebase already constructs (internally generated, ASCII, path-shaped). This is what agent-comms' own bridges (real Node processes) would actually persist their oplog with.
All three primitives are now built and merged: createRoomNotice (#137), the sync protocol (#138), durable Node storage (#139). The only remaining piece of P5 is agent-comms' own integration -- wiring these into WireMeshTransport's frame handling and room-lifecycle.ts's own message-send path, plus the catch-up policy (when to send data-have, which peers' logs to track). Deliberately not started in this pass: wire-mesh-transport.ts is large (778 lines) and under active, ongoing migration (P3.8/P4 work landing PRs in the same session), and this integration is real application-level design work that deserves its own dedicated pass rather than being bolted on opportunistically alongside three separately-scoped primitive PRs.
PR #133 merged (2026-09-16): agent-comms' own wire-mesh-core pin caught up from 1.13.0 to 1.30.0. This was a real, previously-unnoticed blocker for this phase specifically: createRoomNotice, the core/data sync protocol (data-have/data-request/data-entries), and createNodeFsStorage -- all three primitives the prior update above named as "done and merged" -- only existed upstream in wire-mesh, not in what agent-comms actually depended on. One real breaking change fixed along the way (RevocationCheck's isRevoked -> entriesFor contract change, wire-mesh#84) and a trilean-regex packaging workaround added (pnpm-workspace.yaml override, mirroring the same fix wire-mesh's own repo already carries for ExaDev/trilean#46).
agent-comms can now actually import and use createRoomNotice/appendOwnEntry/handleDataHave/handleDataRequest/handleDataEntries/createNodeFsStorage. The remaining piece is unchanged from the last update: agent-comms' own integration -- wiring these into WireMeshTransport's frame handling and room-lifecycle.ts's own message-send path, plus the catch-up policy (when to send data-have, which peers' logs to track). Still real application-level design work of its own, not started here.
A real publishing bug found and fixed at its source (2026-09-16): wire-mesh-core#140, merged. Trying to actually consume createRoomNotice/appendOwnEntry/handleDataHave/handleDataRequest/handleDataEntries/createNodeFsStorage from agent-comms surfaced that domain/data-sync.ts and adapters/node-fs-storage.ts were never added to wire-mesh-core's own tsdown.config.ts entry array, despite merging upstream weeks ago -- neither module was ever bundled or exported, so both imports failed to resolve at all. Fixed at the source in ExaDev/wire-mesh (not worked around locally), along with tsconfig.json's own drifted include list (nine modules correctly published but never directly typechecked). agent-comms bumped again to 1.30.1 (agent-comms#134, merged) to pick up the fix -- confirmed directly via require.resolve against both previously-broken import paths.
agent-comms can now genuinely import and use every P5 primitive. The remaining piece is unchanged: agent-comms' own integration (sender-side room-notice minting, wiring the sync protocol into WireMeshTransport's frame handling, and a real catch-up policy) -- still real application-level design work, not started here.
PR #135 merged (2026-09-16): the mint-and-append half of piece (a) is real code now. recordRoomSendNotice (src/core/room-notice-log.ts) mints a room-notice via createRoomNotice, CBOR-encodes it (cbor2's deterministic-encoding preset, matching handshake.ts's own established convention), and appends it via appendOwnEntry. Tested for the property that actually matters -- round-trip fidelity, decoding the stored bytes back and running them through a real verifyRoomNotice -- with the test's own ability to fail confirmed directly (a corrupted byte was injected, watched fail for the right reason, then reverted) before trusting the green run. identity-store.ts gained oplogDirFor(slot), a sibling directory of the identity file for whoever wires this up to back the KeyValueStorage dependency with createNodeFsStorage.
Still not wired into any send path. recordRoomSendNotice is a standalone, fully-tested primitive a caller can invoke -- nothing in room-messaging.ts/sendRoomMessage calls it yet, and no KeyValueStorage instance is constructed anywhere in MeshStore's own wiring. The three genuinely open pieces, unchanged in kind from every earlier update: (b) wiring handleDataHave/handleDataRequest/handleDataEntries into WireMeshTransport's frame handling so peers can actually exchange sync frames, (c) an opt-in mechanism on sendRoomMessage's own call sites (a new parameter? a separate explicit tool action?) deciding per-message whether to also call recordRoomSendNotice, and (d) the catch-up policy itself (when to send data-have, which peers' logs to track, where the oplog's KeyValueStorage instance actually lives in MeshStore's construction). Each is a real, separable design decision, not a mechanical wiring step.
PR #136 merged (2026-09-16): piece (b), the sync-protocol frame handling, is real and wired. WireMeshTransport gained an optional dataStorage constructor parameter; when set, every session's own frame stream is observed via acceptMeshSession's onFrame hook (wire-mesh#102) and answers a data-have with a data-request, a data-request with a data-entries, and applies an incoming data-entries to local storage. sendDataFrame(peerDeviceHex, frame) is the matching mechanical send primitive. A new connectionsByPeer map tracks each peer's own raw Connection (neither AcceptedMeshSession nor MeshSession exposes a generic "send an arbitrary frame" method), registered unconditionally but only acted on once peerSessions actually trusts that device -- an unapproved stranger's data-domain frames are observed, never acted on. Tested against two real WireMeshTransport instances proving the full three-hop exchange lands an entry in the peer's own storage.
Remaining for P5, narrowed to two pieces now: (c) an opt-in mechanism on sendRoomMessage's own call sites deciding per-message whether to also call recordRoomSendNotice (#135), and (d) the catch-up policy itself -- when to call sendDataFrame with an initial data-have after a recordRoomSendNotice append, which peers' logs to track, and where MeshStore's own dataStorage/KeyValueStorage instance actually gets constructed (via createNodeFsStorage(oplogDirFor(slot))) and wired into bridge-mesh.ts's new WireMeshTransport(...) call. Both are real product-level design decisions -- not mechanical wiring -- and are what's left before P5 is fully integrated end to end.
PR #137 merged (2026-09-16): the opt-in send-path mechanism and storage wiring are both real now. sendRoomMessage gained an optional durable parameter; when true, it also calls recordRoomSendNotice (#135) after the existing fan-out. MeshStoreIdentity gained a dataStorage field -- the same KeyValueStorage instance WireMeshTransport's own frame responder (#136) reads and writes -- and bridge-mesh.ts constructs it once via createNodeFsStorage(oplogDirFor(slot)), wiring the identical instance to both the transport and the store's identity. Every existing fake MeshStoreIdentity fixture across the test suite (12 files) needed the new required field added; the real behavior itself is tested end to end with real identities and a real minted token, decoding the stored bytes back out and running them through a real verifyRoomNotice.
P5 is now fully wired end to end. A caller can mint a room-notice on send (durable: true), and a peer's data-request against this device's log will find it, since both paths now share one real, disk-backed oplog per identity slot. What remains is genuinely optional, forward-looking policy, not a missing mechanism: proactively calling sendDataFrame with an initial data-have right after a durable append (today, a peer only catches up if it already knows to ask), and deciding which peers' logs to track by default. Neither blocks P5's own core deliverable -- the primitives, the wire responder, and the opt-in send path are all real, tested, and merged.
Closing P5. All three upstream primitives (createRoomNotice, the core/data sync protocol, createNodeFsStorage) are built and actually published (the wire-mesh#140 gap that blocked consuming them is fixed); agent-comms' own integration is real and merged: recordRoomSendNotice mints and appends, WireMeshTransport's frame responder answers a real data-have/data-request/data-entries exchange, and sendRoomMessage's opt-in durable flag wires the two together with one shared, disk-backed oplog per identity slot. This was genuinely "the biggest unknown" of the migration, and it's done.
Proactive catch-up (sending an initial data-have automatically after a durable append) and peer-tracking policy are real, tracked follow-up work, not part of this phase's own core scope -- filed as their own concrete piece if/when a caller actually needs automatic catch-up rather than on-demand.
Parent: #45
Blocked by: P4 above
Implement core/data upstream (wire-mesh#34 territory for the shard-distribution piece) rather than accept a per-agent local-history regression. The larger job, but unlocks mailboxes, offline delivery, and the handle registry as side effects, not just message history.
Investigated (2026-09-14), confirming this genuinely is "the biggest unknown" rather than overstated -- three real, largely independent pieces of missing work, not one. Checked directly against the current code, not assumed:
room-notice(spec/room.cddl's noticeboard extension, Section C of this plan) is exactly the right envelope for a catch-up-able message -- self-certifying, embeds the poster's ownroom:membertoken, already covers the cross-author-ordering tiebreak and posting-time-valid revocation semantics this needs.ts/packages/core/src/domain/room.ts(wire-mesh#126, merged) implementsverifyRoomNotice/compareRoomNotices-- but read-side only. There is nocreateRoomNotice/mint function anywhere; nothing in either language can actually produce a signed room-notice yet.core/data's three frames (data-have/data-request/data-entries,spec/data-domain.cddl) have generated Zod types from CDDL codegen but no domain module implements any of them -- no oplog storage abstraction, no gap-rejection logic (data-entries-frame's own documented "reject a frame whose from-seq would leave a gap" obligation), no send/receive wiring, in either wire-mesh or agent-comms. This is the actual "biggest unknown" -- a real peer-to-peer log-sync protocol, not a small addition.messages/dmsare still plain in-memory Maps populated by the legacybroadcastPatch/full-replication mechanism (per P3.5's own remaining note). Migrating onto (1)+(2) means: each sender's own outgoingroom.sendoptionally also gets minted as aroom-noticeand appended to that sender's own oplog (Section C's already-designed "delivery and durable catch-up are the same artifact, opt-in per message" pattern); a new local persistence layer for each peer's own oplog-per-device; and the actualdata-have/data-request/data-entriesexchange on reconnect/join to backfill what was missed while offline.Recommended sequencing, not yet started on any piece: (1) mint-side
createRoomNoticein wire-mesh-core is small, self-contained, and unblocks nothing else being blocked on it -- reasonable first PR. (2)core/data's sync protocol is the real multi-week core of this phase and should get its own dedicated design pass (mirroring the rigor Sections A-C already got) before implementation starts, given its scope: peers' own log storage/persistence port, the have/request/entries state machine, gap handling, and how it composes withdiscovery.cddl's mailbox fan-out for offline durability (already designed in Section C, not yet built either). (3) agent-comms' own integration is the last piece, genuinely blocked on (1) and (2) existing first.Piece 2 (the sync protocol itself, "the actual biggest unknown") implemented (2026-09-14, wire-mesh#138): appendOwnEntry, handleDataHave, handleDataRequest, handleDataEntries. Built directly on the existing
KeyValueStorageport (which already named "data-domain oplog entries" as an intended use in its own comment -- no new storage abstraction needed, only a small type-precision fix to the port's own declared byte-value types). Gap-rejection (data-domain.cddl's own documented obligation) is enforced exactly: adata-entriesresponse whosefrom-seqdoesn't match this store's own current head is refused, covering both a real gap and a stale/replayed response identically. Deliberately scoped to the primitive alone -- proactive broadcast timing, which peers' logs to track, and fan-out across several candidate holders (mailboxes, other replicas) all stay out of scope as application policy, matching this codebase's own established primitive-vs-policy split (mesh-session.ts's frame handling vs. room-protocol.ts's dispatch).Two of three pieces now done (mint-side
createRoomNotice, wire-mesh#137; the sync protocol, wire-mesh#138). Remaining, the last piece: agent-comms' own integration -- replacing its in-memorymessages/dmsMaps and the legacybroadcastPatch-based replication with (a) each sender's own outgoing message optionally also minted as aroom-noticeand appended viaappendOwnEntry, (b) wiringhandleDataHave/handleDataRequest/handleDataEntriesinto agent-comms' ownWireMeshTransport/session frame handling, and (c) an actual catch-up policy (when to senddata-have, which peers' logs to track) -- this last piece is real application-level design work of its own, not yet started.Durable Node storage unblocked (2026-09-14, wire-mesh#139): createNodeFsStorage. No Node filesystem-backed KeyValueStorage adapter existed anywhere -- only in-memory (tests) and browser IndexedDB -- despite the port itself naming "data-domain oplog entries" as an intended use. Mirrors indexeddb-storage.ts's own contract exactly: every key is treated as a relative path (creating nested directories as needed), matching how every key this codebase already constructs (internally generated, ASCII, path-shaped). This is what agent-comms' own bridges (real Node processes) would actually persist their oplog with.
All three primitives are now built and merged:
createRoomNotice(#137), the sync protocol (#138), durable Node storage (#139). The only remaining piece of P5 is agent-comms' own integration -- wiring these intoWireMeshTransport's frame handling androom-lifecycle.ts's own message-send path, plus the catch-up policy (when to send data-have, which peers' logs to track). Deliberately not started in this pass:wire-mesh-transport.tsis large (778 lines) and under active, ongoing migration (P3.8/P4 work landing PRs in the same session), and this integration is real application-level design work that deserves its own dedicated pass rather than being bolted on opportunistically alongside three separately-scoped primitive PRs.PR #133 merged (2026-09-16): agent-comms' own wire-mesh-core pin caught up from 1.13.0 to 1.30.0. This was a real, previously-unnoticed blocker for this phase specifically:
createRoomNotice, thecore/datasync protocol (data-have/data-request/data-entries), andcreateNodeFsStorage-- all three primitives the prior update above named as "done and merged" -- only existed upstream in wire-mesh, not in what agent-comms actually depended on. One real breaking change fixed along the way (RevocationCheck'sisRevoked->entriesForcontract change, wire-mesh#84) and atrilean-regexpackaging workaround added (pnpm-workspace.yamloverride, mirroring the same fix wire-mesh's own repo already carries forExaDev/trilean#46).agent-comms can now actually import and use
createRoomNotice/appendOwnEntry/handleDataHave/handleDataRequest/handleDataEntries/createNodeFsStorage. The remaining piece is unchanged from the last update: agent-comms' own integration -- wiring these intoWireMeshTransport's frame handling androom-lifecycle.ts's own message-send path, plus the catch-up policy (when to senddata-have, which peers' logs to track). Still real application-level design work of its own, not started here.A real publishing bug found and fixed at its source (2026-09-16): wire-mesh-core#140, merged. Trying to actually consume
createRoomNotice/appendOwnEntry/handleDataHave/handleDataRequest/handleDataEntries/createNodeFsStoragefrom agent-comms surfaced thatdomain/data-sync.tsandadapters/node-fs-storage.tswere never added to wire-mesh-core's owntsdown.config.tsentry array, despite merging upstream weeks ago -- neither module was ever bundled or exported, so both imports failed to resolve at all. Fixed at the source inExaDev/wire-mesh(not worked around locally), along withtsconfig.json's own drifted include list (nine modules correctly published but never directly typechecked). agent-comms bumped again to 1.30.1 (agent-comms#134, merged) to pick up the fix -- confirmed directly viarequire.resolveagainst both previously-broken import paths.agent-comms can now genuinely import and use every P5 primitive. The remaining piece is unchanged: agent-comms' own integration (sender-side room-notice minting, wiring the sync protocol into
WireMeshTransport's frame handling, and a real catch-up policy) -- still real application-level design work, not started here.PR #135 merged (2026-09-16): the mint-and-append half of piece (a) is real code now.
recordRoomSendNotice(src/core/room-notice-log.ts) mints a room-notice viacreateRoomNotice, CBOR-encodes it (cbor2's deterministic-encoding preset, matchinghandshake.ts's own established convention), and appends it viaappendOwnEntry. Tested for the property that actually matters -- round-trip fidelity, decoding the stored bytes back and running them through a realverifyRoomNotice-- with the test's own ability to fail confirmed directly (a corrupted byte was injected, watched fail for the right reason, then reverted) before trusting the green run.identity-store.tsgainedoplogDirFor(slot), a sibling directory of the identity file for whoever wires this up to back theKeyValueStoragedependency withcreateNodeFsStorage.Still not wired into any send path.
recordRoomSendNoticeis a standalone, fully-tested primitive a caller can invoke -- nothing inroom-messaging.ts/sendRoomMessagecalls it yet, and noKeyValueStorageinstance is constructed anywhere inMeshStore's own wiring. The three genuinely open pieces, unchanged in kind from every earlier update: (b) wiringhandleDataHave/handleDataRequest/handleDataEntriesintoWireMeshTransport's frame handling so peers can actually exchange sync frames, (c) an opt-in mechanism onsendRoomMessage's own call sites (a new parameter? a separate explicit tool action?) deciding per-message whether to also callrecordRoomSendNotice, and (d) the catch-up policy itself (when to senddata-have, which peers' logs to track, where the oplog'sKeyValueStorageinstance actually lives inMeshStore's construction). Each is a real, separable design decision, not a mechanical wiring step.PR #136 merged (2026-09-16): piece (b), the sync-protocol frame handling, is real and wired.
WireMeshTransportgained an optionaldataStorageconstructor parameter; when set, every session's own frame stream is observed viaacceptMeshSession'sonFramehook (wire-mesh#102) and answers adata-havewith adata-request, adata-requestwith adata-entries, and applies an incomingdata-entriesto local storage.sendDataFrame(peerDeviceHex, frame)is the matching mechanical send primitive. A newconnectionsByPeermap tracks each peer's own rawConnection(neitherAcceptedMeshSessionnorMeshSessionexposes a generic "send an arbitrary frame" method), registered unconditionally but only acted on oncepeerSessionsactually trusts that device -- an unapproved stranger's data-domain frames are observed, never acted on. Tested against two realWireMeshTransportinstances proving the full three-hop exchange lands an entry in the peer's own storage.Remaining for P5, narrowed to two pieces now: (c) an opt-in mechanism on
sendRoomMessage's own call sites deciding per-message whether to also callrecordRoomSendNotice(#135), and (d) the catch-up policy itself -- when to callsendDataFramewith an initialdata-haveafter arecordRoomSendNoticeappend, which peers' logs to track, and whereMeshStore's owndataStorage/KeyValueStorageinstance actually gets constructed (viacreateNodeFsStorage(oplogDirFor(slot))) and wired intobridge-mesh.ts'snew WireMeshTransport(...)call. Both are real product-level design decisions -- not mechanical wiring -- and are what's left before P5 is fully integrated end to end.PR #137 merged (2026-09-16): the opt-in send-path mechanism and storage wiring are both real now.
sendRoomMessagegained an optionaldurableparameter; when true, it also callsrecordRoomSendNotice(#135) after the existing fan-out.MeshStoreIdentitygained adataStoragefield -- the sameKeyValueStorageinstanceWireMeshTransport's own frame responder (#136) reads and writes -- andbridge-mesh.tsconstructs it once viacreateNodeFsStorage(oplogDirFor(slot)), wiring the identical instance to both the transport and the store's identity. Every existing fakeMeshStoreIdentityfixture across the test suite (12 files) needed the new required field added; the real behavior itself is tested end to end with real identities and a real minted token, decoding the stored bytes back out and running them through a realverifyRoomNotice.P5 is now fully wired end to end. A caller can mint a room-notice on send (
durable: true), and a peer'sdata-requestagainst this device's log will find it, since both paths now share one real, disk-backed oplog per identity slot. What remains is genuinely optional, forward-looking policy, not a missing mechanism: proactively callingsendDataFramewith an initialdata-haveright after a durable append (today, a peer only catches up if it already knows to ask), and deciding which peers' logs to track by default. Neither blocks P5's own core deliverable -- the primitives, the wire responder, and the opt-in send path are all real, tested, and merged.Closing P5. All three upstream primitives (createRoomNotice, the core/data sync protocol, createNodeFsStorage) are built and actually published (the wire-mesh#140 gap that blocked consuming them is fixed); agent-comms' own integration is real and merged: recordRoomSendNotice mints and appends, WireMeshTransport's frame responder answers a real data-have/data-request/data-entries exchange, and sendRoomMessage's opt-in durable flag wires the two together with one shared, disk-backed oplog per identity slot. This was genuinely "the biggest unknown" of the migration, and it's done.
Proactive catch-up (sending an initial data-have automatically after a durable append) and peer-tracking policy are real, tracked follow-up work, not part of this phase's own core scope -- filed as their own concrete piece if/when a caller actually needs automatic catch-up rather than on-demand.