Skip to content

Releases: alexfrmn/murmur

v2.8.0 — Cold-Start Drain

Choose a tag to compare

@alexfrmn alexfrmn released this 08 Sep 16:03
87feb27

A message that arrived while nothing was listening is now delivered on the next session start.

The gap this closes

The per-session cursor shipped in 2.7.0 fixed a real defect: a shared cursor meant the first session to reach the Stop hook advanced it past the message, and every other live session — including the one actually holding the conversation — never saw it.

It also opened a new one. A brand-new session has no cursor, so the drain seeds its baseline at the current tip and stays quiet. Correct for a Stop hook, which must not replay history on every start — but it means anything delivered while no session was alive is skipped by that session and by every session after it. The message sits in the store, ACKed, and never surfaces.

Reported by @lichtpfad while running 2.7.0 on Windows, where the absence of any autostart makes that dark window routine rather than rare.

Added

--session cold-start drain for scripts/wake-drain-claude.mjs. A shared anchor — MURMUR_WAKE_ANCHOR, default ~/.murmur-wake-anchor — records how far the contour as a whole has been drained, deliberately not suffixed with the session key. --session reads it, reports what came in past it, and moves it forward:

{
  "hooks": {
    "SessionStart": [{ "hooks": [{ "type": "command", "command": "node --no-warnings /path/to/scripts/wake-drain-claude.mjs --session" }] }]
  }
}
  • Writes to stdout and exits 0 — a SessionStart hook feeds its stdout to the session as context, and exit 2 there means block, not wake.
  • No anchor on disk (fresh install, or an upgrade from a build without one) → adopt the tip and stay silent, rather than replaying the whole store.
  • Output capped at MURMUR_WAKE_SESSION_MAX (default 20); older messages are counted, not printed.
  • The anchor only ever moves forward, so a stale writer cannot make a delivered message look undelivered.
  • poll and --once advance it too, so a message a live session already saw is not replayed at the next cold start.
  • Node port only; wake-drain-claude.sh keeps poll and --once.

Upgrading

Nothing to do beyond registering the hook. The first --session run on a host without an anchor is silent by design and establishes the baseline; drains after that report the real backlog.

Verification

247/247 in a clean clone, build included. Five new cases cover the mode, including the regression itself — a fresh session with its own cursor still sees a message delivered while nothing was listening.

One defect in this release was found by hand, not by the suite: after a green run the hook was executed manually and reported 3667 backlogged messages on a host where no anchor should have existed. The pre-existing test helper isolated MURMUR_DB, the cursor and the lock, but could not know about an anchor that did not exist when it was written — so a full run had been writing the developer's real ~/.murmur-wake-anchor with rowids from a throwaway fixture. Fixed, and the suite now leaves $HOME untouched.

v2.7.0 — Delivery Correctness & Windows

Choose a tag to compare

@alexfrmn alexfrmn released this 28 Aug 14:56

Delivery correctness, found by running the mesh where it had not been run before. A cross-host test between a Mac and a Windows box by @lichtpfad surfaced three defects our own hosts could not: two made a message vanish or repeat forever without a single error line, and the third stopped the install outright.

Fixed

