fix(renderer): classify DesktopEvent types to stop full-refresh fallthrough (D-05)

All 14+ non-specially-handled events were pushing into the batcher with
entityKeyFromEvent returning null, which set hasDomainChanged = true and
triggered a full snapshot refresh on every agent-message, run-status, and
heartbeat-status -- defeating the granular patch machinery.

Per-type classification:
- run-status         -> granular run-updated (patchRun by runId)
- heartbeat-status   -> granular agent-updated (patchAgent by agentId)
- agent-message      -> granular task-updated (patchTask refetches comments)
- new-message, thread-wait-state-updated, domain-changed
                     -> full refresh, coalesced via 500ms batcher window
- automation-fired, recovery-needed
                     -> snapshot-irrelevant, skipped at event source

Widen batcher window from 50ms to 500ms so bursts coalesce into one reload.
Granular batches still flush at the same window cadence.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-16 21:58:32 -04:00
1 parent 8cf6be05bb
commit 2ec77677cb
2 files changed
+38 -3

No files matched your search

+15 -3
View File
@@ -1,6 +1,10 @@
import { useEffect, type MutableRefObject } from "react";
import type { DesktopEvent } from "@shared/types";
import { createDomainEventBatcher, type GranularEventEntry } from "../lib/ipc-batcher";
import {
createDomainEventBatcher,
isSnapshotIrrelevantEvent,
type GranularEventEntry,
} from "../lib/ipc-batcher";
import { useAppStore } from "../state/store";
export function useDesktopEvents(
@@ -106,10 +110,13 @@ export function useDesktopEvents(
}
}
// 500ms window for full-refresh events (e.g. new-message,
// thread-wait-state-updated) so bursts coalesce into one reload.
// Granular entity events still flush at this window but are cheap.
const batcher = createDomainEventBatcher(
() => { void refreshRef.current(); },
(events) => { void handleGranularBatch(events); },
50,
500,
);
const unsubscribe = window.agentCompany.subscribe((event: DesktopEvent) => {
@@ -140,7 +147,12 @@ export function useDesktopEvents(
store.getState().navigateTo(event.section, event.entityId);
return;
}
// All data-mutating events go through the enhanced batcher
// One-shot events that do not affect ProfileSnapshot -- handled by
// their own listeners elsewhere, never trigger a snapshot refresh.
if (isSnapshotIrrelevantEvent(event)) {
return;
}
// All remaining data-mutating events go through the enhanced batcher
batcher.push(event);
});
return () => {
+23
View File
@@ -20,11 +20,34 @@ export function entityKeyFromEvent(event: DesktopEvent): GranularEventEntry | nu
return { type: "agent-updated", entityId: event.agentId, action: event.action };
case "budget-changed":
return { type: "budget-changed", entityId: event.companyId, action: "changed" };
// run-status is a run-level state change -- map onto run entity so
// granular patchRun refetches the run record.
case "run-status":
return { type: "run-updated", entityId: event.runId, action: "status_changed" };
// heartbeat-status mutates agent metadata (nextWakeAt / enabled) -- map
// onto agent entity so granular patchAgent refetches.
case "heartbeat-status":
return { type: "agent-updated", entityId: event.agentId, action: "updated" };
// agent-message posts a new comment under a task; patchTask refetch will
// pick up the task's updated comment list.
case "agent-message":
return { type: "task-updated", entityId: event.taskId, action: "updated" };
default:
return null;
}
}
// Events that do NOT affect ProfileSnapshot and therefore must not trigger
// a snapshot refresh -- they are handled exclusively by their own listeners.
const SNAPSHOT_IRRELEVANT_EVENT_TYPES = new Set<DesktopEvent["type"]>([
"automation-fired",
"recovery-needed",
]);
export function isSnapshotIrrelevantEvent(event: DesktopEvent): boolean {
return SNAPSHOT_IRRELEVANT_EVENT_TYPES.has(event.type);
}
export function createDomainEventBatcher(
onFullRefresh: () => void,
onGranularBatch: (events: GranularEventEntry[]) => void,