AtlatestRenderedmarkdown
Readme

Changelog

0.1.7 — bug-fix-spike: spawn-worker default groups, list-members, markdown sweep

Three contained tool-surface fixes surfaced 2026-04-29 during the first end-to-end apiary↔enclave session against the production v0.2.3 deploy.

Fixed

  • spawn-worker default groups now honors APIARY_WORKER_GROUP env var. Previously, calling spawn-worker without a groups argument silently sent NO groups: segment to EnclaveServ, so the new bot landed in zero groups even though the tool's own description claimed "defaults to APIARY_WORKER_GROUP server-side". The default-resolution now lives apiary-side: an omitted (#f) or empty-string groups arg falls back to the env var. If the env var is also unset/empty, no groups: segment is sent and the new bot has no group memberships (this case is documented in the tool description).
  • list-members queries the leader's actual subordinates. The v0.1.0 implementation issued EnclaveServ's bot list, which is filtered by ownership — and a leader bot doesn't own its workers (the human owner does), so a leader with active subordinates was always told "You own no bots." Switched to bot list-reports-to <self-nick> (added in enclave v0.2.2), which surfaces every bot whose reports-to matches the caller. Also swapped the result-formatting path from (apply string-append ...) to a string-join fold so long subordinate lists don't re-trip the apply-arity ceiling that bit markdown-irc in v0.1.3.
  • send-message markdown→mIRC sweep. Added regression tests for the apply-arity bug fixed in v0.1.3 — the exact trigger text from the bug-fix-spike brief plus six sweep cases (em-dash alone, em-dash + ASCII apostrophe, em-dash + curly single quote, em-dash + curly double quote, em-dash + ellipsis, em-dash + backtick, em-dash + bold) and a ~3000-char multi-sentence stress case. All translate through markdown->irc without hitting the Sigil-VM apply-arity ceiling.

Changed

  • list-members tool description rewritten to reflect the new semantic ("this leader's subordinates" rather than "bots known to EnclaveServ"). Leader-mode instructions in apiary-instructions (consumed by the MCP client during initialize) updated to match.
  • spawn-worker's groups parameter description now states the resolution order (explicit arg → APIARY_WORKER_GROUP env → no group memberships) so callers know what an omitted arg means without reading the source.

