feat(telemetry): complete bb-buff.1.3 - Backend Liveness Refactor

STORY:
The session backend needed to aggregate agent health from a live
telemetry stream rather than static bead metadata. This refactor
makes liveness signals real-time and accurate.

COLLABORATION:
We extended the ActivityEvent model with a native 'heartbeat' kind,
updated extendActivityLease() to emit through the activity bus, and
refactored getAgentLivenessMap() to prioritize heartbeat activity
history over stale bead metadata.

DELIVERABLES:
- ActivityEvent extended with 'heartbeat' kind
- extendActivityLease() emits heartbeats through activity bus
- getAgentLivenessMap() prefers telemetry over static metadata
- Registry APIs support projectRoot injection for testing
- Tests verify preference logic via TDD

VERIFICATION:
- 93/93 tests PASSING
- Heartbeat override verified in isolated temp projects

CLOSES: bb-buff.1.3
BLOCKS: bb-buff.3.2, bb-buff.3.3, bb-buff.2.1
This commit is contained in:
zenchantlive 2026-02-15 21:14:05 -08:00
parent 0016b57e37
commit 4ee550c333
36 changed files with 1380 additions and 541 deletions

View file

@ -48,14 +48,20 @@ export class IssuesWatchManager {
console.log(`[Watcher] Processing event for ${projectRoot}: ${payload.kind} (${payload.changedPath})`);
// 1. Emit basic file change event
this.eventBus.emit(projectRoot, payload.changedPath, payload.kind);
// 2. Perform snapshot diffing if issues.jsonl changed
// If it's just last-touched or a DB file change, we treat it as telemetry
const changedPath = payload.changedPath || '';
const isIssuesJsonl = changedPath.endsWith('issues.jsonl') || changedPath.endsWith('issues.jsonl.new');
const isLastTouched = changedPath.includes('last-touched');
const isDbPulse = changedPath.includes('beads.db');
const kind = (isLastTouched || isDbPulse) && !isIssuesJsonl ? 'telemetry' : payload.kind;
this.eventBus.emit(projectRoot, payload.changedPath, kind);
// 2. Perform snapshot diffing if issues.jsonl changed
const isBeadsDb = changedPath.includes('beads.db') || isLastTouched;
const isGlobalMessages = changedPath.includes('.beadboard') && changedPath.includes('messages');
if (isIssuesJsonl) {
if (isIssuesJsonl || isBeadsDb) {
console.log(`[Watcher] Issues changed. Syncing activity for ${projectRoot}...`);
await this.syncActivity(projectRoot);
} else if (isGlobalMessages) {
@ -71,7 +77,7 @@ export class IssuesWatchManager {
const previous = this.snapshots.get(projectKey) ?? null;
try {
const current = await readIssuesFromDisk({ projectRoot });
const current = await readIssuesFromDisk({ projectRoot, preferBd: true, skipAgentFilter: true });
const events = diffSnapshots(previous, current);
this.snapshots.set(projectKey, current);
@ -92,7 +98,7 @@ export class IssuesWatchManager {
// Pre-populate snapshot to avoid "all created" burst on first change
try {
const initial = await readIssuesFromDisk({ projectRoot });
const initial = await readIssuesFromDisk({ projectRoot, preferBd: true, skipAgentFilter: true });
this.snapshots.set(projectKey, initial);
} catch {
// Ignore initial read failure, will retry on first change
@ -165,7 +171,7 @@ export class IssuesWatchManager {
}
}
const WATCHER_VERSION = 3; // Bump this to force re-creation on HMR
const WATCHER_VERSION = 4; // Bump this to force re-creation on HMR (v4: fix beads.db telemetry classification)
const globalRegistry = globalThis as typeof globalThis & {
__beadboardWatchManager?: IssuesWatchManager;