AtlatestRepositoryapiary
1# Changelog
2
3## 0.1.7 — bug-fix-spike: spawn-worker default groups, list-members, markdown sweep
4
5Three contained tool-surface fixes surfaced 2026-04-29 during the
6first end-to-end apiary↔enclave session against the production
7v0.2.3 deploy.
8
9### Fixed
11- **`spawn-worker` default `groups` now honors `APIARY_WORKER_GROUP`
12 env var.** Previously, calling `spawn-worker` without a `groups`
13 argument silently sent NO `groups:` segment to EnclaveServ, so
14 the new bot landed in zero groups even though the tool's own
15 description claimed `"defaults to APIARY_WORKER_GROUP
16 server-side"`. The default-resolution now lives apiary-side: an
17 omitted (`#f`) or empty-string `groups` arg falls back to the
18 env var. If the env var is also unset/empty, no `groups:`
19 segment is sent and the new bot has no group memberships
20 (this case is documented in the tool description).
21- **`list-members` queries the leader's actual subordinates.**
22 The v0.1.0 implementation issued EnclaveServ's `bot list`,
23 which is filtered by ownership — and a leader bot doesn't
24 *own* its workers (the human owner does), so a leader with
25 active subordinates was always told `"You own no bots."`
26 Switched to `bot list-reports-to <self-nick>` (added in
27 enclave v0.2.2), which surfaces every bot whose `reports-to`
28 matches the caller. Also swapped the result-formatting path
29 from `(apply string-append ...)` to a `string-join` fold so
30 long subordinate lists don't re-trip the apply-arity ceiling
31 that bit `markdown-irc` in v0.1.3.
32- **`send-message` markdown→mIRC sweep.** Added regression tests
33 for the apply-arity bug fixed in v0.1.3 — the exact trigger
34 text from the bug-fix-spike brief plus six sweep cases
35 (em-dash alone, em-dash + ASCII apostrophe, em-dash + curly
36 single quote, em-dash + curly double quote, em-dash +
37 ellipsis, em-dash + backtick, em-dash + bold) and a
38 ~3000-char multi-sentence stress case. All translate through
39 `markdown->irc` without hitting the Sigil-VM apply-arity
40 ceiling.
42### Changed
44- `list-members` tool description rewritten to reflect the new
45 semantic ("this leader's subordinates" rather than "bots known
46 to EnclaveServ"). Leader-mode instructions in `apiary-instructions`
47 (consumed by the MCP client during `initialize`) updated to
48 match.
49- `spawn-worker`'s `groups` parameter description now states
50 the resolution order (explicit arg → `APIARY_WORKER_GROUP` env
51 → no group memberships) so callers know what an omitted arg
52 means without reading the source.
54### Added
56- `dev-dependencies` declared in `package.sgl` so `sigil test`
57 can build the test harness without a manual `--add-dependency`
58 dance. Pulls `sigil-test`, `sigil-test-runner`, and
59 `sigil-version` (the latter is transitively needed by
60 `sigil-build` but isn't auto-resolved on this host).
61- `or-empty-env` and `env-or-false` exported from `(apiary
62 tools)` so unit tests can directly exercise the
63 groups-defaulting helper used by `spawn-worker`.
64- `test/smoke.sh` pre-creates an `ops-workers` group on the
65 throwaway enclave and exports `APIARY_WORKER_GROUP=ops-workers`
66 so the smoke driver can verify the env-var fallback end-to-end.
67- Three smoke checks in `test/smoke-driver.sgl`:
68 `bug-A-spawn-worker-default-groups` (whois confirms the new
69 bot landed in `ops-workers`), `bug-B-list-members-surfaces-
70 subordinates` (the just-spawned bot shows up in the
71 list-reports-to query), and
72 `bug-C-send-message-markdown-em-dash` (the brief's exact
73 trigger text rounds through `enclave-bridge-send-dm!` without
74 raising).
76## 0.1.4 — registration-timeout resilience for enclave restarts
78The keepalive work in v0.1.2 already covered connection-level
79drops (server EOF, network partition) with the 1→60 s
80exponential backoff reconnect path. This release closes the
81remaining production gap: enclave-server restarts. Each
82restart this session (v0.2.10, .11, .12, .14) caused apiary
83to crash during reconnect when the registration handshake
84hung against a server that was up but not yet accepting
85registrations. The 30 s registration timeout fired, raised an
86exception that escaped `do-reconnect!`'s goroutine, and
87crashed apiary's main event loop — surfacing as a `[ERROR]
88Apiary crashed mode=leader error=… registration timed out` in
89the production log and a tool-surface gap in the leader's
90session that required a manual `/mcp` reconnect to fix.
92### Fixed
94- **`do-reconnect!` no longer raises.** The reconnect path
95 used to raise `"do-reconnect!: registration timed out"` on
96 handshake-deadline expiry; despite a `guard` in
97 `reconnect-loop!` around the call, the exception
98 intermittently escaped (likely a sigil-vm guard-in-goroutine
99 corner case — see `topics/apiary-reconnect-resilience` for
100 the diagnosis trace). The fix shifts to a return-value
101 protocol: `do-reconnect!` returns `'ok` on success or
102 `'retry` on any transient failure (TCP refused, SASL
103 rejection, registration timeout, exception during handshake)
104 and never lets a raise escape. Multiple per-step `guard`s
105 inside the function convert any raise to `'retry`. The
106 outer `reconnect-loop!` keeps a belt-and-suspenders `guard`
107 in case a brand-new failure mode finds a way past.
108- **Partial-irc cleanup on every retry path.** A failed
109 reconnect attempt used to leave a half-built
110 `irc-connection` in `enclave-conn-irc`, which the next
111 attempt would silently overwrite without closing. The new
112 `cleanup-partial-irc!` helper closes the half-built socket
113 cleanly (swallowing any close-time exception) before the
114 retry. Prevents file-descriptor leaks across long outages.
116### Changed
118- **Registration handshake timeout tightened from 30 s to
119 10 s.** Production restart windows are typically ≤10 s; the
120 old 30 s left apiary spinning on a single dead handshake
121 while the enclave was already up and accepting
122 registrations. With 10 s + the 1→60 s backoff, the second
123 reconnect attempt typically lands on a healthy server.
124- **`reconnect-loop!`'s log line for transient failures**
125 now reads `"Apiary reconnect attempt raised — treating as
126 retry"` (vs. the old `"Apiary reconnect attempt failed"`)
127 to clarify that the loop survived the failure rather than
128 abandoned the attempt.
130### Added
132- **`await-registration!`** is a small helper extracted from
133 `do-reconnect!`, exported so future tests can drive it
134 directly. Tight contract: returns `#t` once
135 `irc-connected?` flips, `#f` on deadline. Pulling it out
136 also lets the surrounding `guard` in `do-reconnect!` wrap
137 the wait without obscuring control flow.
138- **Phase 3 in `test/smoke-keepalive-integration.sh`** —
139 holds the server down across multiple reconnect attempts
140 (typically 3-5 within an 18 s outage) and asserts apiary
141 cycles through them all without crashing, then reconnects
142 once the server is back. This exercises the production
143 failure mode in miniature.
145### Notes
147- Patch bump (additive: new helper + new internal
148 protocol; no public-API removal). The `'ok` / `'retry` value
149 is the new internal contract between `do-reconnect!` and
150 `reconnect-loop!` — neither is part of the public surface.
151- See [[topics/apiary-reconnect-resilience]] for the design
152 rationale, why the registration timeout was tightened, and
153 the production trace that motivated the fix.
155## 0.1.2 — IRC PING/PONG keepalive + auto-reconnect with backoff
157Apiary now survives idle periods + detects dead connections via
158client-initiated IRC PING. The diagnosis from
159`investigations/apiary-enclave-silent-disconnect-no-keepalive-2026-04-30`
160documented the prior failure mode: relayd's 10-minute upstream
161timeout reaped idle apiary sessions, the resulting FIN was
162swallowed by NAT conntrack expiry, and apiary's kernel-level
163socket sat in ESTABLISHED forever — `send-channel` calls
164silently disappeared into the bit-bucket. The companion fix
165ships in enclave-server v0.2.8.
167### Added
169- **Client-side PING every 60 s of inbound silence.** The event
170 loop's keepalive sweep (running every 5 s as part of the
171 existing 50 ms tick) emits `PING :<token>` whenever the
172 connection has been idle (no inbound or outbound traffic) for
173 more than 60 s. Tokens are jiffy + random integer (~64 bits
174 of session-local entropy) so a misbehaved channel peer can't
175 trivially forge a PONG.
176- **Server-PING handler.** `handle-server-ping` echoes the
177 trailing token back as `PONG :<token>` so the new
178 enclave-side keepalive treats apiary as alive. sigil-irc does
179 not auto-respond to inbound PING, so this handler closes the
180 regression gap.
181- **Server-PONG handler.** `handle-server-pong` clears the
182 pending-ping bookkeeping when the trailing token matches our
183 in-flight client PING. Mismatched / stale PONGs are debug-
184 logged and ignored — the activity-touch on the same line
185 already handles the "still alive" signal.
186- **Auto-reconnect with exponential backoff.** When the
187 pending PING goes 30 s without a matching PONG, OR
188 sigil-irc's state flips to `'disconnected` (clean server
189 close, EOF, or network drop), `trigger-reconnect!` spawns
190 a goroutine that retries `do-reconnect!` with backoff
191 starting at 1 s and doubling up to 60 s. On success the
192 attempt counter resets so the NEXT outage starts at 1 s
193 again. The same goroutine handles all reconnect paths
194 (PING-timeout, EOF-detected, network-failed) so there's
195 one place to reason about backoff state.
196- **Activity tracking on every send + receive.** New
197 `enclave-conn-touch-activity!` is called from every inbound
198 PRIVMSG / BATCH / CAP / FAIL / PING / PONG handler AND from
199 every outbound `enclave-post*` helper, so the keepalive
200 idle clock truly tracks "any IRC traffic in either direction"
201 — not just chat or just inbound.
202- **Reconnect rejoins configured channels.** `do-reconnect!`
203 re-runs the CAP REQ post-registration handshake AND
204 `irc-join`s every channel in `APIARY_CHANNEL` (CSV-aware) so
205 after an outage the bot is back in #hive without manual
206 intervention. The trusted-set is preserved across reconnect
207 (it's session-local in apiary state, not in the irc-
208 connection); listen-peer additions survive too.
209- **`enclave-disconnect` flips a `shutdown?` flag** so a
210 caller-initiated teardown doesn't trigger the reconnect
211 goroutine on the way out. The keepalive sweep + reconnect
212 loop both check the flag and bail.
213- **`test/smoke-keepalive-integration.sh`** drives the cross-
214 repo integration: server-PING reaches apiary, apiary PONGs,
215 no ping-timeout-disconnect; then kill -9 the server, verify
216 apiary detects + logs reconnect-trigger, restart server,
217 verify apiary reconnects. 7 wire-level assertions, runs in
218 ~30 s with tight 5/5 s enclave knobs.
219- **Unit tests** for fresh-conn defaults, `touch-activity!`,
220 the keepalive-tick decision tree (idle-under-threshold no-
221 op, shutdown? short-circuit, reconnecting? short-circuit,
222 pending-ping past timeout flips reconnecting?), and
223 shutdown-prevents-reconnect.
225### Changed
227- `enclave-conn` struct's `irc` slot is now mutable so the
228 reconnect path can swap in a fresh `make-irc-connection`
229 without losing the surrounding bridge state (channel
230 watchers, MCP server reference, trusted set).
231- The event loop re-reads `enclave-conn-irc` on every iteration
232 and yields while `reconnecting?` is set, so the same loop
233 serves across reconnects without spawning a second one.
235### Notes
237- Pre-1.0 patch bump (additive: new fields + new handlers +
238 reconnect path; no API removal). Existing callers that
239 depend on `enclave-conn-irc` being stable across the lifetime
240 of the conn now need to assume it can swap during a reconnect
241 window — but the only callers (apiary's own helpers) re-read
242 the field every time, so this is a contract clarification
243 rather than a behavior change.
244- The keepalive constants (60 s idle threshold, 30 s PONG
245 timeout, 5 s sweep cadence) are baked into the source per the
246 brief — the diagnosis budget (relayd 10 min, typical NAT
247 conntrack ~15 min) puts both well below either timeout, so
248 there's no operator knob to tune. Reconnect backoff (1 s →
249 60 s cap) is also baked.
250- Companion fix: `[[tasks/enclave-server-side-ping-out]]` ships
251 the symmetric server-side keepalive as enclave-server v0.2.8.
252 Either alone is insufficient: server-PING alone leaves
253 consumer-NAT entries cold between server-PING bursts; client-
254 PING alone leaves zombie clients in the server's roster.
256## 0.1.1 — multiline send/receive + reacts
258First patch release after the initial extraction. Multi-line
259agent briefings now ride a single IRCv3 `draft/multiline` BATCH
260on the wire (graceful-degrades to per-line PRIVMSG when the
261server NAKs the cap or FAILs the OPEN), and reactions are a
262first-class MCP tool + a distinct inbound event type.
264### Added
266- **`enclave-post-multiline conn target lines` helper.** Wraps
267 N PRIVMSGs in a `BATCH +<reftag> draft/multiline <target>`
268 envelope so receivers (Goguma, Senpai, Catgirl with the cap
269 negotiated) coalesce the group as one collapsed
270 notification + threaded block instead of N detached alerts.
271 Falls back to per-line PRIVMSG when the server NAKed the
272 cap or returned `FAIL BATCH` on the OPEN. Returns
273 `'sent-batch` / `'sent-per-line` / `'sent-empty` so the MCP
274 tool result reflects what actually shipped.
275- **`send-channel` and `send-message` use the multiline path.**
276 Multi-line text becomes one logical message; the `mention:`
277 prefix attaches to the FIRST line only — subsequent lines
278 belong to the same logical message and don't re-prefix.
279- **Inbound BATCH coalescing.** `(apiary enclave)` installs a
280 `'BATCH` handler. Open batches buffer per reftag, each
281 batch-tagged PRIVMSG appends, and on close a synthetic
282 joined PRIVMSG is dispatched through the normal
283 channel-handler path so trusted-set + mention rules stay
284 authoritative. The agent sees one channel-notify per
285 logical message regardless of how the wire was framed.
286- **Post-registration `CAP REQ`** for `message-tags`, `batch`,
287 `draft/multiline`, `server-time`, `echo-message`,
288 `draft/react`, `draft/reaction`, and `draft/reply`. sigil-irc
289 only requests `sasl` during the initial CAP LS round — the
290 rest layer on after 001 lands. The bridge tracks ACKed caps
291 in `enclave-conn-caps-acked` and exposes
292 `enclave-conn-cap-acked? conn cap-name` so callers can
293 introspect.
294- **`send-react` MCP tool.** Emits a PRIVMSG with the
295 client-only tags `+draft/react=<emoji>;+draft/reaction=<emoji>;+draft/reply=<msgid>`
296 pointing at a prior server-assigned msgid. Both the older
297 `+draft/react` slug and the newer `+draft/reaction` ride
298 the wire so any client variant renders the body as a
299 reaction badge. Useful for fast acks and silent signals
300 without channel chatter.
301- **Inbound react detection.** `+draft/react` OR
302 `+draft/reaction` on an incoming PRIVMSG promotes the event
303 to `type=enclave-react` with `emoji` + `target-msgid` meta,
304 so the agent handles reactions distinctly from regular
305 messages.
306- **`msgid` on every channel-notify event.** Both
307 `enclave-channel` and `enclave-dm` events now surface the
308 server-assigned msgid in meta when present, so the agent
309 can construct a valid `send-react` against any prior
310 message — including DMs from owner.
311- **FAIL detection + per-line fallback.** When the server
312 emits `FAIL BATCH …` (target the validator rejects, type
313 unsupported, etc.), the bridge aborts the in-flight batch
314 and re-sends the lines as per-line PRIVMSGs. Replaces the
315 earlier "9 cascading BATCH_NOT_OPEN errors → user gets
316 nothing" failure mode.
317- **`msgid` field in apiary's debug log line for inbound
318 PRIVMSGs.** `[DEBUG] Enclave PRIVMSG in sender=… target=…
319 msgid=… text-len=…` removes the ssh-roundtrip-to-the-server
320 step when triaging react flows.
321- **Updated instructions string** for both leader and worker
322 modes documents the multiline batch path, react flow
323 (inbound + outbound), and the msgid/meta contract.
325### Wire-level smoke
327`test/smoke-driver.sgl` exercises 13 PASS checks against a
328locally-spun enclave-server: bridge connect + presence
329broadcast, owner mention, listen-peer mutation,
330multiline-cap-negotiated, multiline-batch-emit (asserts the
331BATCH OPEN/CLOSE + tagged PRIVMSGs reach a peer eyeball),
332single-line-no-batch (no envelope when content has no
333newlines), batch-fail-flag-set (FAIL handler captures
334unsupported-type rejections), multiline-fallback-per-line
335(returns `'sent-per-line` and emits per-line when the cap
336isn't ACKed), dm-msgid-surfaced, and send-react-tagged
337(asserts both `+draft/react` and `+draft/reaction` slugs +
338`+draft/reply` are on the outbound wire).
340### Known limitation
342**Goguma's react UI gating.** Goguma exposes its long-press
343react UI only on messages where it has previously seen a
344react (per `networkMsgid` storage), and only when the
345server's ISUPPORT permits the relevant client tags. When
346triaging "Goguma's react UI is greyed out", confirm
347enclave's CAP LS (now logged at INFO) advertises
348`draft/react` + `draft/reaction` + `draft/reply` AND that
349the user's connection negotiated them. The first react in a
350conversation may need to come from a different client to
351prime Goguma's per-message gate.
353**Multi-line BATCH coalesce-on-render** requires the receiving
354client to have negotiated `draft/multiline` itself. Senpai 0.x
355and Goguma 0.x as of 2026-04-29 don't include the cap in their
356default REQ list, so apiary's BATCHes still render as N
357standalone messages in those clients regardless of the relay
358path. The fix is upstream: clients adding `draft/multiline` to
359their default REQ. The new `cap-req` / `cap-end` INFO logs in
360enclave 0.2.4 let operators verify which caps each connection
361actually negotiated.
363## 0.1.0 — initial extraction
365First release. Extracted from the courier-enclave-migration
366work; ships the leader/worker MCP server with trusted-set
367filter, mention syntax, mode-vs-voicing model, and the
368EnclaveServ services-call wrapper. See
369[apiary-design](https://codeberg.org/sigil/folio/notes/topics/apiary-design)
370for the architecture spec.