feat(protocol): wire session aggregation and API with liveness
Finalizing the backend engine for the Operative Protocol. - Updated agent-sessions.ts to use the deriveLiveness logic and the 15m protocol threshold. - Integrated the agentLivenessMap into the session aggregation to provide real-time status in the Hub. - Updated the GET /api/sessions endpoint to fetch and serve liveness metadata. - Fixed linting warnings (unused imports) in reservations, sessions, and test files. This commit completes the backend contract for bb-u6f.6.2, providing the data layer necessary for the upcoming 'War Room' UI enhancements. OPERATIVE: silver-castle SESSION: 2026-02-14-1145
This commit is contained in:
parent
41f7cb8f24
commit
965d11c0b9
7 changed files with 48 additions and 15 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { readIssuesFromDisk } from '../../../lib/read-issues';
|
||||
import { activityEventBus } from '../../../lib/realtime';
|
||||
import { buildSessionTaskFeed, getCommunicationSummary } from '../../../lib/agent-sessions';
|
||||
import { buildSessionTaskFeed, getCommunicationSummary, getAgentLivenessMap } from '../../../lib/agent-sessions';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
|
@ -13,8 +13,9 @@ export async function GET(request: Request): Promise<Response> {
|
|||
const issues = await readIssuesFromDisk({ projectRoot, preferBd: true });
|
||||
const activity = activityEventBus.getHistory(projectRoot);
|
||||
const communication = await getCommunicationSummary();
|
||||
const livenessMap = await getAgentLivenessMap();
|
||||
|
||||
const feed = buildSessionTaskFeed(issues, activity, communication);
|
||||
const feed = buildSessionTaskFeed(issues, activity, communication, livenessMap);
|
||||
|
||||
return NextResponse.json({ ok: true, feed });
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import os from 'node:os';
|
|||
import path from 'node:path';
|
||||
|
||||
import { showAgent, deriveLiveness } from './agent-registry';
|
||||
import type { AgentRecord } from './agent-registry';
|
||||
import type { AgentMessage } from './agent-mail';
|
||||
|
||||
const MIN_TTL_MINUTES = 5;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import type { ActivityEvent } from './activity';
|
||||
import type { BeadIssue } from './types';
|
||||
import { listAgents } from './agent-registry';
|
||||
import { listAgents, deriveLiveness } from './agent-registry';
|
||||
import { inboxAgentMessages, type AgentMessage } from './agent-mail';
|
||||
|
||||
export type AgentSessionState = 'active' | 'reviewing' | 'deciding' | 'needs_input' | 'completed' | 'stale';
|
||||
export type AgentSessionState = 'active' | 'reviewing' | 'deciding' | 'needs_input' | 'completed' | 'stale' | 'evicted';
|
||||
|
||||
export interface SessionTaskCard {
|
||||
id: string;
|
||||
|
|
@ -34,8 +34,21 @@ export interface CommunicationSummary {
|
|||
messages: AgentMessage[];
|
||||
}
|
||||
|
||||
// 24 hours in ms
|
||||
const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
|
||||
// 15 minutes default stale threshold
|
||||
const STALE_THRESHOLD_MS = 15 * 60 * 1000;
|
||||
|
||||
export async function getAgentLivenessMap(): Promise<Record<string, string>> {
|
||||
const agentsResult = await listAgents({});
|
||||
const agents = agentsResult.data ?? [];
|
||||
const map: Record<string, string> = {};
|
||||
const now = new Date();
|
||||
|
||||
for (const agent of agents) {
|
||||
map[agent.agent_id] = deriveLiveness(agent.last_seen_at, now);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers all relevant communication for all agents to build a summary for aggregation.
|
||||
|
|
@ -96,7 +109,8 @@ export async function getAgentMetrics(
|
|||
export function buildSessionTaskFeed(
|
||||
issues: BeadIssue[],
|
||||
activity: ActivityEvent[],
|
||||
communicationSummary: CommunicationSummary
|
||||
communicationSummary: CommunicationSummary,
|
||||
agentLivenessMap: Record<string, string> = {}
|
||||
): EpicBucket[] {
|
||||
const epics = issues.filter(i => i.issue_type === 'epic');
|
||||
const tasks = issues.filter(i => i.issue_type !== 'epic');
|
||||
|
|
@ -136,11 +150,20 @@ export function buildSessionTaskFeed(
|
|||
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())[0] ?? null;
|
||||
};
|
||||
|
||||
const deriveState = (task: BeadIssue, lastEvent: ActivityEvent | null, pendingRequired: boolean): AgentSessionState => {
|
||||
const deriveState = (
|
||||
task: BeadIssue,
|
||||
lastEvent: ActivityEvent | null,
|
||||
pendingRequired: boolean,
|
||||
ownerLiveness?: string
|
||||
): AgentSessionState => {
|
||||
if (task.status === 'closed') return 'completed';
|
||||
if (task.status === 'blocked' || pendingRequired) return 'needs_input';
|
||||
|
||||
// Check staleness
|
||||
// If agent is evicted, the task session state is definitely evicted
|
||||
if (ownerLiveness === 'evicted') return 'evicted';
|
||||
if (ownerLiveness === 'stale') return 'stale';
|
||||
|
||||
// Check staleness of the TASK activity itself
|
||||
const lastActiveTime = lastEvent ? new Date(lastEvent.timestamp).getTime() : new Date(task.updated_at).getTime();
|
||||
if (Date.now() - lastActiveTime > STALE_THRESHOLD_MS) {
|
||||
return 'stale';
|
||||
|
|
@ -172,7 +195,8 @@ export function buildSessionTaskFeed(
|
|||
const pendingRequired = taskMessages.some(m => m.requires_ack && m.state !== 'acked');
|
||||
const latestMessage = taskMessages.sort((a, b) => b.created_at.localeCompare(a.created_at))[0];
|
||||
|
||||
const sessionState = deriveState(task, lastEvent, pendingRequired);
|
||||
const ownerLiveness = task.assignee ? agentLivenessMap[task.assignee] : undefined;
|
||||
const sessionState = deriveState(task, lastEvent, pendingRequired, ownerLiveness);
|
||||
|
||||
const card: SessionTaskCard = {
|
||||
id: task.id,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue