import type { RelayAgentActivityAggregateState, RelayAgentActivityState, } from "@t3tools/contracts/relay"; import * as DateTime from "effect/DateTime"; import * as Option from "effect/Option"; import { isExpiredAgentActivityState, isTerminalPhase, MAX_ACTIVITY_ROWS, sanitizeAgentActivityAggregateState, } from "./agentActivityPayloads.ts"; export function statusForPhase(phase: RelayAgentActivityState["phase"]): string { switch (phase) { case "waiting_for_approval": return "Approval"; case "waiting_for_input": return "Input"; case "completed": return "Done"; case "failed": return "Failed"; case "starting": // Matches the web sidebar's pill wording (Sidebar.logic.ts) so the same // thread reads the same across surfaces. return "Connecting"; case "running": return "Working"; case "stale": return "Waiting"; } } function aggregateRowForState(state: RelayAgentActivityState) { return { environmentId: state.environmentId, threadId: state.threadId, projectTitle: state.projectTitle, threadTitle: state.threadTitle, modelTitle: state.modelTitle, phase: state.phase, status: statusForPhase(state.phase), updatedAt: state.updatedAt, deepLink: state.deepLink, }; } function terminalAggregateState(state: RelayAgentActivityState): RelayAgentActivityAggregateState { return sanitizeAgentActivityAggregateState({ title: "T3 Code", subtitle: state.phase === "failed" ? "Agent work failed" : "Agent work completed", activeCount: 0, updatedAt: state.updatedAt, activities: [aggregateRowForState(state)], }); } // How long a finished thread keeps its Done/Failed row in the aggregate while // other agents are still active. Long enough to be seen on the lock screen, // short enough that the activity list stays about live work. export const TERMINAL_AGENT_ACTIVITY_DISPLAY_TTL_MS = 15 * 60 * 1_000; function isRecentTerminalState(state: RelayAgentActivityState, nowMs: number): boolean { if (!isTerminalPhase(state)) { return false; } const updatedAtMs = Option.match(DateTime.make(state.updatedAt), { onNone: () => Number.NaN, onSome: (dt) => dt.epochMilliseconds, }); if (Number.isNaN(updatedAtMs)) { return false; } return nowMs - updatedAtMs <= TERMINAL_AGENT_ACTIVITY_DISPLAY_TTL_MS; } export function makeAggregateState(input: { readonly activeStates: ReadonlyArray; readonly terminalState: RelayAgentActivityState | null; readonly nowMs: number; }): RelayAgentActivityAggregateState | null { const activeStates = input.activeStates.filter( (state) => !isTerminalPhase(state) && !isExpiredAgentActivityState(state, input.nowMs), ); if (activeStates.length === 0) { if (input.terminalState !== null) { return terminalAggregateState(input.terminalState); } // With no live work, recently finished threads keep the card showing // Done/Failed content (an armed card never renders an empty state). The // newly-terminal alert rules key off the previously delivered aggregate, // so replays repaint this without buzzing. Once the terminal rows age // out, the aggregate is null and the delivery layer ends the card. const recentTerminal = input.activeStates .filter((state) => isRecentTerminalState(state, input.nowMs)) .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); const newest = recentTerminal[0]; if (!newest) { return null; } return sanitizeAgentActivityAggregateState({ title: "T3 Code", subtitle: newest.phase === "failed" ? "Agent work failed" : "Agent work completed", activeCount: 0, updatedAt: newest.updatedAt, activities: recentTerminal.slice(0, MAX_ACTIVITY_ROWS).map(aggregateRowForState), }); } // Recently finished threads ride along after the active ones (display slots // permitting) so a completion is visible as Done/Failed instead of the row // silently vanishing while other agents keep the activity alive. const recentTerminalStates = input.activeStates .filter((state) => isRecentTerminalState(state, input.nowMs)) .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); const displayedStates = [ ...activeStates .toSorted((a, b) => activityPhasePriority(a.phase) - activityPhasePriority(b.phase)) .slice(0, MAX_ACTIVITY_ROWS), ...recentTerminalStates, ].slice(0, MAX_ACTIVITY_ROWS); const updatedAt = [...activeStates, ...recentTerminalStates].reduce((latest, state) => state.updatedAt.localeCompare(latest.updatedAt) > 0 ? state : latest, ).updatedAt; return sanitizeAgentActivityAggregateState({ title: "T3 Code", subtitle: "Agent work in progress", activeCount: activeStates.length, updatedAt, activities: displayedStates.map(aggregateRowForState), }); } export function activityPhasePriority(phase: RelayAgentActivityState["phase"]): number { if (phase === "waiting_for_approval" || phase === "waiting_for_input") return 0; if (phase === "failed") return 1; if (phase === "starting" || phase === "running") return 2; return 3; }