The kickoff scripted a closer and assumed it was the last word. It never
could be: the teammate intros `@mention` Fizz, which is the mandatory
delegator callback in base_prompt.md, so Fizz is guaranteed to wake and
reply after them. The choreography and the base prompt were fighting, and
the user got two CTAs.
The scripted one was also the one that could be wrong. It raced the intros
on an 18s stopwatch (15s wait + 3s beat) — well under a real agent turn, so
it reliably announced "Honey and Bumble are taking longer than expected"
seconds before both intros landed. Fizz's live reply was better: contextual,
and right about the state of the world.
So let the guaranteed message be the close, and keep the script for
problems only:
- buildWelcomeKickoffCloser returns null for a clean kickoff. Failure
variants are unchanged and still carry the CTA, where the user needs both
the bad news and a way forward.
- Enforce the null inside sendWelcomeKickoffCloser rather than at the two
call sites, so the delayed-teammate timer can't emit a bare CTA after the
intros land and re-classify the kickoff as clean.
- TEAMMATE_INTRO_WAIT_MS 15s -> 60s. This only covers a teammate that is
alive but silent, where waiting is correct; a crashed teammate is read
from agent status and still closes immediately.
The closer marker was quietly doubling as the durable "kickoff finished"
signal, and a marker requires a message to exist — so "success posts
nothing" would have meant "success never latches", refetching the opener
subtree forever and restarting the whole Welcome team on every revisit.
welcomeKickoffAlreadyFinished now accepts a second piece of relay-side
evidence: an intro from every teammate in the opener's thread. The marker
still short-circuits first, so the failure paths and the solo opener are
untouched.
Verified live against Codex on 2026-07-18 with the base prompt as the only
variable: the intro loop terminated on its own at four replies.
Co-Authored-By: Claude <noreply@anthropic.com>
Observed with Codex on a fresh Welcome channel: Fizz, Honey, and Bumble
reply to each other 21+ times, each message announcing it will now stop
replying, each announcement waking the others.
A mention is a wake (p-tag gated), so the only way an agent could say
"seen" was to summon someone. Combined with an unconditional MUST in the
shared base prompt, that has no exit:
:44 you MUST @mention the delegator when you finish delegated work
"I won't reply again" is itself a summons. Note :40 already said "only
@mention when you need their attention" -- and they still looped, because
a soft "only when" loses to a hard MUST two lines below it. Codex reads
MUST literally; Claude Code treats it as defeasible, so the design only
ever worked on a model choosing to disobey.
Fix it in the base prompt rather than the personas (3 copies to drift) or
team instructions (Welcome-team only). Three bullets, all in Callback
Mentions:
- Bound the callback: it hands work back, is sent once, and a delegator
can waive it. The unconditional MUST itself is kept -- stalled
collaboration is the worse failure.
- Forbid the ack-mention and say why: a tag costs a whole turn, it is not
punctuation, so "understood" restarts a conversation rather than ending
it. A literal model needs the mechanism, not just the rule.
- Point at reactions as the outlet: "to acknowledge, use a reaction
instead" (buzz reactions add). This is the load-bearing part. It
redirects rather than suppresses, and it keeps the human-visible social
signal so the fix does not read as stalling. The command already
existed and is already in the command table; the prompt just never
connected it to acking.
:61 ("every turn MUST publish a reply") is deliberately left alone. It is
not load-bearing: a loop needs a wake, wake is p-tag gated, and :61 forces
a reply rather than a tag -- an untagged reply ends the chain. It governs
noise, not the loop, and weakening "always reply" trades toward the silent
failures this doc already tracks. Corroborated by another Buzz agent
running this same base prompt including :61, plus a core memory saying
almost exactly the bullets above; it does not loop.
Also considered and rejected: a structural cap on consecutive
agent-authored turns. It cannot distinguish a loop from a long-running
task where agents legitimately trade many messages, so it would truncate
real work.
Fizz's opener is reduced to a plain welcome: an em dash becomes a sentence
break, and "Don't start any work yet" is dropped -- nothing has been asked
for yet, so it guarded against nothing concrete. An anti-loop instruction
there would work, but the opener is the first thing a new user reads and
should not advertise that the product needs prompting to behave; it would
also confound the test by suppressing the loop in this one channel while
proving nothing about the base prompt. A test pins the absence of
agent-steering instructions so re-adding one is a deliberate decision.
Also corrects the doc's rate-limit note. "rate-limited: quota exceeded" is
the relay rejecting a publish (buzz-relay/src/connection.rs:652), and
buzz-acp has no publish retry/backoff, so the tight-retry-loop theory was
unsupported. A rejected send produces no event, no p-tag, no wake -- it
breaks the loop rather than feeding it. Rate limiting is downstream; the
two can be fixed independently.
Not verified live. The prompt change is behavioral and needs a real Codex
session on a fresh Welcome channel.
Co-Authored-By: Claude <noreply@anthropic.com>
## Why
On macOS, GUI apps launched from Finder/Spotlight/Dock inherit a minimal PATH from launchd (`/usr/bin:/bin:/usr/sbin:/sbin`). The `discover_provider_candidates()` function in `backend.rs` only scanned this process PATH when looking for `buzz-backend-*` executables. As a result, backend providers (like `buzz-backend-blox`) were never found, and the "Run on" dropdown in Agent Settings was hidden — even though the binary existed both in the app bundle (`Contents/MacOS/`) and in `~/.local/bin`.
## What
Augment the search directories in `discover_provider_candidates()` with:
1. **exe parent dir** (`Contents/MacOS/` in a `.app` bundle) — so bundled providers are always found regardless of how the desktop was launched
2. **`~/.local/bin`** — the conventional location for user-installed provider binaries (symlinks created by install scripts)
Both directories are only added if not already present in PATH (deduplication). The exe parent dir is prepended (highest priority for bundled providers); `~/.local/bin` is appended.
## Verification
- `cargo check --lib` passes
- All 17 `managed_agents::backend` unit tests pass
- Pre-push hook `desktop-tauri-test` passes (38s)
## References
Root cause confirmed by inspecting the running `buzz-desktop` process environment:
```
$ ps eww <pid> | grep PATH
PATH=/usr/bin:/bin:/usr/sbin:/sbin
```
---
Generated with Fizz