From 23f84eb8fcfaa18c7d814ec13e98b972153ddb6b Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Tue, 30 Jun 2026 15:11:32 -0700 Subject: [PATCH 1/2] Add observer join link and active chat reconciliation --- .../components/broker/BrokerDetailsPage.tsx | 43 +++++++++++++++++-- .../hooks/use-message-reconciliation.test.ts | 14 ++++++ .../src/hooks/use-message-reconciliation.ts | 15 +++++++ 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/components/broker/BrokerDetailsPage.tsx b/src/renderer/src/components/broker/BrokerDetailsPage.tsx index 3d2dfbd3..c7c4c184 100644 --- a/src/renderer/src/components/broker/BrokerDetailsPage.tsx +++ b/src/renderer/src/components/broker/BrokerDetailsPage.tsx @@ -7,6 +7,7 @@ import { Check, Cloud, Copy, + ExternalLink, KeyRound, List, MessageSquare, @@ -99,6 +100,11 @@ function compactValue(value: string | undefined, fallback = 'n/a'): string { return value?.trim() || fallback } +function observerUrlForWorkspaceKey(workspaceKey: string | undefined): string | undefined { + const key = workspaceKey?.trim() + return key ? `https://agentrelay.com/observer?key=${encodeURIComponent(key)}` : undefined +} + function getPrimaryRelaycastWorkspace( broker: BrokerDetails ): NonNullable['workspaces'][number] | undefined { @@ -377,11 +383,13 @@ function DetailField({ function CompactMetaRow({ label, value, - copyValue + copyValue, + action }: { label: string value: string copyValue?: string + action?: React.ReactNode }): React.ReactNode { return (
@@ -389,16 +397,40 @@ function CompactMetaRow({ {value} - {copyValue ? : } + {copyValue || action ? ( + + {action} + {copyValue ? : null} + + ) : ( + + )}
) } +function ObserverLink({ href }: { href: string }): React.ReactNode { + return ( + + + Join + + ) +} + function BrokerMetadataSummary({ broker }: { broker: BrokerDetails }): React.ReactNode { const apiKey = broker.apiKey || (broker.apiKeyAvailable ? 'stored in connection file' : 'n/a') const primaryWorkspace = getPrimaryRelaycastWorkspace(broker) const workspaceId = broker.relaycast?.defaultWorkspaceId || primaryWorkspace?.workspaceId const workspaceKey = broker.relaycast?.workspaceKey || broker.session?.workspaceKey + const observerUrl = observerUrlForWorkspaceKey(workspaceKey) const workspaceAlias = primaryWorkspace?.workspaceAlias || undefined const selfAgent = primaryWorkspace ? `${primaryWorkspace.selfName} / ${primaryWorkspace.selfAgentId}` @@ -423,7 +455,12 @@ function BrokerMetadataSummary({ broker }: { broker: BrokerDetails }): React.Rea value={compactValue(workspaceAlias ? `${workspaceAlias} (${workspaceId || 'no id'})` : workspaceId)} copyValue={workspaceId} /> - + : undefined} + /> { })).toBeNull() }) + it('polls only visible active chat rooms', () => { + expect(hooks.shouldPollActiveRoom('channel:project-1:general', 'visible')).toBe(true) + expect(hooks.shouldPollActiveRoom('dm:project-1:human|worker', 'visible')).toBe(true) + expect(hooks.shouldPollActiveRoom('none', 'visible')).toBe(false) + expect(hooks.shouldPollActiveRoom('channel:project-1:general', 'hidden')).toBe(false) + }) + it('schedules reconciliation after a human message send timestamp changes', () => { const reconciler = { schedule: vi.fn() } @@ -274,4 +281,11 @@ describe('createMessageReconciler', () => { expect(source).toMatch(/scheduleHumanMessageSentReconciliation\([\s\S]*lastHumanMessageSentAt[\s\S]*reconciler/) expect(source).toMatch(/\[lastHumanMessageSentAt,\s*reconciler\]/) }) + + it('wires active chat rooms into periodic reconciliation', () => { + const source = hooks.useMessageReconciliation.toString() + expect(source).toContain('ACTIVE_ROOM_RECONCILE_POLL_MS') + expect(source).toContain('active-room-poll') + expect(source).toMatch(/shouldPollActiveRoom\([\s\S]*activeRoomKey[\s\S]*document\.visibilityState/) + }) }) diff --git a/src/renderer/src/hooks/use-message-reconciliation.ts b/src/renderer/src/hooks/use-message-reconciliation.ts index a7d90e63..8f9810b4 100644 --- a/src/renderer/src/hooks/use-message-reconciliation.ts +++ b/src/renderer/src/hooks/use-message-reconciliation.ts @@ -13,6 +13,7 @@ import type { const DEFAULT_RECONCILE_LIMIT = 50 const DEFAULT_RECONCILE_DEBOUNCE_MS = 750 +export const ACTIVE_ROOM_RECONCILE_POLL_MS = 3_000 const BROKER_CONNECTED_STATUSES = new Set([ 'connected', 'event_stream_connected', @@ -211,6 +212,10 @@ function refreshEventStream(reason: string): void { void broker?.refreshEventStream?.(projectId, reason)?.catch(() => undefined) } +export function shouldPollActiveRoom(activeRoomKey: string, visibilityState: DocumentVisibilityState): boolean { + return activeRoomKey !== 'none' && visibilityState === 'visible' +} + export function useMessageReconciliation(): void { const activeProjectId = useProjectStore((s) => s.activeProjectId) const activeTabId = useUIStore((s) => s.activeTabId) @@ -249,6 +254,16 @@ export function useMessageReconciliation(): void { reconciler.schedule('active-room') }, [activeRoomKey, reconciler]) + useEffect(() => { + if (activeRoomKey === 'none') return + const interval = window.setInterval(() => { + if (shouldPollActiveRoom(activeRoomKey, document.visibilityState)) { + reconciler.schedule('active-room-poll') + } + }, ACTIVE_ROOM_RECONCILE_POLL_MS) + return () => window.clearInterval(interval) + }, [activeRoomKey, reconciler]) + useEffect(() => { if (brokerStatus === 'connected') { refreshEventStream('broker-status') From 8bde2d6f41223fcd7859d7ee49f505a77607baf1 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Tue, 30 Jun 2026 15:24:32 -0700 Subject: [PATCH 2/2] fix(chat): scope canonical-race guard to unclaimed echoes within a tight window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isCanonicalAlreadyPresent matched against any non-local record across the full 10s human dedupe window, including already-reconciled history (local: false) from an earlier send. That falsely suppressed the optimistic append of a later, legitimately repeated identical send. Scope the match to standalone, still-unclaimed canonical records (local === undefined) within a tight 2s race window — the guard exists to catch a sub-second startup race, not general dedupe. Addresses review feedback from gemini-code-assist, chatgpt-codex-connector, and coderabbitai on PR #387. Co-Authored-By: Claude Sonnet 5 --- .../src/stores/agent-store.stress.test.ts | 38 +++++++++++++++++++ src/renderer/src/stores/agent-store.ts | 27 ++++++++++--- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/stores/agent-store.stress.test.ts b/src/renderer/src/stores/agent-store.stress.test.ts index 868ded76..64f4005f 100644 --- a/src/renderer/src/stores/agent-store.stress.test.ts +++ b/src/renderer/src/stores/agent-store.stress.test.ts @@ -245,6 +245,44 @@ describe('agent-store stress', () => { expect(messages[0]!.local).not.toBe(true) }) + it('addHumanMessage does not drop a second identical send shortly after the first was reconciled', () => { + // Regression for review feedback on the race guard above: matching + // isCanonicalAlreadyPresent against ANY non-local record (including + // local: false, set by reconcileChatMessages once an earlier optimistic + // send is confirmed) would falsely suppress a later, legitimately + // repeated send within the 10s human dedupe window. The guard must only + // match a still-unclaimed standalone canonical record (local === + // undefined) within a tight race window, not confirmed history. + const store = useAgentStore.getState() + store.addHumanMessage('#general', 'repeat-body', 'p1') + const firstLocal = useAgentStore.getState().messages.find( + (m) => m.body === 'repeat-body' && m.local === true + ) + expect(firstLocal).toBeTruthy() + + store.reconcileMessages([{ + id: 'canonical-repeat-1', + from: 'human', + to: '#general', + body: 'repeat-body', + timestamp: firstLocal!.timestamp, + isHuman: true, + projectId: 'p1' + }]) + expect( + useAgentStore.getState().messages.find((m) => m.id === 'canonical-repeat-1')?.local + ).toBe(false) + + vi.advanceTimersByTime(3_000) + store.addHumanMessage('#general', 'repeat-body', 'p1') + + const repeatMessages = useAgentStore.getState().messages.filter( + (m) => m.body === 'repeat-body' + ) + expect(repeatMessages.length).toBe(2) + expect(repeatMessages.filter((m) => m.local === true).length).toBe(1) + }) + it('cross-project agent dedupe does not false-positive — identical body, different projectId survives twice', () => { // Invariant 3 — projectId mismatch must NEVER collapse two distinct sends. const store = useAgentStore.getState() diff --git a/src/renderer/src/stores/agent-store.ts b/src/renderer/src/stores/agent-store.ts index 308b985b..279681c0 100644 --- a/src/renderer/src/stores/agent-store.ts +++ b/src/renderer/src/stores/agent-store.ts @@ -113,6 +113,11 @@ const JOIN_NOTICE_DEDUPE_WINDOW_MS = 30_000 // with different event_ids (per AGENTS.md: renderer is the final // guardrail; stable event_id is the broker's job). const AGENT_MESSAGE_DEDUPE_WINDOW_MS = 2_000 +// Tighter window for the canonical-arrived-before-optimistic race guard +// (isCanonicalAlreadyPresent) — that guard only exists to catch a +// sub-second startup race, not general human dedupe, so it must not reach +// back across the full HUMAN_MESSAGE_DEDUPE_WINDOW_MS. +const HUMAN_RACE_DEDUPE_WINDOW_MS = 2_000 export function getAgentKey(projectId: string | undefined, name: string): string { return `${projectId || 'unknown'}:${name}` @@ -457,13 +462,14 @@ function isDuplicateAgentEcho( function humanMessageContentMatches( message: ChatMessage, - candidate: Pick + candidate: Pick, + windowMs: number = HUMAN_MESSAGE_DEDUPE_WINDOW_MS ): boolean { return isHumanMessage(message) && message.body === candidate.body && (!message.projectId || !candidate.projectId || message.projectId === candidate.projectId) && normalizeMessageTarget(message.to) === normalizeMessageTarget(candidate.to) && - Math.abs(message.timestamp - candidate.timestamp) < HUMAN_MESSAGE_DEDUPE_WINDOW_MS + Math.abs(message.timestamp - candidate.timestamp) < windowMs } // Detects the canonical-of-optimistic case: an incoming broker record @@ -484,13 +490,24 @@ function isCanonicalEchoOfLocalHuman( // reconcileChatMessages can never clean up — once the canonical id is // already present in `messages`, reconcile's id-match branch short-circuits // before it ever reaches the content-based optimistic-matching logic, -// leaving the duplicate stuck permanently. Scoped to local !== true records -// for the same reason as isCanonicalEchoOfLocalHuman above. +// leaving the duplicate stuck permanently. +// +// Scoped to `local === undefined` (a standalone canonical record that +// arrived via relay_inbound and was never claimed by reconcileChatMessages) +// rather than `local !== true` — `local: false` means an EARLIER send was +// already fully reconciled, and matching against that confirmed history +// would falsely suppress a later, legitimately repeated send (e.g. "ok" +// then "ok" again a few seconds after the first one synced). A tight race +// window is used for the same reason: this guard exists to catch a sub- +// second startup race, not general dedupe, so it shouldn't reach back +// across the full human dedupe window. function isCanonicalAlreadyPresent( messages: ChatMessage[], candidate: Pick ): boolean { - return messages.some((message) => message.local !== true && humanMessageContentMatches(message, candidate)) + return messages.some((message) => + message.local === undefined && humanMessageContentMatches(message, candidate, HUMAN_RACE_DEDUPE_WINDOW_MS) + ) } function createChannelJoinNotice(