mirror of
https://github.com/block/buzz.git
synced 2026-11-04 16:31:38 +00:00
When Desktop stops or restarts a managed-agent pair, the harness is SIGKILLed after 1s and `turn_completed` never lands. `activeAgentTurnsStore` then sees the "all turns silent at once" pattern and delays badge cleanup for up to 3 min (the pause that protects live badges during transient relay-stream gaps). This is unnecessary when Desktop itself issued the kill — there is no relay-gap ambiguity. ## What changed **`activeAgentTurnsStore.ts`** — new `clearActiveTurnsForAgent(pubkey)` - Tombstones every live turn for the agent via `recordTerminal` (blocks in-flight `turn_liveness` frames from resurrecting them via `resurrectTurn`) - Removes the agent's entry from `activeTurnsByAgent` - Preserves `lastProcessed` (watermark) — full-buffer replay after the clear is a no-op - Preserves `clockOffsetByAgent` — still valid, harmless **`managedAgentRuntimeHooks.ts`** — clearing at the successful-stop boundary - New `clearActiveTurnsForAgentOnStop(pubkey, relayUrl?)` — relay-scope gate: only clears when the stopped pair's relay matches the active community (pair-scoped), or when an active community is configured (agent-wide ops) - New `restartManagedAgentPair(pubkey, relayUrl, stop, clear, start)` — dependency-injected stop → clear → start sequence; the `restart` branch of `useManagedAgentRuntimeAction`'s `mutationFn` is a single call into it. The clear fires after a successful stop and before start begins, so the badge is gone even when start fails, a failed stop clears nothing, and no clear can run after the new process is spawned (genuinely-new turns are never wiped) - `useManagedAgentRuntimeAction.onSuccess` clears for `stop` actions, before the query-cache update **`managedAgentControlActions.ts`** — `onStopped` callback on `respawnManagedAgentWithRules`, invoked after the stop promise resolves and before start begins **`welcomeKickoff.ts`** — same `onStopped` boundary on `restartWelcomeTeammate` **Call sites covered (all stop/restart UI paths):** - `useManagedAgentRuntimeAction` — pair-scoped stop (`onSuccess`) and restart (`restartManagedAgentPair` in `mutationFn`); Members-sidebar + settings card - `useMembersSidebarActions.handleRespawnAll` — via `onStopped` - `useMembersSidebarActions.handleStopAll` — direct local-stop branch - `useMembersSidebarActions.handleLifecycleAction` — local-stop fallback branch - `useAgentLifecycleActions.handleAgentPrimaryAction` — Agents-tab stop - `useAgentLifecycleActions.handleAgentRestart` — via `onStopped` - `useManagedAgentActions.handleStop` / `handleBulkStopRunning` — Agents screen - `useAutoRestartPolicy` — inline, between stop and start - `restartWelcomeTeammate` call site — via `onStopped` Provider agents are excluded at each site: they go through `!shutdown` (relay message), not a direct harness kill. ## Tests Twelve behavior tests across three files: - `activeAgentTurnsStore.test.mjs` (6) — clear removes the agent's turns and notifies subscribers, other agents untouched; full-buffer replay after clear is a no-op (watermark preserved); late `turn_liveness` frame with timestamp ≤ clear time does not resurrect (tombstone); new `turn_started` after clear is tracked normally; badge gone when stop succeeds even if start fails; new frame during start-pending does not resurrect the cleared badge - `managedAgentControlActions.test.mjs` (3) — `onStopped` fires on stop-success/start-failure; does not fire on stop-failure; strict stop → `onStopped` → start ordering - `managedAgentRuntimeHooks.test.mjs` (3) — pair-restart seam: clear ran when start fails (rejection propagates); stop failure invokes neither clear nor start; strict stop → clear → start ordering --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
111 lines
3.1 KiB
JavaScript
111 lines
3.1 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { restartManagedAgentPair } from "./managedAgentRuntimeHooks.ts";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// restartManagedAgentPair: discriminating regression tests for the pair
|
|
// restart lifecycle boundary (stop → relay-scoped clear → start).
|
|
//
|
|
// These tests exercise the exact function called by useManagedAgentRuntimeAction's
|
|
// mutationFn restart branch, so reverting to the old combined Rust command
|
|
// (which cleared only in onSuccess, after the new process was already running)
|
|
// would make tests (a) and (c) fail.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const PUBKEY = "deadbeef".repeat(8);
|
|
const RELAY = "wss://relay.example";
|
|
|
|
/** Returns a resolved-status stub sufficient for the return-type assertion. */
|
|
function makeStatus() {
|
|
return {
|
|
pubkey: PUBKEY,
|
|
relayUrl: RELAY,
|
|
localSetup: true,
|
|
lifecycle: "running",
|
|
};
|
|
}
|
|
|
|
test("test_pair_restart_stop_success_start_failure_clear_still_ran", async () => {
|
|
// Stop succeeds, start throws. The clear must have fired — badge is gone
|
|
// regardless of the start failure. On the old combined-command approach,
|
|
// a rejected command meant onSuccess never ran and the badge survived.
|
|
let clearFired = false;
|
|
|
|
await assert.rejects(
|
|
restartManagedAgentPair(
|
|
PUBKEY,
|
|
RELAY,
|
|
async () => makeStatus(), // stop succeeds
|
|
(_pubkey, _relayUrl) => {
|
|
clearFired = true;
|
|
},
|
|
async () => {
|
|
throw new Error("start failed");
|
|
},
|
|
),
|
|
/start failed/,
|
|
);
|
|
|
|
assert.ok(
|
|
clearFired,
|
|
"clear must fire at stop-success boundary even when start subsequently fails",
|
|
);
|
|
});
|
|
|
|
test("test_pair_restart_stop_failure_neither_clear_nor_start_called", async () => {
|
|
// Stop throws. Neither clear nor start should run — clearing on a failed
|
|
// stop would remove a badge that is still legitimately active.
|
|
let clearFired = false;
|
|
let startCalled = false;
|
|
|
|
await assert.rejects(
|
|
restartManagedAgentPair(
|
|
PUBKEY,
|
|
RELAY,
|
|
async () => {
|
|
throw new Error("stop failed");
|
|
},
|
|
(_pubkey, _relayUrl) => {
|
|
clearFired = true;
|
|
},
|
|
async () => {
|
|
startCalled = true;
|
|
return makeStatus();
|
|
},
|
|
),
|
|
/stop failed/,
|
|
);
|
|
|
|
assert.ok(!clearFired, "clear must NOT fire when stop itself fails");
|
|
assert.ok(!startCalled, "start must NOT be called when stop fails");
|
|
});
|
|
|
|
test("test_pair_restart_strict_stop_clear_start_ordering", async () => {
|
|
// Verify the operations fire in the guaranteed order: stop → clear → start.
|
|
// A clear that fires after start begins can tombstone genuine new turns.
|
|
const events = [];
|
|
|
|
await restartManagedAgentPair(
|
|
PUBKEY,
|
|
RELAY,
|
|
async () => {
|
|
events.push("stop");
|
|
return makeStatus();
|
|
},
|
|
(_pubkey, _relayUrl) => {
|
|
events.push("clear");
|
|
},
|
|
async () => {
|
|
events.push("start");
|
|
return makeStatus();
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(
|
|
events,
|
|
["stop", "clear", "start"],
|
|
"operations must fire in stop → clear → start order",
|
|
);
|
|
});
|