Closes #4.
Wires all five dialect verbs through the daemon: hark dialect publish | subscribe | unsubscribe | query | list. Each lands a real local-API endpoint, validates user input where appropriate, and (for the three that need a response) uses a single-slot per-agent meta-reply correlation primitive to route the router's reply back to the requester.
What ships
hark dialect publish [--define '(define ...)'] [--json]
hark dialect subscribe <pattern> # pattern defaults to "*"
hark dialect unsubscribe
hark dialect query <name> [--json]
hark dialect list
Three commits, one per slice, on top of main (e0e171f):
be2f951— subscribe + unsubscribe (fire-and-forget; reuse existingsend_outbound)63173c1— publish (introducessend_meta_and_await+try_route_meta_reply; daemon-side R1-R5 validation)949c0a6— query + list (reuse the correlation primitive)
Correlation primitive (load-bearing new piece)
AgentEntry.pending_meta_reply: Option<oneshot::Sender<String>> — single-slot per agent. Concurrent meta sends serialise; a second caller returns meta_send_busy rather than queuing.
AgentStore::send_meta_and_await(handle, frame, timeout):
- atomically claims the slot
- sends via existing
send_outbound - awaits the oneshot with timeout (10s default)
- clears slot on timeout / error so the next attempt proceeds
AgentStore::try_route_meta_reply(handle, message):
- called from the receive loop before
enqueue_inbound - returns
Noneif routed (frame is consumed),Some(message)otherwise so the caller falls through to normal recv
Classifier extension
New InboundClass::MetaReply for bare (reply ...) frames. Normal asks arrive wrapped in (lang <dialect> (ask ...)) and stay Ordinary. The receive loop tries to route both DialectPush and MetaReply to a pending meta-await; failing that, they fall through unchanged.
Defense in depth
publish runs cbcl_parser::run_pipeline on (meta <define>) before sending — mirrors the dialect cache's install-time guard on the receive path. An R3 violation (override tell etc.) surfaces as cbcl_validation_failed (exit 8) without ever reaching the router. Verified end-to-end.
Receive-loop reorder
Cache install for DialectPush now runs before the meta-reply routing check, so both subscriber pushes and query teach-backs install into the local cache via the same code path. InboundOutcome::Installed(name) still fires after install regardless of which path consumed the frame, so auto re-hello continues to work as before.
Endpoints
POST /v1/agents/<handle>/meta/subscribe { "pattern": "..." } → 200 { ok }
POST /v1/agents/<handle>/meta/unsubscribe → 200 { ok }
POST /v1/agents/<handle>/meta/publish { "define": "..." } → 200 { ok, digest, name }
POST /v1/agents/<handle>/meta/query { "name": "..." } → 200 { ok, digest, name, define }
404 { error.code: "dialect_unknown_to_router" }
POST /v1/agents/<handle>/meta/list → 200 { ok, names: [...] }
API versioning
Bumps LOCAL_API_VERSION 2 → 3. The new endpoints are the schema change.
New error codes (CLI mapping)
| Code | Where | CLI exit |
|---|---|---|
invalid_subscribe_pattern |
local | 2 (usage) |
meta_send_busy |
local | 7 (agent unavailable) |
meta_reply_timeout |
local | 10 (timeout) |
dialect_unknown_to_router |
router miss | 2 (usage) |
meta_reply_malformed / meta_reply_missing_digest / meta_reply_missing_name |
local | 12 (internal) |
End-to-end verification
Run against a live cbcl-router (slice-3 PR #5 branch) — all transcripts in commit messages. Highlights:
$ hark dialect publish --define '(define alpha-v1 (cbcl) @e2e)'
7b105efd2d9137f88cc969ebc40cebb520e09bd33d6905490424aa831f14c6d1 alpha-v1
$ hark dialect publish --define '(define alpha-v1 (cbcl) @e2e)' # idempotent
7b105efd2d9137f88cc969ebc40cebb520e09bd33d6905490424aa831f14c6d1 alpha-v1
$ hark dialect publish --define '(define bad (cbcl) @e2e (extend tell () x))'
cbcl_validation_failed: define failed R1–R5: R3 violation: core performative(s) overridden: tell
(exit 8)
$ hark dialect list
beta-v1
alpha-v1
$ hark dialect query alpha-v1
7b105efd2d9137f88cc969ebc40cebb520e09bd33d6905490424aa831f14c6d1 alpha-v1
$ hark dialect query nope-not-here
dialect_unknown_to_router: router-does-not-speak (exit 2)
$ hark dialect subscribe 'arena-*'
$ # producer teaches arena-v1 → hark recv picks up the pushed frame
$ hark dialect unsubscribe
$ # producer teaches arena-v2 → hark recv times out (subscription dropped)
Test plan
cargo build --release— cleancargo test --release— 122/122 pass- End-to-end against live router for each verb (transcripts in commit messages and PR description)
- Manual smoke: concurrent meta sends from two threads / shells (should one see
meta_send_busy) - CI green
Non-goals (per issue #4)
- Persistent subscription state across daemon restarts
- A
hark dialect describe <name>UX — pending the cbcl-router dialect /:descriptiondesign conversation - Replacing
recv's handling of stray meta replies — currently if a(reply ...)arrives with no pending meta-await, it falls through torecvunchanged
🤖 Generated with Claude Code