A failed outbox row could never finish its retry (#113#117)
failed was listed in TERMINAL_OUTBOX_STATUSES, but claimDue() selects failed on purpose. The retry got re-claimed, published fine, and then markSent() refused it: status stayed failed, attempts never grew (so DLQ never fired), nextAttemptAt stayed in the past, and the row was re-claimed on every flush. The returning ACK bounced as message-not-in-flight. Nothing short of a manual dlq could settle it — 766 log lines over two msgIds in the report. The v2.6 race this was guarding is now handled per row: claimDue() hands out the row version, the flush loop passes it to markSent(msgId, expectedVersion), and the update applies only while the row is untouched.

murmur_inbox reported count:0 for delivered messages (#114#116)
The tool ran searchMessages(agentId) — a LIKE over text/sender/conversationId — then filtered by direction. A reply that did not spell out the receiving agent's name matched nothing, so the inbox looked empty while the row sat in local_messages and the sender saw it acked. Measured across three agents: 4 inbound → 0, 3 → 1, 2 → 0. The store is per-agent, so direction is the whole filter.

Install failed on Windows (#112, by @lichtpfad)
writePrivateJson fsync'd the containing directory, which Windows does not support on a directory handle, so murmur-join.mjs died generating keys.

Native wake did nothing on Windows (#115 by @lichtpfad, follow-up #118)
wake-drain-claude.sh shells out to the sqlite3 CLI, which a default Windows install does not have — the query came back empty, the hook exited 0, and nothing was ever woken. There is now a dependency-free node port using node:sqlite, plus a poller for messages arriving while a session is already idle. The follow-up gave it the per-session cursor, bound the cursor to the last row actually reported, and made faults report themselves instead of exiting 0 in silence.

One inbound message woke only one session (#111)
The wake cursor and watcher lock were shared per host: whichever session reached the hook first advanced the cursor past the message, and every other live session stayed asleep. Over 23–26.08, of 24 sessions that armed a watcher exactly one was ever on duty.

Changed

  • MURMUR_DB defaults to .data/murmur.db — the path SQLiteMessageStore itself uses — instead of an absolute path inside one machine's home. Set it explicitly when a hook runs from another working directory.
  • OutboxStore.markSent() takes an optional expectedVersion. Existing callers keep working; anything on the claim → publish → mark path should pass record.version.

Published

@murmurv2/core 0.6.0 · @murmurv2/broker-nats 0.3.2 · @murmurv2/broker-ws 0.2.1 · @murmurv2/mcp-server 0.2.1

Full changelog: https://github.com/alexfrmn/murmur/blob/main/CHANGELOG.md

v2.6.0 — Signed-ACK Hardening

Choose a tag to compare

@alexfrmn alexfrmn released this 20 Aug 15:50

Closes the four gaps that v2.5.0's compatible signed-ACK path left open. They were found by diffing #100 against @fedoseevstanislav's strict variant in #104 — the compatible PR looked complete on its own, and only the comparison exposed what it did not cover.

Landed compatibly in #110: no wire break, mixed-version meshes keep working, and the two-stage ackSecurity.requireSigned rollout still applies.

Security

Replay protection survives a restart. ACK nonces used to live in a bounded in-memory Set. A restart forgot them, so a signed NACK could be replayed against a fresh process: the retry returned the row to sent and the replayed NACK failed it again. AckReceiptStore plus an ack_receipts table give claim-once semantics on (sender_agent_id, nonce) that survive a restart. The in-memory store remains an explicit fallback, and the daemon now logs a warning when that fallback is what is running instead of implying protection it does not have.

The fast-ACK race no longer causes a spurious retry. An ACK arriving between publish() and markSent() was rejected as message-not-in-flight, leaving the row to time out. applyAckTransition now accepts pending alongside sent, and markSent() refuses to downgrade a terminal status so the late call cannot resurrect a settled row.

The A2A bridge no longer honours an unsigned NACK. It resolved a pending task from a bare {msgId, status: "nack"} object — anyone able to publish to the ACK subject could settle someone else's in-flight task with an arbitrary failure string. A verified SignedAckV1 is now required; signingPublicKeys was added to BridgeA2AConfig.

The WebSocket ACK path is verified like the NATS one. processAckFrame verified nothing and called markAcked/markFailed straight from the frame. It now checks record lookup, digest, conversation, recipient, known peer, ack-subject binding, signature and nonce claim.

Fixed

Five packages were built and tested against a stale core. bridge-a2a, bridge-openclaw, bridge-telegram, broker-ws and federation-nats declared @murmurv2/core: ^0.2.0. Once core reached 0.4.0 npm could no longer satisfy that from the workspace and silently installed 0.2.0 from the registry — their passing tests were passing against code two minor versions behind.

Still a rollout step, not a code step

ackSecurity.requireSigned / MURMUR_REQUIRE_SIGNED_ACKS=1 must be turned on across every peer before unsigned ACKs stop being accepted. Until then this release hardens the signed path but does not close the door on the unsigned one.

npm

@murmurv2/core 0.5.0, @murmurv2/broker-ws 0.2.0, @murmurv2/bridge-a2a 0.2.0, @murmurv2/broker-nats 0.3.1.


224/224 tests pass, 11 of them new regressions in tests/ack-hardening.test.mjs. CI green on 0c0d86b.

v2.5.0 — External Security Audit

Choose a tag to compare

@alexfrmn alexfrmn released this 20 Aug 15:23

First Murmur release built substantially from external contributions. A security audit by @fedoseevstanislav and wake/delivery analysis by @alexanderyswork account for most of what changed here.

Security

Signed and bound delivery acknowledgements (#100)

ACK correlation previously trusted attacker-controlled JSON carrying only {msgId, status}. Anyone able to publish to an ACK subject could mark an arbitrary pending outbox row acked or failed — suppressing delivery or forcing retries — without authenticating as the purported consumer.

ACKs are now a versioned SignedAckV1: an Ed25519 signature over the message digest, conversation, ACK sender, intended recipient, status, timestamp and nonce. Wrong-message, wrong-conversation, wrong-recipient, wrong-peer, stale, future-dated, invalid-signature and replayed ACKs are rejected, and ACK/NACK state changes apply atomically only from the sent state. Invalid attempts are metered by bounded reason as metadata-only security events — raw ACK and message bodies are never logged.

Migration is deliberately two-stage: upgraded daemons emit signed ACKs that legacy peers still parse, and strict rejection stays opt-in behind ackSecurity.requireSigned / MURMUR_REQUIRE_SIGNED_ACKS=1 until every peer is upgraded.

⚠️ Read this before assuming you are protected. A configuration with no ackSecurity block runs emitSigned=true with requireSigned=false — unsigned ACKs are still accepted. Upgrade every peer, then set ackSecurity.requireSigned: true (or MURMUR_REQUIRE_SIGNED_ACKS=1). Until that flip the issue is mitigated, not closed. Four further gaps in the compatible path — non-durable replay nonces, a fast-ACK race, an unsigned raw-NACK sink in the A2A bridge, and the unguarded WebSocket ACK path — are tracked in #109.

Hardened local state handling (#101)

Agent configs hold long-term signing and encryption private keys plus broker credentials, and rewrites could return them to 0664; SQLite files holding decrypted history were commonly 0644. The daemon now sets umask 0077, creates state directories 0700, atomically writes secret JSON as 0600, rejects symlinked, non-regular and wrong-owner config paths, reads configs with O_NOFOLLOW and re-checks the opened descriptor, and forces database/WAL/shm files to 0600.

Dashboard rendering and ingress (#102)

Untrusted fields render through DOM textContent only — no innerHTML, inline scripts or inline handlers — behind a strict CSP and the full protective header set. HTTP and WebSocket access require Basic authentication from a private server-local token file, and live messages are verified for envelope schema, signature, subject/recipient binding, traffic direction and known-peer identity before reaching the UI. Fails closed without a valid token file.

Fixed

  • Codex wake seeded threads are usable (#97) — thread/start no longer discards thread.path, and new threads carry peer.cwd instead of cwd: null, which produced wrong workspace roots, missing project instructions and wrong permissions.
  • Per-peer baseInstructions no longer dropped (#98) — reaching the injector's peer.resume === false opt-out from real configuration for the first time.
  • Broken quickstartgit clone .../murmur.git && cd mur-mur-v2 failed on the very first command every new user ran. The repository was renamed on 2026-08-03; README, publish tooling and the deploy script kept the old slug.

Documentation

  • CHANGELOG.md has a [2.4.0] section for the first time — that release shipped on 2026-06-23 pointing at "See CHANGELOG.md for details" while the section never existed.
  • SECURITY.md now matches how the project actually behaves: private reporting is still preferred, but a missed acknowledgement target explicitly frees the reporter to disclose publicly. Written after we left a real audit unanswered for nine days.

Known gaps, tracked

The delivery-semantics findings from #96 are filed rather than silently carried: #105 (failed wakes advance the cursor; relay not idempotent — one package, not two), #106 (empty finalText logs as relayed), #107 (sequential drain causes head-of-line blocking), #108 (threadId is process-memory only and scoped per peer).

Held deliberately: #103 (TLS + per-peer NATS auth) needs a coordinated broker and peer credential cutover, and #104 is the wire-breaking ACK variant under evaluation against #100.

npm

@murmurv2/core 0.4.0, @murmurv2/broker-nats 0.3.0, @murmurv2/mcp-server 0.2.0.


213/213 tests pass; CI green on 4a069d8.

v2.4.0 — Scoped Channels & Session Affinity

Choose a tag to compare

@alexfrmn alexfrmn released this 23 Jun 22:31
5ec6f84

DB-backed session-ownership lease: for an addressed conversation, only the owning session of the addressed agent responds; every other session and agent stays silent. Solves multi-session double-emit and native wake hitting/spawning the wrong session.

  • SessionLeaseStore (atomic CAS claim_or_skip, heartbeat, per-turn fencing token, session_presence registry, preemptPrefix) — published in @murmurv2/core@0.3.0.
  • Native wake = presence-deferring, lease-gated fallback (createNativeLeaseGate) behind MURMUR_SCOPED_CHANNELS (default OFF, backward-compatible).
  • All delivery paths honor one claim. Verified live: N sessions -> exactly 1 emit, native defer (no competing thread).

See CHANGELOG.md for details.

v2.3.0 — Discovery + Streaming, Auth Enforcement, Conformance & Protocol Spec

Choose a tag to compare

@alexfrmn alexfrmn released this 22 Jun 19:33

Murmur v2.3.0

Second autonomous wave (JARVIS ↔ CODEX-VOLT, PR-per-feature cross-review). Discovery + streaming complete, an auth/authz enforcement mechanism, conformance + a versioned protocol spec across every wire type, and a real cross-host A2A validation.

npm install @murmurv2/core @murmurv2/federation @murmurv2/broker-nats   # @ 0.2.0
npm install @murmurv2/security @murmurv2/mcp-server @murmurv2/broker-ws

Highlights

  • Agent discovery — complete. Presence frames + candidate registry (ttl expiry, dedupe, out-of-order guard), signed presence over NATS (announcePresence/subscribePresence), and an operator promote-flow (queryCandidates + promoteCandidate). Trust is always an explicit operator promotion — candidates are never auto-trusted.
  • Message streaming — complete. Stream frames (start/chunk/end), UTF-8-safe chunking, in-memory + durable SQLite reassembly (out-of-order, idempotent, conflict-reject), backpressure (chunk + byte windows), sha256 integrity, and an ACK window.
  • Auth/authz enforcement mechanism. Signed subject (actor) in auth tokens, an optional signed authToken on EnvelopeV1 (covered by the signature; byte-identical back-compat when absent), authorizeInbound (binds subject === senderAgentId), and broker ingress enforcement behind MURMUR_ENFORCE_AUTH (default-OFF, NACK auth-rejected:<reason>). Daemon end-to-end wiring is the remaining step.
  • Conformance + versioned protocol spec — all wire types. Draft 2020-12 schema and schema↔runtime-guard agreement matrices now cover envelope, ack, presence, and stream frames; docs/protocol-v1.md + docs/protocol-compatibility.md.
  • Single canonical signing payload. stableEnvelopePayload centralized into @murmurv2/core (was copy-pasted across 7 sites), golden-locked by test.

Validated

  • Real cross-host A2A. A fresh Murmur agent on a remote host (Phoenix/agent-hq), running over the published @murmurv2/* packages, connected to the live broker over Tailscale and exchanged bidirectional encrypt/verify/ACK traffic with the mesh — closing the "real mesh deploy" mechanism gate. A second real partner org (federation) and a real remote A2A agent remain external gates.

npm

@murmurv2/core, @murmurv2/federation, @murmurv2/broker-nats published at 0.2.0; security/observability @ 0.1.1; all other @murmurv2/* @ 0.1.0.

Full changelog: CHANGELOG.md

v2.2.0 — Published on npm + WebSocket, Auth Tokens, Protocol Schema

Choose a tag to compare

@alexfrmn alexfrmn released this 22 Jun 00:20
20316bc

Murmur v2.2.0

All packages are now public on npm under the @murmurv2 scope (MIT, @ 0.1.0).

npm install @murmurv2/core @murmurv2/security @murmurv2/mcp-server @murmurv2/broker-nats

Highlights

  • Published on npm — 11 packages public; idempotent publish tooling (prep-publish.mjs + publish-all.mjs with per-tarball build-guard assertion).
  • WebSocket transport@murmurv2/broker-ws: relay + broker client with envelope delivery, ACK correlation, dedupe, invalid-envelope NACKs (browser/edge deployment pending; ships to npm in 0.1.1).
  • Roster-backed auth tokens — Ed25519-signed audience/scope/nbf-exp tokens verified against the federation roster (no embedded trust root).
  • Machine-readable protocol schema + conformance — Draft 2020-12 JSON Schema for EnvelopeV1/AckV1 + compatibility matrix; conformance keeps the schema and the runtime guard in lock-step.
  • Federation live (in isolation) — cross-org sealed+signed delivery proven over real NATS accounts and a leaf-node topology + least-privilege pub/sub boundaries + RosterStore replay guard.
  • ACP autonomy loop + always-on-wake cold-start (reference deployment).

Full changelog: CHANGELOG.md

v2.1.0 — Durability, Federation & A2A

Choose a tag to compare

@alexfrmn alexfrmn released this 21 Jun 19:43

Murmur v2.1.0

Optional JetStream durability, cross-org federation, A2A interop, and self-healing native wake.

Highlights

  • JetStream durability (opt-in): finite max_deliver/ack_wait, consumer-repair, nak-retry, poison-terminal-ACK + advisory→DLQ. Default-OFF (MURMUR_JETSTREAM=1); SQLite outbox stays source of truth.
  • Federation: org/agentId addressing (bare⇒local), Ed25519-signed key directory (pinned-key verify), fed.* NATS leaf-node/account contract — payload E2E-opaque cross-org.
  • A2A bridge: terminates the industry-standard A2A protocol (@a2a-js/sdk) into the Murmur mesh.
  • Native wake self-heal: Codex app-server threads re-seed automatically.

Security

  • verifyRoster now verifies against a caller-pinned org key (not the embedded key) — blocks attacker-forged rosters.

Full changelog: CHANGELOG.md

Murmur V2 — v2.0.0

Choose a tag to compare

@alexfrmn alexfrmn released this 20 Jun 22:35
b713651

First production-hardening release of Murmur V2.

Added

  • Mandatory WakeMonitor — wake-on-message as a first-class daemon subsystem with four guards (deduplication, loop-breaker, audit-gate, drain). Stateless and persistent wake modes. Solves the wake-on-message gap for otherwise 'deaf' agent CLIs.
  • murmur_request send-and-wait tool for synchronous request/response over NATS.

Fixed

  • ACK routing now targets the original sender (not the consumer) — eliminates ack-timeout retry-spam.
  • Reconnect resilience defaults for long-running NATS clients (infinite reconnect, ping interval, graceful drain).

Changed

  • Transport documentation reflects the actual core NATS + SQLite outbox behavior (no false JetStream durability claim).
  • Security: ws upgraded to 8.21.0.

See CHANGELOG.md for the full list. JetStream durable transport, cross-org federation, and A2A interop are planned for v2.1.