Added

  • dev-dependencies declared in package.sgl so sigil test can build the test harness without a manual --add-dependency dance. Pulls sigil-test, sigil-test-runner, and sigil-version (the latter is transitively needed by sigil-build but isn't auto-resolved on this host).
  • or-empty-env and env-or-false exported from (apiary tools) so unit tests can directly exercise the groups-defaulting helper used by spawn-worker.
  • test/smoke.sh pre-creates an ops-workers group on the throwaway enclave and exports APIARY_WORKER_GROUP=ops-workers so the smoke driver can verify the env-var fallback end-to-end.
  • Three smoke checks in test/smoke-driver.sgl: bug-A-spawn-worker-default-groups (whois confirms the new bot landed in ops-workers), bug-B-list-members-surfaces- subordinates (the just-spawned bot shows up in the list-reports-to query), and bug-C-send-message-markdown-em-dash (the brief's exact trigger text rounds through enclave-bridge-send-dm! without raising).

0.1.4 — registration-timeout resilience for enclave restarts

The keepalive work in v0.1.2 already covered connection-level drops (server EOF, network partition) with the 1→60 s exponential backoff reconnect path. This release closes the remaining production gap: enclave-server restarts. Each restart this session (v0.2.10, .11, .12, .14) caused apiary to crash during reconnect when the registration handshake hung against a server that was up but not yet accepting registrations. The 30 s registration timeout fired, raised an exception that escaped do-reconnect!'s goroutine, and crashed apiary's main event loop — surfacing as a [ERROR] Apiary crashed mode=leader error=… registration timed out in the production log and a tool-surface gap in the leader's session that required a manual /mcp reconnect to fix.

Fixed

  • do-reconnect! no longer raises. The reconnect path used to raise "do-reconnect!: registration timed out" on handshake-deadline expiry; despite a guard in reconnect-loop! around the call, the exception intermittently escaped (likely a sigil-vm guard-in-goroutine corner case — see topics/apiary-reconnect-resilience for the diagnosis trace). The fix shifts to a return-value protocol: do-reconnect! returns 'ok on success or 'retry on any transient failure (TCP refused, SASL rejection, registration timeout, exception during handshake) and never lets a raise escape. Multiple per-step guards inside the function convert any raise to 'retry. The outer reconnect-loop! keeps a belt-and-suspenders guard in case a brand-new failure mode finds a way past.
  • Partial-irc cleanup on every retry path. A failed reconnect attempt used to leave a half-built irc-connection in enclave-conn-irc, which the next attempt would silently overwrite without closing. The new cleanup-partial-irc! helper closes the half-built socket cleanly (swallowing any close-time exception) before the retry. Prevents file-descriptor leaks across long outages.

Changed

  • Registration handshake timeout tightened from 30 s to 10 s. Production restart windows are typically ≤10 s; the old 30 s left apiary spinning on a single dead handshake while the enclave was already up and accepting registrations. With 10 s + the 1→60 s backoff, the second reconnect attempt typically lands on a healthy server.
  • reconnect-loop!'s log line for transient failures now reads "Apiary reconnect attempt raised — treating as retry" (vs. the old "Apiary reconnect attempt failed") to clarify that the loop survived the failure rather than abandoned the attempt.

Added

  • await-registration! is a small helper extracted from do-reconnect!, exported so future tests can drive it directly. Tight contract: returns #t once irc-connected? flips, #f on deadline. Pulling it out also lets the surrounding guard in do-reconnect! wrap the wait without obscuring control flow.
  • Phase 3 in test/smoke-keepalive-integration.sh — holds the server down across multiple reconnect attempts (typically 3-5 within an 18 s outage) and asserts apiary cycles through them all without crashing, then reconnects once the server is back. This exercises the production failure mode in miniature.

Notes

  • Patch bump (additive: new helper + new internal protocol; no public-API removal). The 'ok / 'retry value is the new internal contract between do-reconnect! and reconnect-loop! — neither is part of the public surface.
  • See [[topics/apiary-reconnect-resilience]] for the design rationale, why the registration timeout was tightened, and the production trace that motivated the fix.

0.1.2 — IRC PING/PONG keepalive + auto-reconnect with backoff

Apiary now survives idle periods + detects dead connections via client-initiated IRC PING. The diagnosis from investigations/apiary-enclave-silent-disconnect-no-keepalive-2026-04-30 documented the prior failure mode: relayd's 10-minute upstream timeout reaped idle apiary sessions, the resulting FIN was swallowed by NAT conntrack expiry, and apiary's kernel-level socket sat in ESTABLISHED forever — send-channel calls silently disappeared into the bit-bucket. The companion fix ships in enclave-server v0.2.8.

Added

  • Client-side PING every 60 s of inbound silence. The event loop's keepalive sweep (running every 5 s as part of the existing 50 ms tick) emits PING :<token> whenever the connection has been idle (no inbound or outbound traffic) for more than 60 s. Tokens are jiffy + random integer (~64 bits of session-local entropy) so a misbehaved channel peer can't trivially forge a PONG.
  • Server-PING handler. handle-server-ping echoes the trailing token back as PONG :<token> so the new enclave-side keepalive treats apiary as alive. sigil-irc does not auto-respond to inbound PING, so this handler closes the regression gap.
  • Server-PONG handler. handle-server-pong clears the pending-ping bookkeeping when the trailing token matches our in-flight client PING. Mismatched / stale PONGs are debug- logged and ignored — the activity-touch on the same line already handles the "still alive" signal.
  • Auto-reconnect with exponential backoff. When the pending PING goes 30 s without a matching PONG, OR sigil-irc's state flips to 'disconnected (clean server close, EOF, or network drop), trigger-reconnect! spawns a goroutine that retries do-reconnect! with backoff starting at 1 s and doubling up to 60 s. On success the attempt counter resets so the NEXT outage starts at 1 s again. The same goroutine handles all reconnect paths (PING-timeout, EOF-detected, network-failed) so there's one place to reason about backoff state.
  • Activity tracking on every send + receive. New enclave-conn-touch-activity! is called from every inbound PRIVMSG / BATCH / CAP / FAIL / PING / PONG handler AND from every outbound enclave-post* helper, so the keepalive idle clock truly tracks "any IRC traffic in either direction" — not just chat or just inbound.
  • Reconnect rejoins configured channels. do-reconnect! re-runs the CAP REQ post-registration handshake AND irc-joins every channel in APIARY_CHANNEL (CSV-aware) so after an outage the bot is back in #hive without manual intervention. The trusted-set is preserved across reconnect (it's session-local in apiary state, not in the irc- connection); listen-peer additions survive too.
  • enclave-disconnect flips a shutdown? flag so a caller-initiated teardown doesn't trigger the reconnect goroutine on the way out. The keepalive sweep + reconnect loop both check the flag and bail.
  • test/smoke-keepalive-integration.sh drives the cross- repo integration: server-PING reaches apiary, apiary PONGs, no ping-timeout-disconnect; then kill -9 the server, verify apiary detects + logs reconnect-trigger, restart server, verify apiary reconnects. 7 wire-level assertions, runs in ~30 s with tight 5/5 s enclave knobs.
  • Unit tests for fresh-conn defaults, touch-activity!, the keepalive-tick decision tree (idle-under-threshold no- op, shutdown? short-circuit, reconnecting? short-circuit, pending-ping past timeout flips reconnecting?), and shutdown-prevents-reconnect.

Changed

  • enclave-conn struct's irc slot is now mutable so the reconnect path can swap in a fresh make-irc-connection without losing the surrounding bridge state (channel watchers, MCP server reference, trusted set).
  • The event loop re-reads enclave-conn-irc on every iteration and yields while reconnecting? is set, so the same loop serves across reconnects without spawning a second one.

Notes

  • Pre-1.0 patch bump (additive: new fields + new handlers + reconnect path; no API removal). Existing callers that depend on enclave-conn-irc being stable across the lifetime of the conn now need to assume it can swap during a reconnect window — but the only callers (apiary's own helpers) re-read the field every time, so this is a contract clarification rather than a behavior change.
  • The keepalive constants (60 s idle threshold, 30 s PONG timeout, 5 s sweep cadence) are baked into the source per the brief — the diagnosis budget (relayd 10 min, typical NAT conntrack ~15 min) puts both well below either timeout, so there's no operator knob to tune. Reconnect backoff (1 s → 60 s cap) is also baked.
  • Companion fix: [[tasks/enclave-server-side-ping-out]] ships the symmetric server-side keepalive as enclave-server v0.2.8. Either alone is insufficient: server-PING alone leaves consumer-NAT entries cold between server-PING bursts; client- PING alone leaves zombie clients in the server's roster.

0.1.1 — multiline send/receive + reacts

First patch release after the initial extraction. Multi-line agent briefings now ride a single IRCv3 draft/multiline BATCH on the wire (graceful-degrades to per-line PRIVMSG when the server NAKs the cap or FAILs the OPEN), and reactions are a first-class MCP tool + a distinct inbound event type.

Added

  • enclave-post-multiline conn target lines helper. Wraps N PRIVMSGs in a BATCH +<reftag> draft/multiline <target> envelope so receivers (Goguma, Senpai, Catgirl with the cap negotiated) coalesce the group as one collapsed notification + threaded block instead of N detached alerts. Falls back to per-line PRIVMSG when the server NAKed the cap or returned FAIL BATCH on the OPEN. Returns 'sent-batch / 'sent-per-line / 'sent-empty so the MCP tool result reflects what actually shipped.
  • send-channel and send-message use the multiline path. Multi-line text becomes one logical message; the mention: prefix attaches to the FIRST line only — subsequent lines belong to the same logical message and don't re-prefix.
  • Inbound BATCH coalescing. (apiary enclave) installs a 'BATCH handler. Open batches buffer per reftag, each batch-tagged PRIVMSG appends, and on close a synthetic joined PRIVMSG is dispatched through the normal channel-handler path so trusted-set + mention rules stay authoritative. The agent sees one channel-notify per logical message regardless of how the wire was framed.
  • Post-registration CAP REQ for message-tags, batch, draft/multiline, server-time, echo-message, draft/react, draft/reaction, and draft/reply. sigil-irc only requests sasl during the initial CAP LS round — the rest layer on after 001 lands. The bridge tracks ACKed caps in enclave-conn-caps-acked and exposes enclave-conn-cap-acked? conn cap-name so callers can introspect.
  • send-react MCP tool. Emits a PRIVMSG with the client-only tags +draft/react=<emoji>;+draft/reaction=<emoji>;+draft/reply=<msgid> pointing at a prior server-assigned msgid. Both the older +draft/react slug and the newer +draft/reaction ride the wire so any client variant renders the body as a reaction badge. Useful for fast acks and silent signals without channel chatter.
  • Inbound react detection. +draft/react OR +draft/reaction on an incoming PRIVMSG promotes the event to type=enclave-react with emoji + target-msgid meta, so the agent handles reactions distinctly from regular messages.
  • msgid on every channel-notify event. Both enclave-channel and enclave-dm events now surface the server-assigned msgid in meta when present, so the agent can construct a valid send-react against any prior message — including DMs from owner.
  • FAIL detection + per-line fallback. When the server emits FAIL BATCH … (target the validator rejects, type unsupported, etc.), the bridge aborts the in-flight batch and re-sends the lines as per-line PRIVMSGs. Replaces the earlier "9 cascading BATCHNOTOPEN errors → user gets nothing" failure mode.
  • msgid field in apiary's debug log line for inbound PRIVMSGs. [DEBUG] Enclave PRIVMSG in sender=… target=… msgid=… text-len=… removes the ssh-roundtrip-to-the-server step when triaging react flows.
  • Updated instructions string for both leader and worker modes documents the multiline batch path, react flow (inbound + outbound), and the msgid/meta contract.

Wire-level smoke

test/smoke-driver.sgl exercises 13 PASS checks against a locally-spun enclave-server: bridge connect + presence broadcast, owner mention, listen-peer mutation, multiline-cap-negotiated, multiline-batch-emit (asserts the BATCH OPEN/CLOSE + tagged PRIVMSGs reach a peer eyeball), single-line-no-batch (no envelope when content has no newlines), batch-fail-flag-set (FAIL handler captures unsupported-type rejections), multiline-fallback-per-line (returns 'sent-per-line and emits per-line when the cap isn't ACKed), dm-msgid-surfaced, and send-react-tagged (asserts both +draft/react and +draft/reaction slugs + +draft/reply are on the outbound wire).

Known limitation

Goguma's react UI gating. Goguma exposes its long-press react UI only on messages where it has previously seen a react (per networkMsgid storage), and only when the server's ISUPPORT permits the relevant client tags. When triaging "Goguma's react UI is greyed out", confirm enclave's CAP LS (now logged at INFO) advertises draft/react + draft/reaction + draft/reply AND that the user's connection negotiated them. The first react in a conversation may need to come from a different client to prime Goguma's per-message gate.

Multi-line BATCH coalesce-on-render requires the receiving client to have negotiated draft/multiline itself. Senpai 0.x and Goguma 0.x as of 2026-04-29 don't include the cap in their default REQ list, so apiary's BATCHes still render as N standalone messages in those clients regardless of the relay path. The fix is upstream: clients adding draft/multiline to their default REQ. The new cap-req / cap-end INFO logs in enclave 0.2.4 let operators verify which caps each connection actually negotiated.

0.1.0 — initial extraction

First release. Extracted from the courier-enclave-migration work; ships the leader/worker MCP server with trusted-set filter, mention syntax, mode-vs-voicing model, and the EnclaveServ services-call wrapper. See apiary-design for the architecture spec.