Three CI failures on head 30e25b4a:
Rustfmt: run `just fmt` — collapses no_p let in req.rs:1341, the
publish_event() call in main.rs:747, alpha-orders test use imports in
main.rs:1020, and reflowed lines in relay.rs + event.rs that fell
over the column limit.
E0063 OwnedAgent missing field: the branch was 25 commits behind main
after #887 (config-bridge) added model_overridden: bool to OwnedAgent.
The dream test dummy_agent() helper at lib.rs:4593 didn't set it. Fix:
add model_overridden: false, matching the adjacent field pattern.
nest.rs file-size override: dream-skill scaffolding (+76 lines) pushed
nest.rs to 1526 (gate-count) over the prior 1450 ceiling. Bumped the
override in desktop/scripts/check-file-sizes.mjs with a one-line
rationale comment noting the dream-skill addition mirrors the buzz-cli
seam already in the module; queued to split with the rest of the list.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Brings the branch current with main (25 commits) before CI green run.
Auto-merged — no conflicts.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
KIND_DREAM_DUE (24300) was absent from P_GATED_KINDS, so
p_gated_filters_authorized() never enforced #p-must-match-self for it.
Any authenticated relay user could subscribe with kinds:[24300],
#p:[victim_pubkey] and receive another agent's dream-due signals,
leaking "agent X is over memory budget and idle" to anyone who knows
an agent pubkey.
The kind's own doc already states "single-delivery to the authenticated
agent"; P_GATED_KINDS is the registry that makes the relay enforce it.
Fix: add KIND_DREAM_DUE to P_GATED_KINDS. Ephemeral kinds are included
in this list for filter-layer enforcement only and are never stored, so
no schema/migration/tsvector change is needed.
The ACP harness's own subscription (DREAM_SIGNAL_SUB_ID, #p=[self])
continues to be accepted — the self case is exactly what P_GATED_KINDS
allows through.
Test: dream_due_subscription_requires_matching_p_tag verifies:
- no #p → rejected
- #p:[other] → rejected
- #p:[self] → accepted
Confirmed: test fails without the registry add and passes with it.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Addresses three Thufir pass-1 review findings:
[CRITICAL] ACP now subscribes to KIND_DREAM_DUE
The relay sweep emits KIND_DREAM_DUE as a global #p-tagged ephemeral event.
The old code relied on the membership subscription (hard-coded to only
KIND_MEMBER_ADDED_NOTIFICATION + KIND_MEMBER_REMOVED_NOTIFICATION) to deliver
it — a false assumption. The event was never received.
Fix: dedicate a DREAM_SIGNAL_SUB_ID subscription (kinds: [KIND_DREAM_DUE],
#p=[agent_pubkey], no #h). Infrastructure mirrors the existing membership and
observer-control subscriptions:
- DREAM_SIGNAL_SUB_ID = "dream-signal" const in relay.rs
- SubscribeDream RelayCommand variant
- dream_sub_active flag in BgState
- send_dream_subscribe() + build_dream_req_filter() helper functions
- Routing: forwards BuzzEvent { channel_id: Uuid::nil(), event } for
KIND_DREAM_DUE events — nil sentinel is safe because lib.rs checks
kind_u32 == KIND_DREAM_DUE before any code touches channel_id (continue)
- CLOSED handler resubscribes or triggers reconnect
- resubscribe_after_reconnect() restores dream sub after socket loss
- subscribe_dream_signals() public method on HarnessRelay
- Called in lib.rs right after subscribe_membership_notifications() at startup
No watermark/replay needed: dream-due events are ephemeral relay-initiated
signals. A missed one is safe — the relay sweep re-emits on the next interval.
[IMPORTANT] Honest idle gate comment with design rationale
The relay-side idle gate is intentionally a coarse pre-filter: no live presence
key = no heartbeat in 90s = probably idle. This is by design, not an oversight.
The real over-fire protection is agent-side: maybe_dispatch_dream() requires no
flushable queue work, no heartbeat in flight, and an idle agent in the pool;
dream runs at lowest priority and is preemptible via ControlSignal::Cancel.
A false "idle" from the relay costs nothing — the agent ignores it or runs a
preemptible low-priority turn.
The idle gate logic is extracted into agent_is_idle(presence: Option<&str>) -> bool
in buzz-relay/main.rs — a pure function that makes the binary decision testable
without a Redis integration harness.
No last_turn_ended_at column exists in the DB; a timestamp-based gate is not
implementable. Presence-TTL (90s) is the only available signal.
[IMPORTANT] Subscription filter tests + idle gate boundary tests
Six tests in relay::dream_subscription_tests pin the NIP-01 filter contract:
- KIND_DREAM_DUE is present in kinds
- Exactly one kind (no accidental broadening)
- #p contains the agent pubkey
- No #h (global, not channel-scoped)
- Different pubkeys produce different filters
- DREAM_SIGNAL_SUB_ID is distinct from MEMBERSHIP_NOTIF_SUB_ID and
OBSERVER_CONTROL_SUB_ID
Four tests in relay::tests cover the idle gate:
- live "online" presence blocks
- live "away" presence blocks
- any non-None status blocks
- None (expired/absent) allows
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Implements the full dream consolidation system across four areas:
**1. Dispatch liveness (Thufir IMPORTANT #1)**
Extracts `maybe_dispatch_dream()` helper, called at four state-transition
points: after KIND_DREAM_DUE receipt, after heartbeat fires (idle gap),
after prompt result + drain, after panic recovery + drain. Removes the
old else-if chain that made dream reachable only when a heartbeat was
already in flight — fixing both starvation modes:
- Single-agent pool + fast heartbeat: dream now fires in the idle gap
after heartbeat completes, not waiting for a concurrent heartbeat slot.
- Heartbeat disabled: dream fires immediately on dream-due receipt.
Guard: no flushable queue work, dream_pending, !dream_in_flight,
!heartbeat_in_flight, idle agent. Preserves pending > heartbeat > dream
priority.
**2. Skill distribution via Nest scaffolder (Will's requirement)**
Adds nest_dream_skill.md (include_str!'d as DREAM_SKILL_MD), written
to .agents/skills/dream/SKILL.md at Nest init, mirroring the buzz-cli
skill pattern (nest.rs:42-174). Includes NEST_DREAM_SKILL_VERSION=1
for refresh-on-bump, refresh_dream_skill_md_if_stale() with atomic
tempfile write, and dream skill symlinks in ensure_skill_symlinks() for
all known provider dirs (.goose/skills, .claude/skills, etc.).
This makes load_dream_prompt()'s cwd-relative read correct — the Nest
IS the cwd, and the scaffolder now guarantees the file.
**3. Missing prompt is startup-visible error (Thufir IMPORTANT #2)**
dispatch_dream() now logs tracing::error! (target: "dream") when the
prompt is None, returns the claimed agent, clears dream_pending so
it does not retry on every signal, and documents this as a
configuration error (missing SKILL.md). No more silent drop.
**4. Relay sweep + dream-due emission (Phase 3)**
In buzz-relay:
- Config: dream_memory_budget_bytes (default 65536, env
BUZZ_DREAM_MEMORY_BUDGET_BYTES), dream_sweep_interval_secs (default
300, env BUZZ_DREAM_SWEEP_INTERVAL_SECS). Set budget to 0 to disable.
- main.rs: spawns sweep task if budget > 0 and deployment_community
is Some. Each interval: queries agents_over_memory_budget, idle-gates
each via buzz_pubsub::presence::get_presence (no live Redis key = idle),
builds KIND_DREAM_DUE ephemeral event tagged #p=[agent_pubkey], signs
with relay keypair, publishes via pubsub.
- Staleness ceiling satisfied: presence TTL is 90s, default sweep is
300s, so "no presence key" already implies > 90s silence.
In buzz-db: agents_over_memory_budget() aggregates SUM(LENGTH(content))
for non-tombstone kind:30174 engrams per community, returns agents
exceeding budget.
**5. Tests (Phase 4)**
Six unit tests in dream_dispatch_tests cover:
- Guard: dream_pending=false → no dispatch, agent not consumed.
- Guard: heartbeat_in_flight=true → blocked (priority order).
- Regression: heartbeat disabled → dream still dispatches.
- Guard: empty pool → pending preserved for next idle gap.
- Fix 3: missing prompt → pending cleared, agent returned, no task.
- Guard: dream_in_flight=true → no double dispatch.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Implement Phase 2 of the dream skill: harness-side dispatch logic for
memory consolidation turns. Dream is lowest-priority (pending work >
heartbeat > dream), preemptible by any inbound event, and follows the
heartbeat session lifecycle (reuse until invalidated on cancel).
Key additions:
- KIND_DREAM_DUE (24300) ephemeral event constant in buzz-core
- PromptSource::Dream variant with full match coverage
- Dream session management (create once, reuse, invalidate on cancel)
- dispatch_dream() with control_tx for preemption
- Dream-due event detection sets pending flag (dispatch on next tick)
- cancel_in_flight_dream() fires Cancel on any accepted inbound event
- Panic recovery clears dream_in_flight via is_dream on TaskMeta
- load_dream_prompt() reads .agents/skills/dream/SKILL.md at startup
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>