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

@ -1,9 +1,16 @@
'use client';
import { useEffect, useState } from 'react';
import type { AgentRecord, AgentLiveness } from '../../lib/agent-registry';
import { ProjectScopeControls } from '../shared/project-scope-controls';
import type { ProjectScopeOption } from '../../lib/project-scope';
import { AgentStation } from './agent-station';
import { getSwarmHealth } from './sessions-header-logic';
export interface SwarmGroup {
swarmId: string;
swarmLabel: string;
members: AgentRecord[];
}
interface SessionsHeaderProps {
agents: AgentRecord[];
@ -18,6 +25,9 @@ interface SessionsHeaderProps {
completed: number;
};
livenessMap?: Record<string, string>;
swarmGroups?: SwarmGroup[];
unassignedAgents?: AgentRecord[];
missionCounts?: Record<string, number>;
}
export function SessionsHeader({
@ -29,6 +39,9 @@ export function SessionsHeader({
projectScopeOptions,
stats,
livenessMap = {},
swarmGroups = [],
unassignedAgents = [],
missionCounts = {},
}: SessionsHeaderProps) {
return (
<header className="sticky top-0 z-50 flex flex-col border-b border-white/5 bg-[#0b0c10]/60 backdrop-blur-3xl shadow-2xl">
@ -40,15 +53,68 @@ export function SessionsHeader({
</div>
<div className="flex items-center gap-2 overflow-x-auto no-scrollbar py-1">
{agents.map((agent) => (
<AgentStation
key={agent.agent_id}
agent={agent}
isSelected={activeAgentId === agent.agent_id}
onSelect={onSelectAgent}
liveness={(livenessMap[agent.agent_id] as AgentLiveness) || 'active'}
/>
))}
{swarmGroups.length > 0 || unassignedAgents.length > 0 ? (
<div className="flex items-center gap-4">
{swarmGroups.map((group) => {
const health = getSwarmHealth(group.members, livenessMap);
return (
<div key={group.swarmId} className="swarm-container flex items-center gap-2">
<div className="flex flex-col items-end">
<span className="ui-text text-[0.55rem] font-black uppercase tracking-[0.15em] text-sky-400/50 whitespace-nowrap">
{group.swarmLabel}
</span>
<span className={`ui-text text-[0.45rem] font-bold uppercase tracking-wider ${health.color} flex items-center gap-0.5`}>
<span className="text-xs"></span>
{health.status}
</span>
</div>
<div className="flex items-center gap-1">
{group.members.map((agent) => (
<AgentStation
key={agent.agent_id}
agent={agent}
isSelected={activeAgentId === agent.agent_id}
onSelect={onSelectAgent}
liveness={(livenessMap[agent.agent_id] as AgentLiveness) || 'active'}
missionCount={missionCounts[agent.agent_id]}
/>
))}
</div>
</div>
);
})}
{unassignedAgents.length > 0 && (
<div className="swarm-container flex items-center gap-2">
<span className="ui-text text-[0.55rem] font-black uppercase tracking-[0.15em] text-zinc-500/40 whitespace-nowrap">
No Swarm
</span>
<div className="flex items-center gap-1">
{unassignedAgents.map((agent) => (
<AgentStation
key={agent.agent_id}
agent={agent}
isSelected={activeAgentId === agent.agent_id}
onSelect={onSelectAgent}
liveness={(livenessMap[agent.agent_id] as AgentLiveness) || 'active'}
missionCount={missionCounts[agent.agent_id]}
/>
))}
</div>
</div>
)}
</div>
) : (
agents.map((agent) => (
<AgentStation
key={agent.agent_id}
agent={agent}
isSelected={activeAgentId === agent.agent_id}
onSelect={onSelectAgent}
liveness={(livenessMap[agent.agent_id] as AgentLiveness) || 'active'}
missionCount={missionCounts[agent.agent_id]}
/>
))
)}
</div>
</div>
@ -78,95 +144,6 @@ export function SessionsHeader({
);
}
function useTimeAgo(isoTimestamp: string) {
const [timeAgo, setTimeAgo] = useState('');
useEffect(() => {
const update = () => {
const seconds = Math.floor((new Date().getTime() - new Date(isoTimestamp).getTime()) / 1000);
if (seconds < 60) setTimeAgo(`${seconds}s`);
else if (seconds < 3600) setTimeAgo(`${Math.floor(seconds / 60)}m`);
else setTimeAgo(`${Math.floor(seconds / 3600)}h`);
};
update();
const interval = setInterval(update, 10000);
return () => clearInterval(interval);
}, [isoTimestamp]);
return timeAgo;
}
function AgentStation({
agent,
isSelected,
onSelect,
liveness
}: {
agent: AgentRecord,
isSelected: boolean,
onSelect: (id: string | null) => void,
liveness: AgentLiveness
}) {
const timeAgo = useTimeAgo(agent.last_seen_at);
const statusStyles = {
active: {
dot: 'bg-emerald-500 animate-pulse shadow-[0_0_10px_rgba(16,185,129,0.8)]',
label: 'On Mission',
color: 'text-emerald-400/60'
},
stale: {
dot: 'bg-amber-500 shadow-[0_0_8px_rgba(245,158,11,0.5)]',
label: 'Lease Expiring',
color: 'text-amber-400/60'
},
evicted: {
dot: 'bg-rose-500/50 shadow-none',
label: 'Disconnected',
color: 'text-rose-400/40'
},
idle: {
dot: 'bg-zinc-700 shadow-none',
label: 'Idle',
color: 'text-zinc-500/30'
}
}[liveness];
return (
<button
onClick={() => onSelect(isSelected ? null : agent.agent_id)}
className={`flex-none group flex w-[10rem] items-center gap-2 rounded-lg border px-2 py-1.5 transition-all duration-300 ${
isSelected
? 'border-sky-500/50 bg-sky-500/10 shadow-[0_0_10px_rgba(14,165,233,0.1)]'
: 'border-white/5 bg-white/[0.01] hover:bg-white/5'
} ${liveness === 'idle' ? 'opacity-40 grayscale-[0.5]' : ''}`}
>
<div className="relative flex-none">
<div className={`h-7 w-7 rounded-md bg-gradient-to-br from-zinc-700 to-zinc-900 flex items-center justify-center border border-white/10 shadow-inner transition-transform duration-300 ${isSelected ? 'scale-90' : 'group-hover:scale-105'}`}>
<span className="ui-text text-[0.6rem] font-black text-zinc-400">
{agent.agent_id.slice(0, 2).toUpperCase()}
</span>
</div>
<span className={`absolute -bottom-0.5 -right-0.5 h-2 w-2 rounded-full border-2 border-[#0b0c10] ${statusStyles.dot}`} />
</div>
<div className="flex flex-col items-start min-w-0 text-left">
<div className="flex items-center gap-1 w-full justify-between pr-1">
<span className={`ui-text text-[0.65rem] font-black truncate transition-colors ${isSelected ? 'text-sky-300' : 'text-text-body'}`}>
{agent.agent_id}
</span>
<span className="system-data text-[0.5rem] font-bold text-text-muted/40">
{timeAgo}
</span>
</div>
<span className={`system-data text-[0.5rem] font-bold uppercase tracking-tighter ${statusStyles.color}`}>
{statusStyles.label}
</span>
</div>
</button>
);
}
function StatPill({ label, value, color }: { label: string, value: number, color: string }) {
return (
<div className="flex items-center gap-1 rounded-full border border-white/5 bg-white/5 px-1.5 py-0.5">