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-workerdefaultgroupsnow honorsAPIARY_WORKER_GROUPenv var. Previously, callingspawn-workerwithout agroupsargument silently sent NOgroups: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-stringgroupsarg falls back to the env var. If the env var is also unset/empty, nogroups:segment is sent and the new bot has no group memberships (this case is documented in the tool description).list-membersqueries the leader's actual subordinates. The v0.1.0 implementation issued EnclaveServ'sbot 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 tobot list-reports-to <self-nick>(added in enclave v0.2.2), which surfaces every bot whosereports-tomatches the caller. Also swapped the result-formatting path from(apply string-append ...)to astring-joinfold so long subordinate lists don't re-trip the apply-arity ceiling that bitmarkdown-ircin v0.1.3.send-messagemarkdown→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 throughmarkdown->ircwithout hitting the Sigil-VM apply-arity ceiling.
Changed
list-memberstool description rewritten to reflect the new semantic ("this leader's subordinates" rather than "bots known to EnclaveServ"). Leader-mode instructions inapiary-instructions(consumed by the MCP client duringinitialize) updated to match.spawn-worker'sgroupsparameter description now states the resolution order (explicit arg →APIARY_WORKER_GROUPenv → no group memberships) so callers know what an omitted arg means without reading the source.
Added
dev-dependenciesdeclared inpackage.sglsosigil testcan build the test harness without a manual--add-dependencydance. Pullssigil-test,sigil-test-runner, andsigil-version(the latter is transitively needed bysigil-buildbut isn't auto-resolved on this host).or-empty-envandenv-or-falseexported from(apiary tools)so unit tests can directly exercise the groups-defaulting helper used byspawn-worker.test/smoke.shpre-creates anops-workersgroup on the throwaway enclave and exportsAPIARY_WORKER_GROUP=ops-workersso 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 inops-workers),bug-B-list-members-surfaces- subordinates(the just-spawned bot shows up in the list-reports-to query), andbug-C-send-message-markdown-em-dash(the brief's exact trigger text rounds throughenclave-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 aguardinreconnect-loop!around the call, the exception intermittently escaped (likely a sigil-vm guard-in-goroutine corner case — seetopics/apiary-reconnect-resiliencefor the diagnosis trace). The fix shifts to a return-value protocol:do-reconnect!returns'okon success or'retryon any transient failure (TCP refused, SASL rejection, registration timeout, exception during handshake) and never lets a raise escape. Multiple per-stepguards inside the function convert any raise to'retry. The outerreconnect-loop!keeps a belt-and-suspendersguardin 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-connectioninenclave-conn-irc, which the next attempt would silently overwrite without closing. The newcleanup-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 fromdo-reconnect!, exported so future tests can drive it directly. Tight contract: returns#tonceirc-connected?flips,#fon deadline. Pulling it out also lets the surroundingguardindo-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/'retryvalue is the new internal contract betweendo-reconnect!andreconnect-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-pingechoes the trailing token back asPONG :<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-pongclears 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 retriesdo-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 outboundenclave-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 ANDirc-joins every channel inAPIARY_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-disconnectflips ashutdown?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.shdrives 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-connstruct'sircslot is now mutable so the reconnect path can swap in a freshmake-irc-connectionwithout losing the surrounding bridge state (channel watchers, MCP server reference, trusted set).- The event loop re-reads
enclave-conn-ircon every iteration and yields whilereconnecting?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-ircbeing 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 lineshelper. Wraps N PRIVMSGs in aBATCH +<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 returnedFAIL BATCHon the OPEN. Returns'sent-batch/'sent-per-line/'sent-emptyso the MCP tool result reflects what actually shipped.send-channelandsend-messageuse the multiline path. Multi-line text becomes one logical message; themention: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'BATCHhandler. 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 REQformessage-tags,batch,draft/multiline,server-time,echo-message,draft/react,draft/reaction, anddraft/reply. sigil-irc only requestssaslduring the initial CAP LS round — the rest layer on after 001 lands. The bridge tracks ACKed caps inenclave-conn-caps-ackedand exposesenclave-conn-cap-acked? conn cap-nameso callers can introspect. send-reactMCP 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/reactslug and the newer+draft/reactionride 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/reactOR+draft/reactionon an incoming PRIVMSG promotes the event totype=enclave-reactwithemoji+target-msgidmeta, so the agent handles reactions distinctly from regular messages. msgidon every channel-notify event. Bothenclave-channelandenclave-dmevents now surface the server-assigned msgid in meta when present, so the agent can construct a validsend-reactagainst 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. msgidfield 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.