AtlatestRepositoryapiary
1
# Changelog3
## 0.1.7 — bug-fix-spike: spawn-worker default groups, list-members, markdown sweep5
Three contained tool-surface fixes surfaced 2026-04-29 during the6
first end-to-end apiary↔enclave session against the production7
v0.2.3 deploy.9
### Fixed11
- **`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, so14
the new bot landed in zero groups even though the tool's own15
description claimed `"defaults to APIARY_WORKER_GROUP16
server-side"`. The default-resolution now lives apiary-side: an17
omitted (`#f`) or empty-string `groups` arg falls back to the18
env var. If the env var is also unset/empty, no `groups:`19
segment is sent and the new bot has no group memberships20
(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't24
*own* its workers (the human owner does), so a leader with25
active subordinates was always told `"You own no bots."`26
Switched to `bot list-reports-to <self-nick>` (added in27
enclave v0.2.2), which surfaces every bot whose `reports-to`28
matches the caller. Also swapped the result-formatting path29
from `(apply string-append ...)` to a `string-join` fold so30
long subordinate lists don't re-trip the apply-arity ceiling31
that bit `markdown-irc` in v0.1.3.32
- **`send-message` markdown→mIRC sweep.** Added regression tests33
for the apply-arity bug fixed in v0.1.3 — the exact trigger34
text from the bug-fix-spike brief plus six sweep cases35
(em-dash alone, em-dash + ASCII apostrophe, em-dash + curly36
single quote, em-dash + curly double quote, em-dash +37
ellipsis, em-dash + backtick, em-dash + bold) and a38
~3000-char multi-sentence stress case. All translate through39
`markdown->irc` without hitting the Sigil-VM apply-arity40
ceiling.42
### Changed44
- `list-members` tool description rewritten to reflect the new45
semantic ("this leader's subordinates" rather than "bots known46
to EnclaveServ"). Leader-mode instructions in `apiary-instructions`47
(consumed by the MCP client during `initialize`) updated to48
match.49
- `spawn-worker`'s `groups` parameter description now states50
the resolution order (explicit arg → `APIARY_WORKER_GROUP` env51
→ no group memberships) so callers know what an omitted arg52
means without reading the source.54
### Added56
- `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`, and59
`sigil-version` (the latter is transitively needed by60
`sigil-build` but isn't auto-resolved on this host).61
- `or-empty-env` and `env-or-false` exported from `(apiary62
tools)` so unit tests can directly exercise the63
groups-defaulting helper used by `spawn-worker`.64
- `test/smoke.sh` pre-creates an `ops-workers` group on the65
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 new69
bot landed in `ops-workers`), `bug-B-list-members-surfaces-70
subordinates` (the just-spawned bot shows up in the71
list-reports-to query), and72
`bug-C-send-message-markdown-em-dash` (the brief's exact73
trigger text rounds through `enclave-bridge-send-dm!` without74
raising).76
## 0.1.4 — registration-timeout resilience for enclave restarts78
The keepalive work in v0.1.2 already covered connection-level79
drops (server EOF, network partition) with the 1→60 s80
exponential backoff reconnect path. This release closes the81
remaining production gap: enclave-server restarts. Each82
restart this session (v0.2.10, .11, .12, .14) caused apiary83
to crash during reconnect when the registration handshake84
hung against a server that was up but not yet accepting85
registrations. The 30 s registration timeout fired, raised an86
exception that escaped `do-reconnect!`'s goroutine, and87
crashed apiary's main event loop — surfacing as a `[ERROR]88
Apiary crashed mode=leader error=… registration timed out` in89
the production log and a tool-surface gap in the leader's90
session that required a manual `/mcp` reconnect to fix.92
### Fixed94
- **`do-reconnect!` no longer raises.** The reconnect path95
used to raise `"do-reconnect!: registration timed out"` on96
handshake-deadline expiry; despite a `guard` in97
`reconnect-loop!` around the call, the exception98
intermittently escaped (likely a sigil-vm guard-in-goroutine99
corner case — see `topics/apiary-reconnect-resilience` for100
the diagnosis trace). The fix shifts to a return-value101
protocol: `do-reconnect!` returns `'ok` on success or102
`'retry` on any transient failure (TCP refused, SASL103
rejection, registration timeout, exception during handshake)104
and never lets a raise escape. Multiple per-step `guard`s105
inside the function convert any raise to `'retry`. The106
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 failed109
reconnect attempt used to leave a half-built110
`irc-connection` in `enclave-conn-irc`, which the next111
attempt would silently overwrite without closing. The new112
`cleanup-partial-irc!` helper closes the half-built socket113
cleanly (swallowing any close-time exception) before the114
retry. Prevents file-descriptor leaks across long outages.116
### Changed118
- **Registration handshake timeout tightened from 30 s to119
10 s.** Production restart windows are typically ≤10 s; the120
old 30 s left apiary spinning on a single dead handshake121
while the enclave was already up and accepting122
registrations. With 10 s + the 1→60 s backoff, the second123
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 as126
retry"` (vs. the old `"Apiary reconnect attempt failed"`)127
to clarify that the loop survived the failure rather than128
abandoned the attempt.130
### Added132
- **`await-registration!`** is a small helper extracted from133
`do-reconnect!`, exported so future tests can drive it134
directly. Tight contract: returns `#t` once135
`irc-connected?` flips, `#f` on deadline. Pulling it out136
also lets the surrounding `guard` in `do-reconnect!` wrap137
the wait without obscuring control flow.138
- **Phase 3 in `test/smoke-keepalive-integration.sh`** —139
holds the server down across multiple reconnect attempts140
(typically 3-5 within an 18 s outage) and asserts apiary141
cycles through them all without crashing, then reconnects142
once the server is back. This exercises the production143
failure mode in miniature.145
### Notes147
- Patch bump (additive: new helper + new internal148
protocol; no public-API removal). The `'ok` / `'retry` value149
is the new internal contract between `do-reconnect!` and150
`reconnect-loop!` — neither is part of the public surface.151
- See [[topics/apiary-reconnect-resilience]] for the design152
rationale, why the registration timeout was tightened, and153
the production trace that motivated the fix.155
## 0.1.2 — IRC PING/PONG keepalive + auto-reconnect with backoff157
Apiary now survives idle periods + detects dead connections via158
client-initiated IRC PING. The diagnosis from159
`investigations/apiary-enclave-silent-disconnect-no-keepalive-2026-04-30`160
documented the prior failure mode: relayd's 10-minute upstream161
timeout reaped idle apiary sessions, the resulting FIN was162
swallowed by NAT conntrack expiry, and apiary's kernel-level163
socket sat in ESTABLISHED forever — `send-channel` calls164
silently disappeared into the bit-bucket. The companion fix165
ships in enclave-server v0.2.8.167
### Added169
- **Client-side PING every 60 s of inbound silence.** The event170
loop's keepalive sweep (running every 5 s as part of the171
existing 50 ms tick) emits `PING :<token>` whenever the172
connection has been idle (no inbound or outbound traffic) for173
more than 60 s. Tokens are jiffy + random integer (~64 bits174
of session-local entropy) so a misbehaved channel peer can't175
trivially forge a PONG.176
- **Server-PING handler.** `handle-server-ping` echoes the177
trailing token back as `PONG :<token>` so the new178
enclave-side keepalive treats apiary as alive. sigil-irc does179
not auto-respond to inbound PING, so this handler closes the180
regression gap.181
- **Server-PONG handler.** `handle-server-pong` clears the182
pending-ping bookkeeping when the trailing token matches our183
in-flight client PING. Mismatched / stale PONGs are debug-184
logged and ignored — the activity-touch on the same line185
already handles the "still alive" signal.186
- **Auto-reconnect with exponential backoff.** When the187
pending PING goes 30 s without a matching PONG, OR188
sigil-irc's state flips to `'disconnected` (clean server189
close, EOF, or network drop), `trigger-reconnect!` spawns190
a goroutine that retries `do-reconnect!` with backoff191
starting at 1 s and doubling up to 60 s. On success the192
attempt counter resets so the NEXT outage starts at 1 s193
again. The same goroutine handles all reconnect paths194
(PING-timeout, EOF-detected, network-failed) so there's195
one place to reason about backoff state.196
- **Activity tracking on every send + receive.** New197
`enclave-conn-touch-activity!` is called from every inbound198
PRIVMSG / BATCH / CAP / FAIL / PING / PONG handler AND from199
every outbound `enclave-post*` helper, so the keepalive200
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 AND204
`irc-join`s every channel in `APIARY_CHANNEL` (CSV-aware) so205
after an outage the bot is back in #hive without manual206
intervention. The trusted-set is preserved across reconnect207
(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 a210
caller-initiated teardown doesn't trigger the reconnect211
goroutine on the way out. The keepalive sweep + reconnect212
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, verify216
apiary detects + logs reconnect-trigger, restart server,217
verify apiary reconnects. 7 wire-level assertions, runs in218
~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?), and223
shutdown-prevents-reconnect.225
### Changed227
- `enclave-conn` struct's `irc` slot is now mutable so the228
reconnect path can swap in a fresh `make-irc-connection`229
without losing the surrounding bridge state (channel230
watchers, MCP server reference, trusted set).231
- The event loop re-reads `enclave-conn-irc` on every iteration232
and yields while `reconnecting?` is set, so the same loop233
serves across reconnects without spawning a second one.235
### Notes237
- Pre-1.0 patch bump (additive: new fields + new handlers +238
reconnect path; no API removal). Existing callers that239
depend on `enclave-conn-irc` being stable across the lifetime240
of the conn now need to assume it can swap during a reconnect241
window — but the only callers (apiary's own helpers) re-read242
the field every time, so this is a contract clarification243
rather than a behavior change.244
- The keepalive constants (60 s idle threshold, 30 s PONG245
timeout, 5 s sweep cadence) are baked into the source per the246
brief — the diagnosis budget (relayd 10 min, typical NAT247
conntrack ~15 min) puts both well below either timeout, so248
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]]` ships251
the symmetric server-side keepalive as enclave-server v0.2.8.252
Either alone is insufficient: server-PING alone leaves253
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 + reacts258
First patch release after the initial extraction. Multi-line259
agent briefings now ride a single IRCv3 `draft/multiline` BATCH260
on the wire (graceful-degrades to per-line PRIVMSG when the261
server NAKs the cap or FAILs the OPEN), and reactions are a262
first-class MCP tool + a distinct inbound event type.264
### Added266
- **`enclave-post-multiline conn target lines` helper.** Wraps267
N PRIVMSGs in a `BATCH +<reftag> draft/multiline <target>`268
envelope so receivers (Goguma, Senpai, Catgirl with the cap269
negotiated) coalesce the group as one collapsed270
notification + threaded block instead of N detached alerts.271
Falls back to per-line PRIVMSG when the server NAKed the272
cap or returned `FAIL BATCH` on the OPEN. Returns273
`'sent-batch` / `'sent-per-line` / `'sent-empty` so the MCP274
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 lines278
belong to the same logical message and don't re-prefix.279
- **Inbound BATCH coalescing.** `(apiary enclave)` installs a280
`'BATCH` handler. Open batches buffer per reftag, each281
batch-tagged PRIVMSG appends, and on close a synthetic282
joined PRIVMSG is dispatched through the normal283
channel-handler path so trusted-set + mention rules stay284
authoritative. The agent sees one channel-notify per285
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-irc289
only requests `sasl` during the initial CAP LS round — the290
rest layer on after 001 lands. The bridge tracks ACKed caps291
in `enclave-conn-caps-acked` and exposes292
`enclave-conn-cap-acked? conn cap-name` so callers can293
introspect.294
- **`send-react` MCP tool.** Emits a PRIVMSG with the295
client-only tags `+draft/react=<emoji>;+draft/reaction=<emoji>;+draft/reply=<msgid>`296
pointing at a prior server-assigned msgid. Both the older297
`+draft/react` slug and the newer `+draft/reaction` ride298
the wire so any client variant renders the body as a299
reaction badge. Useful for fast acks and silent signals300
without channel chatter.301
- **Inbound react detection.** `+draft/react` OR302
`+draft/reaction` on an incoming PRIVMSG promotes the event303
to `type=enclave-react` with `emoji` + `target-msgid` meta,304
so the agent handles reactions distinctly from regular305
messages.306
- **`msgid` on every channel-notify event.** Both307
`enclave-channel` and `enclave-dm` events now surface the308
server-assigned msgid in meta when present, so the agent309
can construct a valid `send-react` against any prior310
message — including DMs from owner.311
- **FAIL detection + per-line fallback.** When the server312
emits `FAIL BATCH …` (target the validator rejects, type313
unsupported, etc.), the bridge aborts the in-flight batch314
and re-sends the lines as per-line PRIVMSGs. Replaces the315
earlier "9 cascading BATCH_NOT_OPEN errors → user gets316
nothing" failure mode.317
- **`msgid` field in apiary's debug log line for inbound318
PRIVMSGs.** `[DEBUG] Enclave PRIVMSG in sender=… target=…319
msgid=… text-len=…` removes the ssh-roundtrip-to-the-server320
step when triaging react flows.321
- **Updated instructions string** for both leader and worker322
modes documents the multiline batch path, react flow323
(inbound + outbound), and the msgid/meta contract.325
### Wire-level smoke327
`test/smoke-driver.sgl` exercises 13 PASS checks against a328
locally-spun enclave-server: bridge connect + presence329
broadcast, owner mention, listen-peer mutation,330
multiline-cap-negotiated, multiline-batch-emit (asserts the331
BATCH OPEN/CLOSE + tagged PRIVMSGs reach a peer eyeball),332
single-line-no-batch (no envelope when content has no333
newlines), batch-fail-flag-set (FAIL handler captures334
unsupported-type rejections), multiline-fallback-per-line335
(returns `'sent-per-line` and emits per-line when the cap336
isn't ACKed), dm-msgid-surfaced, and send-react-tagged337
(asserts both `+draft/react` and `+draft/reaction` slugs +338
`+draft/reply` are on the outbound wire).340
### Known limitation342
**Goguma's react UI gating.** Goguma exposes its long-press343
react UI only on messages where it has previously seen a344
react (per `networkMsgid` storage), and only when the345
server's ISUPPORT permits the relevant client tags. When346
triaging "Goguma's react UI is greyed out", confirm347
enclave's CAP LS (now logged at INFO) advertises348
`draft/react` + `draft/reaction` + `draft/reply` AND that349
the user's connection negotiated them. The first react in a350
conversation may need to come from a different client to351
prime Goguma's per-message gate.353
**Multi-line BATCH coalesce-on-render** requires the receiving354
client to have negotiated `draft/multiline` itself. Senpai 0.x355
and Goguma 0.x as of 2026-04-29 don't include the cap in their356
default REQ list, so apiary's BATCHes still render as N357
standalone messages in those clients regardless of the relay358
path. The fix is upstream: clients adding `draft/multiline` to359
their default REQ. The new `cap-req` / `cap-end` INFO logs in360
enclave 0.2.4 let operators verify which caps each connection361
actually negotiated.363
## 0.1.0 — initial extraction365
First release. Extracted from the courier-enclave-migration366
work; ships the leader/worker MCP server with trusted-set367
filter, mention syntax, mode-vs-voicing model, and the368
EnclaveServ services-call wrapper. See369
[apiary-design](https://codeberg.org/sigil/folio/notes/topics/apiary-design)370
for the architecture spec.