mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-27 12:04:49 +02:00
[eric] frontend: the self-heal pill's flag lives beside the sessions, so a server refresh cannot wipe it before it shows
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012G8kyALnPjsA7aJFmMBq3R
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
5e4d7cd096
commit
8acfe2c15e
@@ -83,8 +83,9 @@ def squeezed_context_window() -> int:
|
||||
if n < VALVE_ELIGIBLE_WINDOW:
|
||||
logger.warning(
|
||||
"cli_context_squeeze window %d puts the compaction trigger under a turn's ~%d-token "
|
||||
"baseline, so OUR mid-turn breaker is INELIGIBLE and cannot fire at any input. This "
|
||||
"run can reproduce the CLI's autocompact thrash; it can say nothing about our valve.",
|
||||
"baseline, so the breaker's CROSSING rule is INELIGIBLE (every reading already sits above "
|
||||
"the trigger); only its 20,000-token growth rule can fire here, so a break in this run "
|
||||
"proves that half alone. The run still reproduces the CLI's autocompact thrash.",
|
||||
n, TURN_BASELINE_TOKENS,
|
||||
)
|
||||
return n
|
||||
|
||||
@@ -26,7 +26,7 @@ const COPY: Record<SelfHealKind, { text: string; why: (outstandingS: number | nu
|
||||
export const SelfHealPill: React.FC<{ sessionId: string }> = ({ sessionId }) => {
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
const heal = useAppSelector((s) => s.agents.sessions[sessionId]?.self_heal);
|
||||
const heal = useAppSelector((s) => s.agents.selfHeals[sessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!heal) return;
|
||||
|
||||
@@ -125,8 +125,6 @@ export interface AgentSession {
|
||||
// Parked waiting for the connection back; unlike the pills above this can last minutes, so the UI has to say so.
|
||||
reconnect_wait?: { retry_in_s: number | null; attempt: number | null; at: string } | null;
|
||||
provider_retrying?: { attempt: number | null; delay_ms: number | null; at: string } | null;
|
||||
// One transient "OpenSwarm healed something mid-turn" pill; the kind picks the wording.
|
||||
self_heal?: { kind: SelfHealKind; at: string; outstanding_s: number | null } | null;
|
||||
// Set when a view-builder turn installed/changed deps, so the app card does a HARD reload (Vite restart) at turn-finish instead of the soft one. Reset when the next turn starts.
|
||||
app_deps_changed?: boolean;
|
||||
mcp_suggestions?: Array<{ id: string; title: string; description: string; reason?: string }>;
|
||||
@@ -176,8 +174,17 @@ interface HistorySearchState {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
// One transient "OpenSwarm healed something mid-turn" pill per session; the kind picks the wording.
|
||||
export interface SelfHeal {
|
||||
kind: SelfHealKind;
|
||||
at: string;
|
||||
outstanding_s: number | null;
|
||||
}
|
||||
|
||||
interface AgentsState {
|
||||
sessions: Record<string, AgentSession>;
|
||||
// Kept OUTSIDE the session objects on purpose: every server refresh replaces a session wholesale, and a flag stored on it died before the pill could mount.
|
||||
selfHeals: Record<string, SelfHeal>;
|
||||
history: Record<string, HistorySession>;
|
||||
activeSessionId: string | null;
|
||||
expandedSessionIds: string[];
|
||||
@@ -190,6 +197,7 @@ interface AgentsState {
|
||||
|
||||
const initialState: AgentsState = {
|
||||
sessions: {},
|
||||
selfHeals: {},
|
||||
history: {},
|
||||
activeSessionId: null,
|
||||
expandedSessionIds: [],
|
||||
@@ -1102,19 +1110,15 @@ const agentsSlice = createSlice({
|
||||
state,
|
||||
action: PayloadAction<{ sessionId: string; kind: SelfHealKind; outstandingS?: number | null }>
|
||||
) {
|
||||
const session = state.sessions[action.payload.sessionId];
|
||||
if (session) {
|
||||
session.self_heal = {
|
||||
kind: action.payload.kind,
|
||||
at: new Date().toISOString(),
|
||||
outstanding_s: action.payload.outstandingS ?? null,
|
||||
};
|
||||
}
|
||||
state.selfHeals[action.payload.sessionId] = {
|
||||
kind: action.payload.kind,
|
||||
at: new Date().toISOString(),
|
||||
outstanding_s: action.payload.outstandingS ?? null,
|
||||
};
|
||||
},
|
||||
|
||||
clearSelfHeal(state, action: PayloadAction<{ sessionId: string }>) {
|
||||
const session = state.sessions[action.payload.sessionId];
|
||||
if (session) session.self_heal = null;
|
||||
delete state.selfHeals[action.payload.sessionId];
|
||||
},
|
||||
|
||||
clearContextOverflow(
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import reducer, { clearSelfHeal, fetchSession, resumeSession, setSelfHeal, updateSession } from './agentsSlice';
|
||||
import type { AgentSession } from './agentsSlice';
|
||||
|
||||
// The pill's flag used to live ON the session object, and every server refresh replaces that object
|
||||
// wholesale (fetchSession on expand, updateSession on each status frame, resumeSession), so a heal that
|
||||
// landed while the card was collapsed was wiped before the pill could mount: the wire carried
|
||||
// agent:tool_recovered, the screen showed nothing. The flag now lives beside the sessions, not in them.
|
||||
function session(id: string): AgentSession {
|
||||
return { id, name: 'F2', status: 'running', messages: [], pending_approvals: [], tool_group_meta: {} } as unknown as AgentSession;
|
||||
}
|
||||
const seeded = reducer(undefined, updateSession(session('s1')));
|
||||
|
||||
test('a heal survives every reducer that replaces the session object', () => {
|
||||
let state = reducer(seeded, setSelfHeal({ sessionId: 's1', kind: 'tool_restarted', outstandingS: 25 }));
|
||||
assert.equal(state.selfHeals.s1?.kind, 'tool_restarted');
|
||||
state = reducer(state, fetchSession.fulfilled(session('s1'), 'req', { sessionId: 's1' } as never));
|
||||
assert.equal(state.selfHeals.s1?.kind, 'tool_restarted', 'fetchSession (card expanded) kept it');
|
||||
state = reducer(state, updateSession(session('s1')));
|
||||
assert.equal(state.selfHeals.s1?.kind, 'tool_restarted', 'a status frame kept it');
|
||||
state = reducer(state, resumeSession.fulfilled(session('s1'), 'req', { sessionId: 's1' } as never));
|
||||
assert.equal(state.selfHeals.s1?.outstanding_s, 25, 'resume kept it');
|
||||
assert.ok(!('self_heal' in state.sessions.s1), 'nothing on the session object carries it any more');
|
||||
});
|
||||
|
||||
test('the pill clears it, and a heal for a card that is not on the board is still recorded', () => {
|
||||
let state = reducer(seeded, setSelfHeal({ sessionId: 'not-loaded', kind: 'cli_compacted' }));
|
||||
assert.equal(state.selfHeals['not-loaded']?.kind, 'cli_compacted');
|
||||
state = reducer(state, clearSelfHeal({ sessionId: 'not-loaded' }));
|
||||
assert.equal(state.selfHeals['not-loaded'], undefined);
|
||||
});
|
||||
Reference in New Issue
Block a user