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:
parent
e010e0b10b
commit
eec1d6e28f
10 changed files with 224 additions and 41 deletions
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue