You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(automation)!: getSuspendedScreen reads the durable store, not just the hot cache (#4515) (#4566)
`AutomationEngine.getSuspendedScreen` was synchronous, so it could only ever
read the in-memory hot cache — it structurally could not consult the
suspended-run store. But `SuspendedRun.screen` IS persisted
(`sys_automation_run.screen_json`) and `resume()` cold-reads it back via
`loadSuspendedRun` on a cache miss. The result, for a durably suspended screen
run after a process restart: `POST …/runs/:runId/resume` worked while
`GET …/runs/:runId/screen` returned 404 "No pending screen for run" — the
refresh-safe re-fetch failing in exactly the situation it exists for (page
refresh, another device). That is the rendering half of ADR-0019's
durable-suspend promise, missing while the resuming half shipped.
BREAKING: `IAutomationService.getSuspendedScreen(runId)` now returns
`Promise<ScreenSpec | null>`. No sync variant remains on the contract; every
consumer is migrated in this change (the runtime automation domain route, the
contract-checked http-dispatcher mock, three engine test call sites).
The engine keeps the hot cache as its fast path and falls through to the store
via the same `loadSuspendedRun` that `resume` rehydrates from — one loader, two
callers, no duplicated rehydration logic. A run that does not exist, is no
longer suspended, or paused at a non-screen node still resolves to `null`, so
the route keeps 404-ing for genuinely absent runs. A store outage reads as
`null` (this backs a 404); `hasSuspendedRun` remains the strict variant that
throws for callers who must tell "gone" from "unknown".
Tests: `suspended-screen-durability.test.ts` pins the hot path, the cold-boot
cache-miss (the bug), the absent-run and no-screen null cases, the store-outage
degradation and the no-store behaviour. `flow-durable-suspend.dogfood.test.ts`
adds the end-to-end assertion over a real `stop()` → cold `bootStack`: the
second kernel re-fetches the persisted screen with its field contract intact,
without consuming the pause. Both fail with the fallback removed.
Co-authored-by: Claude <support@objectstack.ai>
**BREAKING**: `IAutomationService.getSuspendedScreen(runId)` is now **async** — it returns `Promise<ScreenSpec | null>` instead of `ScreenSpec | null` (#4515).
One-line fix: `await` the call (the enclosing function is almost certainly already `async`), and make any test double resolve rather than return (`mockResolvedValue`, not `mockReturnValue`).
22
+
23
+
Why it had to change: the method could only ever read the engine's in-memory hot cache, because a synchronous signature cannot consult the durable suspended-run store. `SuspendedRun.screen`*is* persisted (`sys_automation_run.screen_json`) and `resume()` cold-reads it back, so after a process restart a still-suspended screen run could be resumed (`POST …/runs/:runId/resume` → 200) while `GET …/runs/:runId/screen` returned 404 “No pending screen for run” — the refresh-safe re-fetch failing in exactly the situation it exists for (page refresh, another device), and the rendering half of ADR-0019's durable-suspend promise missing while the resuming half shipped.
24
+
25
+
`AutomationEngine.getSuspendedScreen` now takes the hot cache as its fast path and falls through to the store via the same loader `resume()` rehydrates from. A run that does not exist, is no longer suspended, or paused at a non-screen node still resolves to `null`, so `GET …/runs/:runId/screen` keeps returning 404 for genuinely absent runs. No sync variant of the method remains on the contract.
-**resume**: apply `signal.variables` as **bare** variables (`variables.set(name, value)`) in addition to the existing `signal.output` (`${nodeId}.key`). If the continuation suspends at another screen, return that screen (multi-screen wizards).
29
-
-`getSuspendedScreen(runId)` getter so HTTP can re-fetch the current screen.
29
+
-`async getSuspendedScreen(runId)` getter so HTTP can re-fetch the current screen. Durable (#4515): hot cache first, then the `SuspendedRunStore` via the same loader `resume` rehydrates from, so a screen run that survived a restart renders as well as it resumes.
-**Launch**: existing `POST /api/v1/automation/:name/trigger` — when the run pauses at a screen, the response includes `{ status:'paused', runId, screen }`.
0 commit comments