mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-14 05:37:40 +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:
@@ -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