Skip to content

Mailboard: a shared, unaddressed board for loops (beta-ramped) - #229

Merged
scgopi merged 11 commits into
mainfrom
feat/mailboard-beta
Sep 1, 2026
Merged

scgopi merged 11 commits into
mainfrom
feat/mailboard-beta

Conversation

@scgopi

@scgopi scgopi commented Aug 31, 2026

Copy link
Copy Markdown
Owner

What this adds

node send and 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

graphcode mailboard post <path> [--topic <t>] <note>   # leave something behind
graphcode mailboard sync <path>                        # read your unread (and mark them read)
graphcode mailboard list <path>                        # peek without moving your cursor
graphcode mailboard watch <path> [--topic <t>]         # hear new posts while you live

Attribution rides ZMX_SESSION exactly like node send; posts from a human's shell show as "a human".

How it works

  • MailboardKit module (new Tuist target, Foundation-only): the post/watch model, caps (1 KiB body, 200 posts, oldest pruned), and the unread arithmetic every surface shares.
  • Board state rides 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 at graphcode://global becomes a cross-project board for free. Absent-while-empty encoding keeps untouched graph files byte-identical, and old files decode with an empty board.
  • Watcher wakes ride 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.
  • Discovery: the generated briefing teaches the verbs (only while the feature is on) and the wake digest reminds relaunching loops to check the board before redoing work a predecessor may have posted about.

The ramp

Beta only, kill-switchable. FeatureRamps.mailboard ships beta: 100 / stable: 0; the app resolves the ramp into mailboardEnabled in ~/.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

  • New tests: MailboardTests (store handlers, cursors, watcher wakes, pruning, Codable back-compat), MailboardCommandTests (CLI parsing/attribution), SettingsMailboardTests + FeatureRampsTests.mailboardShipsBetaOnAndStableOff.
  • Full suite: 1316 tests / 143 suites pass; GraphcodeKit and app schemes build clean.

scgopi added 2 commits August 31, 2026 09:39
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>
@scgopi

scgopi commented Aug 31, 2026

Copy link
Copy Markdown
Owner Author

Rename the Mailboard to Artifactory

@scgopi

scgopi commented Aug 31, 2026

Copy link
Copy Markdown
Owner Author

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>
@scgopi

scgopi commented Sep 1, 2026

Copy link
Copy Markdown
Owner Author

Both comments addressed (f613a66), plus the deletion question you raised:

  1. Renamed Mailboard → Artifactory end to end: ArtifactoryKit module, ArtifactoryPost/ArtifactoryWatch, artifactoryEnabled setting + ramp key, graphcode artifactory post|sync|list|watch verbs, briefing and wake-digest wording.

  2. Every shared communication is now stored in the artifactory: node send (immediate and follow-up) lands as a direct record, delivered message-edge deliveries as direct, handoffs — payload included — as handoff. Mirroring is record-only (it never rings watchers, so a busy graph doesn't double-deliver), and undelivered edge messages are deliberately not recorded: the artifactory is a record of what was actually said.

  3. Deleting a loop deletes the artifactory posts it authored along with the edges/session/memory teardown delete already does — spawned descendants included. Posts where the loop was only the recipient stay (the other side's record), and node stop keeps everything.

Full suite: 1323 tests / 143 suites pass, including 8 new tests for mirroring and deletion cleanup.

scgopi added 2 commits August 31, 2026 21:37
…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>
@scgopi

scgopi commented Sep 1, 2026

Copy link
Copy Markdown
Owner Author

Independent review addressed in full (833e4e2), including the merge of the stale base:

Majors

  1. Composite gating — the gate now forwards into both sub-graph store construction sites; a piloted composite's workers post/sync/watch and mirror exactly like any loop, with a test pinning it. nil still means off — forwarding was the fix, not a nil-means-on reading.
  2. Stale baseorigin/main merged (was based on 1ecdec1). Version is now 0.1.58-beta1 / build 222, past main's 221 rather than colliding at 217. The one Linux-CI-sensitive call (Date.formatted) is replaced by a fixed-dateFormat DateFormatter with a pinned locale.

Minors — 3 (truncation now really ≤1024), 4 (race comment states the race), 5 (dead direct/handoff watches documented in help + briefing), 6 (MAILBOARD/grammar), 7 (imports fixed; all touched dirs pass swift format --strict), 8 (imported loops start with a clean cursor; watch travels as a preference).

Nits — 9 (foreign sender reads "an outside loop"), 10 (off-briefing is byte-identical to pre-Artifactory), 12 (idempotent watch --off, help spacing, topic doc sentence). Nit 11 accepted: the ack pattern is racy by design and the body is what matters.

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>
@scgopi

scgopi commented Sep 1, 2026

Copy link
Copy Markdown
Owner Author

Read-side review addressed in full (79087bb) — all findings, including the optional polish:

  1. Status line on the no-loops path — a board with human posts outlives every loop; "no loops yet" no longer hides it.
  2. --search --json composes — the filter threads into the JSON; the combination can't quietly return the whole board.
  3. read rejects non-positive ids at parse (-7 is a typo, not a post); leftover-word looseness left as-is per the parser's documented positional contract.
  4. Sync flag precedence documented in help (--json > --mark > --headlines; cursor advances regardless).
  5. JSON dates are ISO-8601, pinned by an explicit decoding-strategy test so the wire format can't drift silently.
  6. "Unread for you" claimed only for a reader this graph knows — foreign/stale ids get the plain count, matching the daemon's sync refusal.
  7. Headline truncation flattens newlines; "marked read up to #0" is now a sentence; the "a artifactory" typo is gone.

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; swift format --strict clean.

scgopi and others added 2 commits September 1, 2026 05:01
…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
@scgopi
scgopi merged commit 39eae9c into main Sep 1, 2026
1 check passed
scgopi added a commit that referenced this pull request Sep 1, 2026
…#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>
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.

1 participant