mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] canvas: an unrelated spawn no longer despawns another agent's revealed subagents; reconcile learns keep-not-create ids (ENG-304)
This commit is contained in:
@@ -88,6 +88,7 @@ P_RELEASES: List[ReleaseNote] = [
|
||||
"An agent that sent work to a browser can no longer hang forever when the finished result gets lost on the way back; it notices, recovers, and redoes the step.",
|
||||
"The app opens seconds faster on machines with a crowded system temp folder. File uploads moved into OpenSwarm's own folder, so startup no longer pays a toll that grew with years of temp-file clutter.",
|
||||
"Clicking a chat in the sidebar or history now frames the whole card. The camera used to aim at the chat's collapsed footprint, so an opened chat could land with its bottom half off-screen and need a manual pan after every autofocus.",
|
||||
"Starting a new chat no longer makes another agent's revealed subagents disappear from the canvas. Their cards were being cleaned up as strays by the same pass that places the new one.",
|
||||
"Heavy sessions no longer vanish without a trace. When memory climbs past the safe line the app now sheds weight itself: preview thumbnails pause and refetchable caches drop, instead of growing until the operating system kills it mid-task.",
|
||||
],
|
||||
),
|
||||
|
||||
@@ -11,6 +11,12 @@ export interface SessionOrigin {
|
||||
/** A run that is still working, so its card is worth having on screen. */
|
||||
const LIVE_STATUSES: ReadonlySet<string> = new Set(['running', 'waiting_approval']);
|
||||
|
||||
/** True for a helper session (sub/browser/invoked agent) whose card can only exist by an explicit
|
||||
* reveal click; reconcile must never delete such a card while its session lives (ENG-304). */
|
||||
export function isPlumbingSession(session: SessionOrigin): boolean {
|
||||
return PLUMBING_MODES.has(session.mode);
|
||||
}
|
||||
|
||||
/** True for a chat the user started themselves, which is the only kind that earns a notification. */
|
||||
export function isUserLaunchedSession(session: SessionOrigin): boolean {
|
||||
return !session.workflow_run_id && !session.workflow_edit_id && !PLUMBING_MODES.has(session.mode);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// ENG-304: spawning an unrelated agent despawned another agent's live subagents. Mechanism:
|
||||
// revealSubAgent places cards for plumbing sessions (sub/browser/invoked agents) that
|
||||
// deservesCanvasCard filters out of the reconcile id-list forever, so the next id-list change
|
||||
// handed reconcileSessions a list without them and its delete loop removed the cards. keepIds is
|
||||
// the exemption: delete-immune while the session lives, never create-eligible.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import reducer, { reconcileSessions } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { isPlumbingSession, deservesCanvasCard } from '@/shared/state/isUserLaunchedSession';
|
||||
|
||||
function seedBoard() {
|
||||
let s = reducer(undefined, { type: '@@init' });
|
||||
const place = (sessionId: string, x: number) => {
|
||||
s = reducer(s, { type: 'dashboardLayout/placeCard', payload: { sessionId, x, y: 100, width: 480, height: 280, expandedSessionIds: [], exact: true } });
|
||||
};
|
||||
place('parent-1', 100);
|
||||
place('sub-1', 700);
|
||||
return { get: () => s, apply: (a: unknown) => { s = reducer(s, a as { type: string }); } };
|
||||
}
|
||||
|
||||
test('the bug, pinned: without keepIds an unrelated spawn despawns the revealed subagent', () => {
|
||||
const b = seedBoard();
|
||||
b.apply(reconcileSessions({ sessionIds: ['parent-1', 'new-agent'], expandedSessionIds: [] }));
|
||||
assert.equal(b.get().cards['sub-1'], undefined, 'this IS ENG-304; if this starts passing with a card present, the id-list derivation changed');
|
||||
assert.ok(b.get().cards['new-agent'], 'the spawn itself must still land');
|
||||
});
|
||||
|
||||
test('the fix: keepIds makes the revealed subagent delete-immune', () => {
|
||||
const b = seedBoard();
|
||||
b.apply(reconcileSessions({ sessionIds: ['parent-1', 'new-agent'], expandedSessionIds: [], keepIds: ['sub-1'] }));
|
||||
assert.ok(b.get().cards['sub-1'], 'live subagent card must survive an unrelated spawn');
|
||||
assert.ok(b.get().cards['new-agent']);
|
||||
assert.ok(b.get().cards['parent-1']);
|
||||
});
|
||||
|
||||
test('keep is not create: a keep id with no card stays cardless', () => {
|
||||
const b = seedBoard();
|
||||
b.apply(reconcileSessions({ sessionIds: ['parent-1'], expandedSessionIds: [], keepIds: ['sub-never-revealed'] }));
|
||||
assert.equal(b.get().cards['sub-never-revealed'], undefined, 'granting cards here would undo the ENG-256 rule');
|
||||
});
|
||||
|
||||
test('a stopped workflow run still loses its card (keepIds must not cover it)', () => {
|
||||
// The keep set is derived from isPlumbingSession, and a run session is mode "agent": not plumbing.
|
||||
const run = { mode: 'agent', workflow_run_id: 'r1', status: 'completed' };
|
||||
assert.equal(isPlumbingSession(run), false);
|
||||
assert.equal(deservesCanvasCard(run), false);
|
||||
const b = seedBoard();
|
||||
b.apply(reconcileSessions({ sessionIds: ['parent-1'], expandedSessionIds: [], keepIds: [] }));
|
||||
assert.equal(b.get().cards['sub-1'], undefined, 'not in the list, not in keep: deleted, as a stopped run should be');
|
||||
});
|
||||
|
||||
test('the lifecycle effect actually derives and passes keepIds', () => {
|
||||
const src = fs.readFileSync(path.join(process.cwd(), 'src/app/pages/Dashboard/hooks/lifecycle/useDashboardLifecycle.ts'), 'utf8');
|
||||
assert.ok(src.includes('isPlumbingSession(s)'), 'an exemption nobody computes is a boundary drawn but not enforced');
|
||||
assert.ok(src.includes('expandedSessionIds, keepIds }'), 'keepIds must reach the reducer');
|
||||
});
|
||||
Reference in New Issue
Block a user