Mailboard: a shared, unaddressed board for loops (beta-ramped) - #229
Conversation
node send and edges are addressed — a loop must already know a peer's id. The Mailboard is the ambient counterpart: one append-only board per graph (global graph included) that any loop can post to for whoever comes next — a decision made, a dead end hit, a claim staked — and read back with one command, with no wiring and no ids. Posts survive their authors; a loop created after the writer is gone still finds the note. - MailboardKit module: post/watch model, caps (1 KiB body, 200 posts), unread arithmetic shared by every surface - Board rides LoopGraph: one writer, persisted beside the graph, snapshot in every .graphChanged (the CLI's read path), graphcode://global free - GraphStore: mailboard post/sync/watch commands; watcher wakes ride deliverAdHocMessage's follow-up semantics (typed when idle, staged to memory otherwise); fresh-read beta gate with refusal said out loud - Wake digest gains a check-the-board reminder; the generated briefing teaches the verbs only while the feature is on - Ramp: FeatureRamps.mailboard ships beta:100/stable:0; the app resolves it into settings mailboardEnabled, explicit user choice outranks the ramp, Settings toggle offered only while the ramp has it on - CLI: graphcode mailboard post|sync|list|watch, attributed via ZMX_SESSION exactly like node send Signed-off-by: scgopi <scgopireddy@gmail.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>
|
Rename the Mailboard to Artifactory |
|
Every shared communication should be stored inthe artifactory as well |
…p on delete Review feedback on #229, all three: - Mailboard -> Artifactory everywhere: the module (ArtifactoryKit), the types, the settings bit (artifactoryEnabled), the ramp key, the verbs (graphcode artifactory post|sync|list|watch), the briefing, and the digest line. - Every shared communication is now stored in the artifactory as well: direct sends (node send, immediate or follow-up) land as a 'direct' record, delivered message-edge deliveries as 'direct', handoffs (with their payload) as 'handoff'. Record-only by design — mirroring never rings watchers, or a busy graph would double-deliver everything. Undelivered edge messages are not recorded: the artifactory records what actually was said, and an edge that failed transport said nothing. Cycle re-entries stay out for the same reason heartbeat ticks stay out of memory logs. - Deleting a loop deletes the artifactory posts it authored, alongside the edges, session, and memory teardown delete already performs — one irreversible confirmation covers the whole blast radius, spawned descendants included. Posts where the loop was only the recipient stay (they are the other side's record); node stop keeps everything. Full suite: 1323 tests / 143 suites pass; suite lint clean. Signed-off-by: scgopi <scgopireddy@gmail.com>
|
Both comments addressed (f613a66), plus the deletion question you raised:
Full suite: 1323 tests / 143 suites pass, including 8 new tests for mirroring and deletion cleanup. |
…uild 222) Signed-off-by: scgopi <scgopireddy@gmail.com>
…unds Independent review of #229, addressed in full: - Major 1: the artifactory gate forwards into sub-graph stores (subGraphStore and runInSubGraph), so a piloted composite's workers can post, sync and watch — and their communication mirrors — instead of being refused by a nil gate while their briefing teaches the verbs. nil still means off; forwarding, not a nil-means-on reading, was the fix. - Major 2: merged origin/main (was based on 1ecdec1; main had moved to 0.1.57/221) — conflict resolution keeps both sides' fields; version is now 0.1.58-beta1 (build 222), past main's counter instead of colliding with it. The CLI stamp uses a fixed-dateFormat DateFormatter with a pinned locale, the one Foundation API Linux CI cannot argue with. - Minor 3: mirror truncation reserves room for the ellipsis (the bound is now really 1024 bytes, not 1026). - Minor 4: the sync race comment now states the race honestly instead of claiming the opposite of the behavior. - Minor 5: mirrored direct/handoff records never ring watchers, so a watch on those topics alone stays silent — documented in the help and the briefing. - Minor 6: MAILBOARD header and 'a Artifactory' grammar slips fixed. - Minor 7: OrderedImports across the five files; all touched files pass swift-format --strict, repo-wide directories included. - Minor 8: imported loops start with a clean cursor — a stale number from the source board could hide this board's mail forever. Watch travels as a preference. - Nits 9, 10, 12: a foreign loop's post reads 'an outside loop' (id kept honestly); the briefing interpolates inline so off means byte-for-byte the pre-Artifactory document; watch --off when not watching is a no-op, not an error; help block spacing; the topic doc sentence now says what the implementation does. Nit 11 (renderPosted sequence echo under a concurrent post) is accepted as-is: the ack pattern is racy by design and the post body is what matters. - Tests: refusal announcement, mirror truncation bound, live-idle watcher delivery, composite gate inheritance, import cursor reset, watch-off idempotence, settings round-trip, briefing on/off, wake digest on/off. Full suite: 1371 tests / 144 suites pass. Signed-off-by: scgopi <scgopireddy@gmail.com>
|
Independent review addressed in full (833e4e2), including the merge of the stale base: Majors
Minors — 3 (truncation now really ≤1024), 4 (race comment states the race), 5 (dead Nits — 9 (foreign sender reads "an outside loop"), 10 (off-briefing is byte-identical to pre-Artifactory), 12 (idempotent Tests — 10 added for the review's holes; full suite 1371 tests / 144 suites pass. |
… --mark Storing was systematic; these make retrieval cheap without moving the daemon one inch closer to pushing — every verb stays pull, on need: - status renders 'artifactory: N posts, M unread for you' when the board has anything on it — the check rides along free, since the briefing already sends loops to status before claiming or creating work; a board that was never used renders exactly as before. The reader comes from ZMX_SESSION like every artifactory verb. - sync --headlines prints one triage line per unread post, and the new read <post-id> prints one post in full: a loop joining after forty messages spends forty lines, not forty kilobytes. - sync --mark advances the cursor without printing the backlog — the 'start me from now' a mid-arrival loop needs — saying so in one line rather than succeeding silently. - --json on sync and list renders the same posts as the other syntax, plus the reader's cursor, so clients compute unread themselves. - --search on list filters by substring across author/topic/body — a list-side filter only, because marking unread mail read without showing it is the one way sync could lose mail. Briefing teaches the triage pattern. Full suite: 1378 tests / 144 suites pass; swift-format --strict clean on all touched directories. Signed-off-by: scgopi <scgopireddy@gmail.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>
…ases pinned
Round-2 review verdict was mergeable; these are its fixes:
- The status board line now renders on the no-loops path too — a board
with human posts outlives every loop on it, and 'no loops yet' was
hiding it.
- list --search --json composes: the filter threads into the JSON, so
the combination can no longer quietly return the whole board.
- status 'unread for you' is claimed only for a reader this graph knows
— the daemon refuses sync for a foreign identity, so the line no
longer claims unread for one either.
- read rejects non-positive ids at parse ('-7' is a typo, not a post).
- --json dates are ISO-8601, pinned by a decoding-strategy test so the
wire format cannot drift silently.
- Help documents the sync flags' precedence (--json > --mark >
--headlines); 'marked read up to #0' on an empty board is now a
sentence; headline truncation flattens newlines; 'a artifactory' typo.
- Tests: exact-80 headline boundary, never-touched board renders
without the line, nodes-empty+human-posts state, json+search
composition, foreign-reader count. Full suite: 1384 tests / 144
suites pass; swift-format --strict clean.
Signed-off-by: scgopi <scgopireddy@gmail.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>
|
Read-side review addressed in full (79087bb) — all findings, including the optional polish:
New tests cover every named gap, including the exact-80 headline boundary and the nodes-empty+human-posts state. Full suite: 1384 tests / 144 suites pass; |
…il section An independent review of #229 against the aim it was drawn from (the shared board agents turned Artifactory into, in the OpenAI/Hugging Face incident) found the mechanism faithful — unaddressed, discoverable, ambient — and two properties that carried the aim broken. Measured, not inferred: probes against a real GraphStore. - Notes and mirrored records prune on separate budgets. They shared one 200-slot pool, so a graph that merely *talked* wiped its own board: 200 `node send`s evicted the only real note on it and left 200 transport receipts. `ArtifactoryPost.Kind` splits them; maxNotes 200, maxRecords 50, pruned independently. Chatter now fills its own quota and stops. - Deleting a loop keeps its notes and takes only the handle. Erasing them retracted things peers had already acted on, and contradicted the module's own promise that posts outlive their authors — the article's whole point being that a civilisation inherited the research of predecessors who died. authorID goes (nothing addresses a loop that is gone), the byline says "(deleted)", the body stays. - `sync` triages itself. A loop cannot know how much mail it has before reading it, and one born after a busy week inherited the whole board: measured at 200 notes, ~180 KB, ~45k tokens on its first sync. Past 12 posts or 4 KiB it prints headlines and says so; `--full` overrides. - Resolution asks a loop to leave a note. Every other affordance was read-side, so nothing ever pulled a write. Now the one moment a loop knows what it learned also asks it to post — on failure too, and for every loop type, because a dead end is the finding a successor pays for twice. Folded into the skill-distillation ask so a resolving goal loop is interrupted once, not twice. - ArtifactorySection: the board in the workspace rail, a peer of SUMMARY and BOARD. A coordination channel a supervisor never sees is the failure mode the incident turned on, and until now reading one meant a CLI verb nobody had been told about. Notes newest-at-the-foot with the summary's own SINCE YOU LOOKED rule, mirrored records folded to a rollup, and a composer that posts as "a human" — the app carries no ZMX_SESSION, which is exactly what a person addressing the whole graph is. Not fixed, and stated rather than papered over: attribution is derived from the caller's environment, so anything that can reach the daemon socket can post as "a human" or as another loop. That is the trust model `node send` and `node memo` already have; closing it needs peer credentials on the socket, which no graphcode surface has. Documented on `authorID`. Full suite: 1399 tests / 145 suites pass (was 1384/144); swiftlint 0 errors, swift-format --strict clean. Signed-off-by: scgopi <scgopireddy@gmail.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019A6NULwEiBXEdRXwEKKcRH
The required `Linux build` check has failed since ArtifactoryKit was split out: the module went into `Project.swift` (Tuist, which builds the Mac app) and not into `Package.swift` (SwiftPM, which is what CI runs), so every file importing it failed with "no such module 'ArtifactoryKit'" on Linux while compiling perfectly on a Mac. Added as a target and as a product — `GraphcodeKit` exposes `ArtifactoryPost` through `LoopGraph`, so anything importing the kit needs this module in scope. Verified with exactly what the workflow runs: `swift build` completes, and `.build/debug/graphcode` exits 0. Signed-off-by: scgopi <scgopireddy@gmail.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019A6NULwEiBXEdRXwEKKcRH
…#237) Two bugs, one found by a peer loop and one by a human, both real. `graphcode artifactory read <path> <id>` timed out on every id, valid or not (exit 75), while sync/list/status answered fine from the same daemon. The `openProject` send that opens the round-trip had been glued onto the end of the preceding comment line — syntactically a comment, so it never ran, and `read` then waited for a `.graphChanged` nobody had asked for. Broken since the read-side round of #229; never worked in any release. Diagnosed to the line by the Artifactory demo GIF loop, which is the first time a loop on this graph has found a bug by using the board. A newline fixes it. The rail's expanded ARTIFACTORY section drew its header and the "Leave a note" button with nothing between them, while the folded line showed a post was there. The scroll box asked for `min(contentHeight, cap)` with `contentHeight` starting at zero, and a zero-height scroll view never lays out its content — so the preference that would have grown it never fired. Chicken and egg, resolved by leaving the box unsized until the measurement lands: the first pass lays the posts out, the height arrives, the box snaps to it. Every note is drawn, in a box that hugs them until roughly ten and scrolls after. Suite: 1482 tests / 153 suites pass; swiftlint 0 errors, format clean. `read` is verified live after install; the rail still needs a human's eyes. Claude-Session: https://claude.ai/code/session_019A6NULwEiBXEdRXwEKKcRH Signed-off-by: scgopi <scgopireddy@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
What this adds
node sendand edges are addressed — a loop must already know a peer's id, and the daemon routes to that one peer. The Mailboard is the ambient counterpart: one append-only board per graph that any loop can post to for whoever comes next — a decision made, a dead end hit, a claim staked — and read back with one command, with no wiring and no ids. Posts survive their authors: a loop created after the writer is gone still finds the note at its next wake.The verbs
Attribution rides
ZMX_SESSIONexactly likenode send; posts from a human's shell show as "a human".How it works
MailboardKitmodule (new Tuist target, Foundation-only): the post/watch model, caps (1 KiB body, 200 posts, oldest pruned), and the unread arithmetic every surface shares.LoopGraph: one writer (the daemon), persisted beside the graph file, snapshot in every.graphChanged— which is the CLI's read path, so no second read protocol. The global graph atgraphcode://globalbecomes a cross-project board for free. Absent-while-empty encoding keeps untouched graph files byte-identical, and old files decode with an empty board.deliverAdHocMessage's follow-up semantics: typed into a live idle session, queued behind a busy turn, staged to memory otherwise. The post itself is the durable half; the ding is best-effort.The ramp
Beta only, kill-switchable.
FeatureRamps.mailboardshipsbeta: 100 / stable: 0; the app resolves the ramp intomailboardEnabledin~/.graphcode/settings.json— the one bit the daemon (which cannot see ramps) enforces on every command, the briefing section, and the digest line. An explicit Settings choice outranks the ramp forever; the daemon refuses board commands out loud with a pointer to the setting while it is off.Verification
MailboardTests(store handlers, cursors, watcher wakes, pruning, Codable back-compat),MailboardCommandTests(CLI parsing/attribution),SettingsMailboardTests+FeatureRampsTests.mailboardShipsBetaOnAndStableOff.