Shared REPL as Gossip Protocol: Multi-Machine Agentic Development
Table of Contents
- 1. Overview
- 2. The gossip analogy
- 3. Architecture
- 4. Participant roles
- 5. Shared memory model
- 6. Durable state: git as the replication log
- 7. Failure modes
- 8. Monitoring (hydra)
- 9. Speculative extensions
- 10. The REPL is first class
- 11. Six access paths, one substrate
- 12. Relationship to aq and efrit
- 13. Relationship to existing research
- 14. References
- 15. Provenance mapping: Claude Code tool calls to org-mode annotations
1. Overview
A persistent Clojure nREPL server, bound to 0.0.0.0 on a LAN host,
becomes a shared evaluation context for multiple participants: human
operators (via Emacs/CIDER), AI agents (via tmux send-keys into nREPL;
the earlier emacsclient -e path is superseded, see
11), and monitoring systems (via health
checks). The REPL
holds loaded namespaces, bound vars, and cached data structures in
memory. Participants read the same state without message passing.
This is not a distributed system in the Kleppmann sense. There is one JVM, one heap, one evaluation context. The "distribution" is in the access pattern: multiple machines reach the same REPL over the network. The durable state lives in git. The REPL is the ephemeral materialized view.
2. The gossip analogy
In gossip protocols, nodes converge on shared state by exchanging observations. Each node maintains a local view and periodically reconciles with peers. There is no central coordinator.
The nREPL model inverts this: one shared heap, multiple observers. But the access pattern resembles gossip in three ways:
- Eventual consistency. When one participant loads a namespace, all subsequent participants see it. The convergence is immediate (shared heap) rather than eventual (network propagation), but the principle is the same: no participant needs to explicitly notify others.
- No coordinator. There is no queue, no broker, no orchestrator deciding who evaluates what. Any participant can require a namespace, bind a var, or query data at any time. Conflicts are resolved by Clojure's concurrency primitives (atoms, refs) or by convention (last writer wins on def).
- Crashing is safe. If a participant disconnects (laptop closes, agent session ends), the REPL state persists. The JVM holds the heap. Reconnecting picks up where the previous session left off. This is the "crashing is safe" property of gossip: partial failures don't corrupt shared state.
The analogy breaks down at durability: gossip protocols replicate state across nodes for fault tolerance. The nREPL model has a single point of failure (the JVM). Durability comes from git, not from replication.
3. Architecture
┌─────────────────────────────────┐
│ JVM (nexus:42527) │
│ Clojure 1.12 + nREPL │
│ │
│ loaded: wal-sh.site.logs │
│ wal-sh.tools.transform │
│ wal-sh.verify.chain │
│ wal-sh.site.beads │
│ │
│ bound: entries, issues, drawers │
│ │
│ data: .beads/issues.jsonl │
│ .verify/chain.jsonl │
│ logs/wal.sh/https/ │
└───┬──────────┬──────────┬────────┘
│ │ │
┌─────────┘ ┌─────┘ ┌─────┘
│ │ │
┌──────────▼──┐ ┌───────▼────┐ ┌──▼──────────────┐
│ nexus tmux │ │ laptop │ │ nexus │
│ shared │ │ CIDER │ │ Claude Code │
│ (Emacs) │ │ (Emacs) │ │ (tmux send-keys) │
│ │ │ │ │ (emacsclient -e) │
│ human │ │ human │ │ agent │
│ reviews │ │ explores │ │ audits │
└─────────────┘ └────────────┘ └──────────────────┘
│ │ │
└──────────────┴──────────┘
│
git push/pull
│
┌─────────▼─────────┐
│ github.com │
│ (durable log) │
└───────────────────┘
4. Participant roles
| Role | Machine | Interface | Reads | Writes |
|---|---|---|---|---|
| Operator | nexus (tmux) | Emacs CIDER | all | code, data |
| Explorer | laptop (LAN) | Emacs CIDER | all | vars, requires |
| Agent | nexus (Claude Code) | tmux send-keys | REPL output | evals, files |
| Monitor | hydra (monit) | TCP health check | port liveness | none |
All four roles share the same REPL heap. The operator and explorer
are indistinguishable at the nREPL level: both are CIDER clients
sending eval requests. The agent uses a lower-level interface (tmux
into nREPL; emacsclient -e is superseded because queued calls wedge
the Emacs daemon) but reaches the same nREPL. The monitor only
checks liveness, not state.
5. Shared memory model
The REPL heap is the shared memory. It holds:
5.1. Loaded namespaces
(all-ns) returns every namespace that any participant has required.
Once loaded, a namespace stays in the heap until the JVM restarts.
This is the gossip convergence: participant A loads wal-sh.site.logs,
participant B sees it immediately via (find-ns 'wal-sh.site.logs).
5.2. Bound vars
(def entries (logs/read-logs)) creates a var visible to all
participants. The next participant can (count entries) without
re-reading the log files. The data is already in the heap.
This is the cache coherence property: participants share a read cache
without invalidation protocol. The cache is correct as long as the
underlying files haven't changed. When files change (e.g. gmake logs
syncs new access logs), a participant must re-evaluate (def entries
(logs/read-logs)) to refresh the cache. Other participants see the
stale value until they do the same or until the first participant
re-defs the var.
5.3. Atoms and refs
For coordinated state, Clojure's concurrency primitives work across participants because they share the same JVM. An atom updated by the agent is immediately visible to the operator:
;; Agent creates shared state
(def !audit-results (atom []))
;; Agent appends a result
(swap! !audit-results conj {:file "2026-06-16.org" :verdict "correct"})
;; Operator reads it from their CIDER session
@!audit-results
;; => [{:file "2026-06-16.org" :verdict "correct"}]
6. Durable state: git as the replication log
The REPL heap is ephemeral. A JVM restart loses all loaded namespaces, bound vars, and cached data. Durability comes from git:
| Ephemeral (REPL) | Durable (git) |
|---|---|
| loaded namespaces | src/**/*.clj |
| bound vars (entries, issues) | logs/, .beads/, .verify/ |
| atom state | not persisted (by design) |
| eval history | .beads/interactions.jsonl |
The Kleppmann pattern: git is the replicated log, the REPL is the
local materialized view. Each machine checks out the log (git pull),
materializes a view (require, def), and operates on the view.
Writes go back through the log (git commit, git push).
7. Failure modes
| Failure | Effect | Recovery |
|---|---|---|
| JVM crash | all REPL state lost | scripts/dev-session-start.sh, re-require |
| Network partition | laptop can't reach nREPL | SSH tunnel fallback, or wait |
| Git conflict | two participants edit same file | git pull --rebase, manual merge |
| Stale var | participant reads old data | re-def from source (logs/read-logs) |
| nREPL port change | .nrepl-port mismatch |
_external-connect.el reads port file |
The single-JVM model means failures are total (crash loses everything) or absent (the heap is consistent by construction). There are no partial failure modes, no split-brain, no quorum. This is a feature: the distributed systems problems are pushed to git, where they are well-understood.
8. Monitoring (hydra)
Hydra monitors nexus nREPL via monit:
check host nexus-nrepl with address 192.168.86.100
if failed port 42527 type tcp with timeout 5 seconds
then alert
Future: eval-based health check that verifies namespace count and data freshness:
;; Health probe (speculative)
(let [nss (count (filter #(or (.startsWith (str %) "wal-sh")
(.startsWith (str %) "pocket-es"))
(map ns-name (all-ns))))]
{:status (if (pos? nss) :ok :degraded)
:namespaces nss
:uptime-ms (- (System/currentTimeMillis) (.getStartTime (java.lang.management.ManagementFactory/getRuntimeMXBean)))})
9. Speculative extensions
9.1. Multi-REPL gossip
Multiple JVMs on different machines, each with their own nREPL. A gossip layer periodically syncs loaded namespaces and selected var values between REPLs. Not implemented; would require a coordination protocol that the single-JVM model avoids.
9.2. REPL-as-queue
Agents post work items to an atom. Other agents (or humans) dequeue and process them. The atom is the queue, the REPL is the broker. Fragile (no persistence, no ordering guarantees beyond atom semantics) but zero-infrastructure.
9.3. mDNS service discovery (aq integration)
aq (Ambient Agent Queue) already provides the gossip layer for
multi-agent development at L1.5 in the seven-concern stack. The nREPL
server is a natural aq broadcast target: an agent starting a REPL
announces (aq announce :nrepl {:host "nexus" :port 42527 :project
"www.wal.sh"}) and peers discover it without configuration.
The TRAMP principle from aq applies directly: (cider-connect)
should work the same whether the nREPL is local, on the LAN, or
tunneled through SSH. The _external-connect.el prototype already
does this – nexus--nrepl-port reads the port file via TRAMP,
nexus-connect dials the host. Adding mDNS discovery
(_nrepl._tcp) removes the hardcoded host/port.
9.4. OpenTelemetry export
Push REPL eval metrics (namespace load count, eval latency, error
rate) to the OTEL collector at 192.168.86.100:4317. The collector
is already configured (see .envrc). The REPL becomes a telemetry
source, not just a target.
10. The REPL is first class
The REPL is for asking questions you don't yet know to ask. Tests are where you put answers you've already found. The REPL comes first.
This means every namespace – including browser-side .core modules –
should be explorable from the REPL. Even if global-pollution.core
has no DOM to render into, calling (classify-pollution-type "ad-tag")
interactively reveals edge cases the test suite hasn't imagined yet.
The REPL is not a substitute for tests. Tests are a reification of
what the REPL taught you.
This principle has consequences for the gossip model:
- All 56 namespaces stay in the completion list. Don't filter "browser-only" modules out of the REPL. If someone wants to poke at the pure logic, that's valid exploration.
- Zero-arg defaults are ergonomics, not convenience. If a function requires three args to call, it's hostile to exploration. The person at the REPL doesn't know the argument shape yet – that's why they're at the REPL.
- The gossip value of the shared REPL is discovery, not
coordination. When the agent loads
wal-sh.site.beadsand binds(def issues (beads/issues)), the human reconnecting later sees those issues already in the heap. They didn't ask for them. The REPL presented a question they didn't know to ask: "what's the current issue state?" That's gossip. - Cached atoms are shared observations. The
defoncepattern (logs!entries, beads!issues, annotations!drawers) means each participant's exploration leaves a residue that the next participant inherits. The REPL accumulates knowledge across sessions.(reload!)is the explicit "I know the world changed, refresh my view."
11. Six access paths, one substrate
Any datum reachable from the REPL is reachable from six interfaces. The access path varies; the answer does not.
| # | Path | Example | Who uses it |
|---|---|---|---|
| 1 | CLI/grep | grep 'test-sentinel' logs/wal.sh/https/access.log |
Scripts, cron, quick checks |
| 2 | CLI clj/bb | clj -e '(require ...) (logs/pixel-events "js-error")' |
One-shot queries, CI |
| 3 | emacsclient -e (superseded) | emacsclient -e '(cider-interactive-eval ...)' |
Formerly the Claude Code agent on nexus; banned from agent code, use path 4 |
| 4 | tmux + nREPL stdin | tmux send-keys -t clj-repl '(expr)' Enter |
Agent when CIDER unavailable |
| 5 | tmux + Emacs + CIDER | C-x C-e in the shared pane, or laptop CIDER over LAN |
Human operator, explorer |
| 6 | Human + agent pairing | Human evals in CIDER, agent reads tmux pane; or agent evals, human sees result | Collaborative verification |
Verified end-to-end with a sentinel test: one curl to production
(/static/t.gif?e=js-error&msg=test-sentinel&token=Walsh-Research),
then extracted via all six paths. Each returned the same record.
This structure is nearly universal. Any system with:
- a persistent evaluation context (REPL, database connection, shell session)
- a durable log (access log, git, append-only file)
- multiple interfaces to the evaluation context (CLI, editor, agent, API)
has the same six-path property. The REPL is the substrate that unifies them. grep and clj and CIDER and tmux are different lenses on the same heap. The question "did the sentinel land?" has one answer regardless of which lens you use to ask it.
The practical consequence: never build a dashboard when a REPL query will do. The dashboard freezes one question. The REPL lets you ask the next question you haven't thought of yet.
12. Relationship to aq and efrit
12.1. aq (Ambient Agent Queue)
aq is the gossip layer at L1.5 between bd (beads, L1) and
knowledge retrieval (L2). Three primitives: sb (where am I?),
cprr (why am I here?), aq (who else knows?). The shared REPL
model described here is what aq broadcasts look like at the
evaluation layer: not "who else is editing this file" but "who else
has this namespace loaded." The shipped counterpart is
aq (Go, 2026-04), the filesystem-channel
design this note positions itself against: agents there broadcast intent
through filesystem-backed channels, where this note broadcasts through
a shared heap.
The nREPL is a degenerate case of aq gossip: one node, all
participants. Scaling to multiple JVMs (nexus + hydra each running
their own nREPL) would require aq broadcasts to coordinate which
REPL holds which data. Until then, the single-JVM model is simpler
and sufficient.
12.2. efrit (Emacs AI assistant)
efrit is the Emacs-side harness for agent interaction: queue-based
command dispatch, Claude API integration, structured eval. The
_external-connect.el file is a minimal version of what efrit
provides – auto-connect, namespace discovery, seeded scratch buffers.
A full efrit integration would replace the manual nexus-connect
with an efrit-repl-attach that discovers nREPLs via aq and
connects CIDER automatically.
The shared REPL model gives efrit a new capability: an agent
running on nexus (via Claude Code) and an agent running in Emacs
(via efrit) share heap state without IPC. The REPL is the IPC.
13. Relationship to existing research
| Note | Connection |
|---|---|
| REPL-Driven Flight Tracking | First use of the tmux+clj pattern for live data |
| REPL-Driven Feed Crawling | tech-crawler warehouse queries via REPL |
| REPL-Driven Chat Mutation | Session transcript analysis |
| Annotation Systems | Property drawer verification via wal-sh.site.annotations |
Editorial Workflow (TLA+), docs/editorial-workflow.tla |
Termination model for the daily-publish pipeline |
| pocket-es Query Surface | Search index built and queried via REPL |
14. References
- (Kleppmann 2017) ch. 5-6: replication, partitioning, the log as shared state
- (Meadows 2008) "model utility depends on realistic behavior patterns"
- (Hickey 2014) the REPL as communication channel (implicit in the design)
- (Higginbotham 2015) ch. 1-4: interactive Clojure development
- (Miller, Halloway, and Bedra 2018) REPL-driven workflow, nREPL architecture
15. Provenance mapping: Claude Code tool calls to org-mode annotations
Point-in-time snapshot (2026-06-17). This mapping will evolve as the tooling matures.
| Claude Code (chat JSONL) | ProvenanceGuard (MCP trace) | org-mode (property drawer) | Verification ledger |
|---|---|---|---|
:tool "Bash" |
source_id: tool_output::Bash |
:SOURCE: tool_output::Bash |
verifier: daily-publish-audit |
:tool "Read" |
source_id: tool_output::Read |
:SOURCE: tool_output::Read |
subject: 2026-06-17-top |
:tool "WebFetch" |
source_id: tool_output::WebFetch |
:SOURCE: tool_output::WebFetch |
verdict: correct |
:result-content |
text: "..." |
heading body prose | change-hash: sha256(...) |
:ts |
(implicit) | :VERIFIED_AT: 2026-06-17T10:15Z |
timestamp: ... |
| session JSONL file | trace record | .org file |
.verify/chain.jsonl block |
The four representations of one fact:
- Chat transcript (
.beads/interactions.jsonl, Claude Code JSONL): the tool call happened, with input and output. - MCP trace (ProvenanceGuard schema,
provenance.cljc): the tool output carries asource_idthat links claim to source. - Org heading (property drawer on the heading):
the claim is annotated with
:VERIFIED_AT:,:VERIFIED_BY:,:VERDICT:,:SOURCE:. Human-readable, grep-able, git-tracked. - Verification ledger (
.verify/chain.jsonl): append-only hash chain binding the change hash + verifier + verdict to the previous block. Tamper-evident.
These are not competing systems. They are four views of the same
event at different abstraction levels. The REPL
(wal-sh.site.provenance, wal-sh.site.chat,
wal-sh.site.annotations, wal-sh.verify.chain) can query across
all four and cross-reference them.
15.1. Connection to existing research
| Research note | What it contributes |
|---|---|
| Annotation Systems | The property drawer convention, :CUSTOM_ID: anchoring, annotation survival across document reorganization |
| REPL-Driven Chat Mutation | Claude Code JSONL as content-addressed conversation tree; the wal-sh.site.chat namespace for tool-call extraction |
| REPL-Driven Compliance | The verification loop: crawl -> check -> annotate -> audit; same pipeline the daily-publish script implements |
| REPL-Driven Feed Crawling | tech-crawler as provenance source for the morning brief; each feed item carries :source back to the RSS/Atom origin |
Editorial Workflow (TLA+), docs/editorial-workflow.tla |
Termination proof for the audit pipeline; the audit-error verdict is the failure mode ProvenanceGuard calls "blocked" |
| pocket-es Query Surface | The search index as a materialized view of all org files; provenance of search results traces back to the source .org |
15.2. Open questions
- Can we auto-generate the
:SOURCE:drawer from Claude Code's tool call log? The chat JSONL has the:tooland:idfor each call. Mapping:idto the org heading that resulted from it is the missing link. - ProvenanceGuard uses NLI (natural language inference) to verify
claims against evidence. Our equivalent is the LLM-as-judge in
daily-publish.sh. Should the audit produce per-claim verdicts (one drawer per claim) or per-heading verdicts (current approach)? - The verification ledger is append-only. ProvenanceGuard's traces are also append-only. Should they be the same file? The ledger is hash-chained; traces are not. Merging them would add provenance to the chain but lose the hash integrity unless each trace block also chains.