mirror of
https://github.com/block/buzz.git
synced 2026-11-09 02:41:57 +00:00
Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co>
359 lines
10 KiB
TypeScript
359 lines
10 KiB
TypeScript
import * as React from "react";
|
|
|
|
import { subscribeToAgentObserverFrames } from "@/shared/api/observerRelay";
|
|
import type { RelayEvent, ManagedAgent } from "@/shared/api/types";
|
|
import { getIdentity } from "@/shared/api/tauri";
|
|
import { decryptObserverEvent } from "@/shared/api/tauriObserver";
|
|
import { normalizePubkey } from "@/shared/lib/pubkey";
|
|
import type {
|
|
ConnectionState,
|
|
ObserverEvent,
|
|
TranscriptItem,
|
|
} from "./ui/agentSessionTypes";
|
|
import {
|
|
type TranscriptState,
|
|
buildTranscriptState,
|
|
createEmptyTranscriptState,
|
|
processTranscriptEvent,
|
|
} from "./ui/agentSessionTranscript";
|
|
|
|
const MAX_OBSERVER_EVENTS = 800;
|
|
|
|
export type ObserverSnapshot = {
|
|
connectionState: ConnectionState;
|
|
errorMessage: string | null;
|
|
events: ObserverEvent[];
|
|
};
|
|
|
|
const IDLE_SNAPSHOT: ObserverSnapshot = {
|
|
connectionState: "idle",
|
|
errorMessage: null,
|
|
events: [],
|
|
};
|
|
|
|
const EMPTY_TRANSCRIPT: TranscriptItem[] = [];
|
|
|
|
const listeners = new Set<() => void>();
|
|
const eventsByAgent = new Map<string, ObserverEvent[]>();
|
|
const transcriptByAgent = new Map<string, TranscriptState>();
|
|
const snapshotByAgent = new Map<string, ObserverSnapshot>();
|
|
|
|
// Normalized pubkeys of agents we are actively managing. Only events whose
|
|
// "agent" tag matches an entry here will be decrypted (defense-in-depth).
|
|
//
|
|
// This set is the *union* of every active subscriber's contribution. Multiple
|
|
// callers of `useManagedAgentObserverBridge` (e.g. the channel screen and the
|
|
// profile panel) can be mounted at once, each tracking a different agent list.
|
|
// We key each subscriber's contribution in `knownAgentsBySubscription` and
|
|
// recompute the union, so co-mounted callers no longer clobber each other.
|
|
const knownAgentPubkeys = new Set<string>();
|
|
const knownAgentsBySubscription = new Map<string, Set<string>>();
|
|
|
|
function recomputeKnownAgentPubkeys() {
|
|
knownAgentPubkeys.clear();
|
|
for (const subscriptionAgents of knownAgentsBySubscription.values()) {
|
|
for (const pubkey of subscriptionAgents) {
|
|
knownAgentPubkeys.add(pubkey);
|
|
}
|
|
}
|
|
}
|
|
|
|
function registerKnownAgents(
|
|
subscriptionId: string,
|
|
pubkeys: readonly string[],
|
|
) {
|
|
knownAgentsBySubscription.set(
|
|
subscriptionId,
|
|
new Set(pubkeys.map((pubkey) => normalizePubkey(pubkey))),
|
|
);
|
|
recomputeKnownAgentPubkeys();
|
|
}
|
|
|
|
function unregisterKnownAgents(subscriptionId: string) {
|
|
if (knownAgentsBySubscription.delete(subscriptionId)) {
|
|
recomputeKnownAgentPubkeys();
|
|
}
|
|
}
|
|
|
|
let connectionState: ConnectionState = "idle";
|
|
let errorMessage: string | null = null;
|
|
let unsubscribeRelay: (() => Promise<void>) | null = null;
|
|
let startPromise: Promise<void> | null = null;
|
|
let eventProcessingQueue: Promise<void> = Promise.resolve();
|
|
let generation = 0;
|
|
|
|
function notifyListeners() {
|
|
for (const listener of listeners) {
|
|
listener();
|
|
}
|
|
}
|
|
|
|
function invalidateSnapshot(key: string) {
|
|
snapshotByAgent.delete(key);
|
|
}
|
|
|
|
function setConnectionState(
|
|
nextState: ConnectionState,
|
|
nextErrorMessage: string | null = errorMessage,
|
|
) {
|
|
connectionState = nextState;
|
|
errorMessage = nextErrorMessage;
|
|
// Invalidate all cached snapshots since connectionState changed
|
|
snapshotByAgent.clear();
|
|
notifyListeners();
|
|
}
|
|
|
|
function observerTag(event: RelayEvent, tagName: string) {
|
|
return event.tags.find((tag) => tag[0] === tagName)?.[1] ?? null;
|
|
}
|
|
|
|
function appendAgentEvent(agentPubkey: string, event: ObserverEvent) {
|
|
const key = normalizePubkey(agentPubkey);
|
|
const current = eventsByAgent.get(key) ?? [];
|
|
if (
|
|
current.some(
|
|
(existing) =>
|
|
existing.seq === event.seq && existing.timestamp === event.timestamp,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const sorted = [...current, event].sort(compareObserverEvents);
|
|
const trimmed = sorted.length > MAX_OBSERVER_EVENTS;
|
|
const final = trimmed
|
|
? sorted.slice(sorted.length - MAX_OBSERVER_EVENTS)
|
|
: sorted;
|
|
eventsByAgent.set(key, final);
|
|
|
|
// Determine whether the new event landed at the end of the sorted array.
|
|
// If it did (common case), we can incrementally process just this event.
|
|
// If not (out-of-order arrival) or if we trimmed, fall back to full rebuild.
|
|
const eventAtEnd = sorted[sorted.length - 1] === event;
|
|
|
|
if (eventAtEnd && !trimmed) {
|
|
// Fast path: incremental update
|
|
const transcriptState =
|
|
transcriptByAgent.get(key) ?? createEmptyTranscriptState();
|
|
const updatedTranscript = processTranscriptEvent(transcriptState, event);
|
|
transcriptByAgent.set(key, updatedTranscript);
|
|
} else {
|
|
// Slow path: full rebuild (out-of-order insertion or trim fired)
|
|
transcriptByAgent.set(key, buildTranscriptState(final));
|
|
}
|
|
|
|
// Invalidate cached snapshot for this agent
|
|
invalidateSnapshot(key);
|
|
|
|
notifyListeners();
|
|
}
|
|
|
|
export function compareObserverEvents(
|
|
left: ObserverEvent,
|
|
right: ObserverEvent,
|
|
) {
|
|
const leftTime = Date.parse(left.timestamp);
|
|
const rightTime = Date.parse(right.timestamp);
|
|
if (Number.isFinite(leftTime) && Number.isFinite(rightTime)) {
|
|
const timeDiff = leftTime - rightTime;
|
|
if (timeDiff !== 0) {
|
|
return timeDiff;
|
|
}
|
|
}
|
|
|
|
return left.seq - right.seq;
|
|
}
|
|
|
|
async function handleRelayObserverEvent(
|
|
event: RelayEvent,
|
|
activeGeneration: number,
|
|
) {
|
|
const agentPubkey = observerTag(event, "agent");
|
|
const frame = observerTag(event, "frame");
|
|
if (!agentPubkey || frame !== "telemetry") {
|
|
return;
|
|
}
|
|
|
|
// Verify agent is known/trusted before decrypting.
|
|
// Silently drop events from agents we are not managing.
|
|
if (!knownAgentPubkeys.has(normalizePubkey(agentPubkey))) {
|
|
return;
|
|
}
|
|
|
|
// Defense-in-depth: verify the event sender matches the claimed agent pubkey.
|
|
// The relay gates on is_agent_owner, but a compromised relay could misroute.
|
|
if (normalizePubkey(event.pubkey) !== normalizePubkey(agentPubkey)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const parsed = (await decryptObserverEvent(event)) as ObserverEvent;
|
|
if (activeGeneration !== generation) {
|
|
return;
|
|
}
|
|
appendAgentEvent(agentPubkey, parsed);
|
|
} catch (error) {
|
|
if (activeGeneration !== generation) {
|
|
return;
|
|
}
|
|
setConnectionState(
|
|
"error",
|
|
error instanceof Error
|
|
? `Observer event decrypt failed: ${error.message}`
|
|
: "Observer event decrypt failed.",
|
|
);
|
|
}
|
|
}
|
|
|
|
export function ensureRelayObserverSubscription() {
|
|
if (unsubscribeRelay) {
|
|
return Promise.resolve();
|
|
}
|
|
if (startPromise) {
|
|
return startPromise;
|
|
}
|
|
|
|
const activeGeneration = generation;
|
|
setConnectionState("connecting", null);
|
|
startPromise = (async () => {
|
|
const identity = await getIdentity();
|
|
const unsubscribe = await subscribeToAgentObserverFrames(
|
|
identity.pubkey,
|
|
(event) => {
|
|
eventProcessingQueue = eventProcessingQueue
|
|
.then(() => handleRelayObserverEvent(event, activeGeneration))
|
|
.catch((error) => {
|
|
if (activeGeneration !== generation) {
|
|
return;
|
|
}
|
|
setConnectionState(
|
|
"error",
|
|
error instanceof Error
|
|
? `Observer event handling failed: ${error.message}`
|
|
: "Observer event handling failed.",
|
|
);
|
|
});
|
|
},
|
|
);
|
|
if (activeGeneration !== generation) {
|
|
await unsubscribe();
|
|
return;
|
|
}
|
|
unsubscribeRelay = unsubscribe;
|
|
setConnectionState("open", null);
|
|
})()
|
|
.catch((error) => {
|
|
if (activeGeneration === generation) {
|
|
setConnectionState(
|
|
"error",
|
|
error instanceof Error
|
|
? error.message
|
|
: "Observer relay subscription failed.",
|
|
);
|
|
}
|
|
})
|
|
.finally(() => {
|
|
if (activeGeneration === generation) {
|
|
startPromise = null;
|
|
}
|
|
});
|
|
|
|
return startPromise;
|
|
}
|
|
|
|
export function subscribeAgentObserverStore(listener: () => void) {
|
|
listeners.add(listener);
|
|
return () => {
|
|
listeners.delete(listener);
|
|
};
|
|
}
|
|
|
|
export function getAgentObserverSnapshot(
|
|
agentPubkey?: string | null,
|
|
enabled?: boolean,
|
|
): ObserverSnapshot {
|
|
if (!enabled || !agentPubkey) {
|
|
return IDLE_SNAPSHOT;
|
|
}
|
|
const key = normalizePubkey(agentPubkey);
|
|
const cached = snapshotByAgent.get(key);
|
|
if (
|
|
cached &&
|
|
cached.connectionState === connectionState &&
|
|
cached.errorMessage === errorMessage
|
|
) {
|
|
return cached;
|
|
}
|
|
const snapshot: ObserverSnapshot = {
|
|
connectionState,
|
|
errorMessage,
|
|
events: eventsByAgent.get(key) ?? [],
|
|
};
|
|
snapshotByAgent.set(key, snapshot);
|
|
return snapshot;
|
|
}
|
|
|
|
export function getAgentTranscript(
|
|
agentPubkey?: string | null,
|
|
enabled?: boolean,
|
|
): TranscriptItem[] {
|
|
if (!enabled || !agentPubkey) {
|
|
return EMPTY_TRANSCRIPT;
|
|
}
|
|
const key = normalizePubkey(agentPubkey);
|
|
const state = transcriptByAgent.get(key);
|
|
return state?.items ?? EMPTY_TRANSCRIPT;
|
|
}
|
|
|
|
export function useManagedAgentObserverBridge(
|
|
agents: readonly Pick<ManagedAgent, "pubkey" | "status">[],
|
|
) {
|
|
const subscriptionId = React.useId();
|
|
const hasActiveAgent = React.useMemo(
|
|
() =>
|
|
agents.some(
|
|
(agent) => agent.status === "running" || agent.status === "deployed",
|
|
),
|
|
[agents],
|
|
);
|
|
|
|
const agentPubkeys = React.useMemo(
|
|
() => agents.map((agent) => agent.pubkey),
|
|
[agents],
|
|
);
|
|
|
|
// Keep this subscriber's slice of the trusted-pubkey set in sync with its
|
|
// own agent list. The store recomputes the union across all subscribers, so
|
|
// a co-mounted caller no longer wipes out this caller's agents.
|
|
React.useEffect(() => {
|
|
registerKnownAgents(subscriptionId, agentPubkeys);
|
|
return () => {
|
|
unregisterKnownAgents(subscriptionId);
|
|
};
|
|
}, [subscriptionId, agentPubkeys]);
|
|
|
|
React.useEffect(() => {
|
|
if (!hasActiveAgent) {
|
|
return;
|
|
}
|
|
void ensureRelayObserverSubscription();
|
|
}, [hasActiveAgent]);
|
|
}
|
|
|
|
export function resetAgentObserverStore() {
|
|
generation += 1;
|
|
const unsubscribe = unsubscribeRelay;
|
|
unsubscribeRelay = null;
|
|
startPromise = null;
|
|
eventProcessingQueue = Promise.resolve();
|
|
eventsByAgent.clear();
|
|
transcriptByAgent.clear();
|
|
snapshotByAgent.clear();
|
|
knownAgentPubkeys.clear();
|
|
knownAgentsBySubscription.clear();
|
|
connectionState = "idle";
|
|
errorMessage = null;
|
|
notifyListeners();
|
|
void unsubscribe?.();
|
|
}
|