mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 06:06:24 +00:00
fix(control,ui): close review findings - heartbeat starvation, resume race, stale UI writes
Fixes from the post-migration code review (3 major, 4 minor): - worker: heartbeat between every drained command so a long queue can't starve proof-of-life into a false reap; worker_stale_after default 60s -> 300s (one slow sync/login command must not look like a crash) - repository.create_run: enforces one running run per agent atomically (agent-row FOR UPDATE on Postgres; SQLite's single writer suffices) - two workers claiming resumes for the same agent can no longer both launch claude on one session; resume surfaces the loss loudly - headless._settle: upload the final session archive BEFORE marking the run finished - a resume claimed the instant a run leaves 'running' materializes from session_archives, and the old order let it race an incomplete archive into needless context re-injection (found as a test flake, real in production) - store.tsx: generation token drops in-flight loadRun writes after the user switches runs (run A's events/log/checkmark no longer land on run B), plus id-keyed dedup on event appends from overlapping polls - credsync: credential files written 0600 from the first byte - headless: seq counter locked (reader thread + supervisor both emit events); proc.stdout closed after reader join - login: submit pins to the latest CLAIMED login_start (a still-running one previously pinned to the wrong worker) Suite 296 green (new: create_run conflict coverage); reaper tests track the new staleness default.
This commit is contained in:
@@ -211,6 +211,10 @@ export function DashboardProvider({
|
||||
const [events, setEvents] = useState<AgentEvent[]>([]);
|
||||
const eventsRef = useRef<AgentEvent[]>([]);
|
||||
eventsRef.current = events;
|
||||
/* Bumped on every selectRun: an in-flight loadRun that resolves after the user picked
|
||||
* a different run must drop its writes, or run A's checkmark/log/events land on run
|
||||
* B's panel (the 5s tick keeps polls in flight constantly). */
|
||||
const runGenRef = useRef(0);
|
||||
const [approvals, setApprovals] = useState<Approval[]>([]);
|
||||
const [hosts, setHosts] = useState<Host[]>([]);
|
||||
const [commands, setCommands] = useState<Command[]>([]);
|
||||
@@ -270,12 +274,15 @@ export function DashboardProvider({
|
||||
|
||||
const loadRun = useCallback(async (projectId: string, name: string) => {
|
||||
const path = `/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(name)}`;
|
||||
const gen = runGenRef.current;
|
||||
const stale = () => gen !== runGenRef.current;
|
||||
try {
|
||||
const cm = await clientRef.current.api<Checkmark>(`${path}/checkmark`);
|
||||
if (stale()) return;
|
||||
setCheckmark(cm);
|
||||
setCheckmarkMissing(false);
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) return;
|
||||
if (e instanceof AuthError || stale()) return;
|
||||
if ((e as ApiError).status === 404) {
|
||||
setCheckmark(null);
|
||||
setCheckmarkMissing(true);
|
||||
@@ -285,9 +292,10 @@ export function DashboardProvider({
|
||||
const entries = await clientRef.current.api<LogEntry[]>(
|
||||
`${path}/log?limit=${LOG_LIMIT}&offset=${logOffsetRef.current}`,
|
||||
);
|
||||
if (stale()) return;
|
||||
setLog(entries);
|
||||
} catch (e) {
|
||||
swallow(e);
|
||||
if (!stale()) swallow(e);
|
||||
}
|
||||
try {
|
||||
// Cursor poll: only events newer than what we already hold come back.
|
||||
@@ -296,9 +304,16 @@ export function DashboardProvider({
|
||||
const fresh = await clientRef.current.api<AgentEvent[]>(
|
||||
`${path}/events?after_id=${after}&limit=500`,
|
||||
);
|
||||
if (fresh.length) setEvents((prev) => [...prev, ...fresh]);
|
||||
if (stale() || fresh.length === 0) return;
|
||||
// Dedup by id at append time: overlapping polls for the same run can both fetch
|
||||
// from the same cursor; only genuinely-new rows may append.
|
||||
setEvents((prev) => {
|
||||
const last = prev.length ? prev[prev.length - 1].id : 0;
|
||||
const add = fresh.filter((e) => e.id > last);
|
||||
return add.length ? [...prev, ...add] : prev;
|
||||
});
|
||||
} catch (e) {
|
||||
swallow(e);
|
||||
if (!stale()) swallow(e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -419,6 +434,7 @@ export function DashboardProvider({
|
||||
|
||||
const selectRun = useCallback(
|
||||
(projectId: string, name: string) => {
|
||||
runGenRef.current += 1; // invalidate any in-flight loadRun for the previous run
|
||||
setSelectedRun({ projectId, name });
|
||||
setLogOffset(0);
|
||||
logOffsetRef.current = 0;
|
||||
|
||||
Reference in New Issue
Block a user