feat(protocol): deliver 'War Room' UI with Incursion Engine

We've transformed the Social-Dense Hub into a high-fidelity operational surface.
- BACKEND: Implemented Global Incursion Engine in agent-sessions.ts (N^2 overlap detection) and added the 60m 'Idle' state.
- API: Enriched the sessions payload with full metadata and active conflict arrays.
- HEADER: Delivered 4-state agent stations (Active/Stale/Evicted/Idle) with real-time 'time-ago' timers.
- FEED: Implemented the 'Fire Map' visuals:
  * Global Incursion Ticker: High-visibility alerts for agent collisions.
  * Local Conflict Badges: Pulsing pills on affected task cards.
- Refactored components for React-static compliance and strict TypeScript safety.

This commit completes the visibility track, allowing the human supervisor to monitor agent presence and friction in real-time.

OPERATIVE: silver-castle
SESSION: 2026-02-14-1430
This commit is contained in:
zenchantlive 2026-02-14 11:36:32 -08:00
parent e010e0b10b
commit eec1d6e28f
10 changed files with 224 additions and 41 deletions

View file

@ -52,8 +52,6 @@ export interface ActivityLeaseInput {
agent: string;
}
export type AgentLiveness = 'active' | 'stale' | 'evicted';
function userProfileRoot(): string {
return process.env.USERPROFILE?.trim() || os.homedir();
}
@ -260,16 +258,23 @@ export async function showAgent(input: ShowAgentInput): Promise<AgentCommandResp
}
}
export type AgentLiveness = 'active' | 'stale' | 'evicted' | 'idle';
/**
* Derives the liveness state of an agent based on its last seen timestamp.
* stale threshold: staleMinutes (default 15)
* evicted threshold: 2 * staleMinutes (default 30)
* active: < 15m
* stale: 15m - 30m
* evicted: 30m - 60m
* idle: >= 60m
*/
export function deriveLiveness(lastSeenAt: string, now: Date = new Date(), staleMinutes: number = 15): AgentLiveness {
const lastSeen = new Date(lastSeenAt).getTime();
const diffMs = now.getTime() - lastSeen;
const diffMin = diffMs / (1000 * 60);
if (diffMin >= 60) {
return 'idle';
}
if (diffMin >= 2 * staleMinutes) {
return 'evicted';
}

View file

@ -2,8 +2,9 @@ import type { ActivityEvent } from './activity';
import type { BeadIssue } from './types';
import { listAgents, deriveLiveness } from './agent-registry';
import { inboxAgentMessages, type AgentMessage } from './agent-mail';
import { statusAgentReservations, classifyOverlap } from './agent-reservations';
export type AgentSessionState = 'active' | 'reviewing' | 'deciding' | 'needs_input' | 'completed' | 'stale' | 'evicted';
export type AgentSessionState = 'active' | 'reviewing' | 'deciding' | 'needs_input' | 'completed' | 'stale' | 'evicted' | 'idle';
export interface SessionTaskCard {
id: string;
@ -50,6 +51,49 @@ export async function getAgentLivenessMap(): Promise<Record<string, string>> {
return map;
}
export interface Incursion {
scope: string;
agents: string[];
severity: 'exact' | 'partial';
}
/**
* Calculates global incursions by comparing all active reservations.
*/
export async function calculateIncursions(): Promise<Incursion[]> {
const statusResult = await statusAgentReservations({});
if (!statusResult.ok || !statusResult.data) return [];
const reservations = statusResult.data.reservations;
const incursions: Incursion[] = [];
const processedPairs = new Set<string>();
for (let i = 0; i < reservations.length; i++) {
for (let j = i + 1; j < reservations.length; j++) {
const resA = reservations[i];
const resB = reservations[j];
// Don't compare an agent against themselves
if (resA.agent_id === resB.agent_id) continue;
const overlap = classifyOverlap(resA.scope, resB.scope);
if (overlap !== 'disjoint') {
const key = [resA.agent_id, resB.agent_id].sort().join(':') + ':' + [resA.scope, resB.scope].sort().join('|');
if (processedPairs.has(key)) continue;
processedPairs.add(key);
incursions.push({
scope: overlap === 'exact' ? resA.scope : `${resA.scope}${resB.scope}`,
agents: [resA.agent_id, resB.agent_id],
severity: overlap
});
}
}
}
return incursions;
}
/**
* Gathers all relevant communication for all agents to build a summary for aggregation.
*/