mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-28 12:34:50 +02:00
[eric] canvas: Close says it dismissed the card, because the executor's closed_at cannot mean that (ENG-421)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U6zrBsUCNzpMBnov3rTVYV
This commit is contained in:
co-authored by
Claude Opus 5
parent
e277aec52c
commit
4fd0c1a10e
@@ -61,10 +61,12 @@ test('a run that ended on its own still despawns', () => {
|
||||
});
|
||||
|
||||
test('Close still dismisses it, even though Close also sets ended_by_user', () => {
|
||||
// Both routes stamp ended_by_user; only Close stamps closed_at, which is what separates them.
|
||||
// Both routes stamp ended_by_user, and `closed_at` cannot separate them: the workflow executor
|
||||
// stamps it at the end of EVERY step, including a stopped one. `dismissed_by_user` is the fact
|
||||
// that only the close route writes, so it is what tells "put it away" from "this run is over".
|
||||
assert.equal(deservesCanvasCard({
|
||||
mode: 'agent', workflow_run_id: 'r1', status: 'stopped',
|
||||
ended_by_user: true, closed_at: '2026-08-28T00:00:00Z',
|
||||
ended_by_user: true, closed_at: '2026-08-28T00:00:00Z', dismissed_by_user: true,
|
||||
}), false);
|
||||
});
|
||||
|
||||
@@ -80,3 +82,29 @@ test('a live run is unaffected either way', () => {
|
||||
test('a user-launched chat never depends on any of this', () => {
|
||||
assert.equal(deservesCanvasCard({ mode: 'agent', status: 'stopped' }), true);
|
||||
});
|
||||
|
||||
test('a user-stopped workflow run keeps its card even though the executor stamped closed_at', () => {
|
||||
// The bug the packaged drill found, and the reason this asserts the FIELD COMBINATION rather than
|
||||
// trusting the rule's prose: the executor closes the session at the end of every step, including a
|
||||
// stopped one, so `closed_at` is always set here. Gating on it made the fix dead code and the card
|
||||
// vanished ~2s after Stop on the shipped build.
|
||||
assert.equal(
|
||||
deservesCanvasCard({
|
||||
workflow_run_id: 'run-1',
|
||||
status: 'stopped',
|
||||
ended_by_user: true,
|
||||
closed_at: '2026-08-28T19:33:47.873352',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('a workflow run that ended ON ITS OWN still despawns, closed_at or not', () => {
|
||||
// The nightly-litter case the original rule exists for. Losing this is how the fix becomes a leak.
|
||||
for (const closed_at of [null, '2026-08-28T19:33:47.873352']) {
|
||||
assert.equal(
|
||||
deservesCanvasCard({ workflow_run_id: 'run-2', status: 'completed', ended_by_user: false, closed_at }),
|
||||
false,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface SessionOrigin {
|
||||
ended_by_user?: boolean;
|
||||
// Set by Close, never by Stop. It is what tells the two apart here.
|
||||
closed_at?: string | null;
|
||||
dismissed_by_user?: boolean;
|
||||
}
|
||||
|
||||
/** A run that is still working, so its card is worth having on screen. */
|
||||
@@ -38,7 +39,17 @@ export function isUserLaunchedSession(session: SessionOrigin): boolean {
|
||||
* this could not tell them apart, so pressing Stop on a workflow-backed agent made its card vanish
|
||||
* along with the transcript the user stopped it to read (ENG-421). Someone stops a run to LOOK at
|
||||
* it; the nightly-litter case it was written for is the one that ends by itself, which still
|
||||
* despawns. Close is what dismisses a card, and Close is the only route that stamps `closed_at`.
|
||||
* despawns.
|
||||
*
|
||||
* `closed_at` is NOT the release, and assuming it was made this whole rule dead code on the only
|
||||
* path it exists for. The workflow executor closes the agent session at the end of EVERY step,
|
||||
* including one the user stopped, purely so the run sorts into chat history (`get_history` orders by
|
||||
* `closed_at`, and nulls fall off the first page). So `closed_at` means "this run is over", not
|
||||
* "the user dismissed this card", and gating on it deleted the card ~2s after Stop. Caught on the
|
||||
* packaged build; dev never showed it because the drill there stopped a plain agent, not a
|
||||
* workflow-backed one. Dismissal is the layout's job: closing the card removes it from the
|
||||
* dashboard, which is where a user's "I'm done with this" already lives. The backend now says which
|
||||
* of the two happened: `dismissed_by_user` is stamped by the close route alone.
|
||||
*/
|
||||
export function deservesCanvasCard(
|
||||
session: SessionOrigin & { status?: string },
|
||||
@@ -46,5 +57,5 @@ export function deservesCanvasCard(
|
||||
if (isUserLaunchedSession(session)) return true;
|
||||
if (!session.workflow_run_id) return false;
|
||||
if (LIVE_STATUSES.has(session.status ?? '')) return true;
|
||||
return !!session.ended_by_user && !session.closed_at;
|
||||
return !!session.ended_by_user && !session.dismissed_by_user;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user