Skip to content

Commit 8bd4694

Browse files
fix(tables): unstick cells + resync counter on usage-limit halt
Addresses PR review: - Clear each blocked cell's pre-stamp on a 402 so it reverts to un-run instead of being stuck "Queued" (no error/cancelled badge); covers auto-fire cells with no owning dispatch. - Client re-syncs run-state counts and refetches rows on usageLimitReached so the stale "X running" / Stop-all control clears and queued cells drop. - Make usageLimitReached.dispatchId optional; client only touches the dispatch overlay when present.
1 parent 877600d commit 8bd4694

3 files changed

Lines changed: 40 additions & 14 deletions

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table-event-stream.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ interface UseTableEventStreamArgs {
4646
enabled?: boolean
4747
/** Fired when the server halts a dispatch because the billed account is over
4848
* its usage limit. The page surfaces an upgrade prompt + redirect. */
49-
onUsageLimitReached?: (event: { dispatchId: string; message: string }) => void
49+
onUsageLimitReached?: (event: { dispatchId?: string; message: string }) => void
5050
}
5151

5252
/**
@@ -215,12 +215,23 @@ export function useTableEventStream({
215215

216216
const applyUsageLimit = (event: Extract<TableEvent, { kind: 'usageLimitReached' }>): void => {
217217
// Drop the halted dispatch from the overlay so the "running" UI clears
218-
// immediately (the dispatcher was marked complete server-side).
219-
queryClient.setQueryData<TableRunState>(tableKeys.activeDispatches(tableId), (prev) => {
220-
if (!prev) return prev
221-
const filtered = prev.dispatches.filter((d) => d.id !== event.dispatchId)
222-
return filtered.length === prev.dispatches.length ? prev : { ...prev, dispatches: filtered }
223-
})
218+
// immediately (the dispatcher was marked complete server-side). Cascade /
219+
// auto-fire events carry no dispatchId — nothing to remove.
220+
if (event.dispatchId) {
221+
queryClient.setQueryData<TableRunState>(tableKeys.activeDispatches(tableId), (prev) => {
222+
if (!prev) return prev
223+
const filtered = prev.dispatches.filter((d) => d.id !== event.dispatchId)
224+
return filtered.length === prev.dispatches.length
225+
? prev
226+
: { ...prev, dispatches: filtered }
227+
})
228+
}
229+
// Blocked cells are left `queued` in the DB with no terminal cell event,
230+
// so `runningByRowId` would otherwise stay non-zero (stale "X running").
231+
// Re-sync the server counts, and refetch rows so cells whose pre-stamps
232+
// the server cleared drop their "Queued" state.
233+
scheduleDispatchInvalidate()
234+
void queryClient.invalidateQueries({ queryKey: tableKeys.rowsRoot(tableId) })
224235
onUsageLimitReachedRef.current?.({ dispatchId: event.dispatchId, message: event.message })
225236
}
226237

apps/sim/background/workflow-column-execution.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -429,10 +429,24 @@ async function runWorkflowAndWriteTerminal(
429429
logger.warn(
430430
`Usage limit reached — halting dispatch (table=${tableId} row=${rowId} group=${groupId})`
431431
)
432+
// Don't leave the cell stuck on its `pending` pre-stamp. Clear this
433+
// cell's exec so it reverts to un-run (no error/cancelled badge —
434+
// matching "don't mark"; re-runnable after upgrade). Each blocked
435+
// cell clears its own.
436+
const { updateRow } = await import('@/lib/table/service')
437+
await updateRow(
438+
{ tableId, rowId, data: {}, workspaceId, executionsPatch: { [groupId]: null } },
439+
table,
440+
requestId
441+
).catch((err) =>
442+
logger.warn(`Failed to clear cell pre-stamp on usage limit`, {
443+
error: toError(err).message,
444+
})
445+
)
432446
// With up to 20 concurrent cells all hitting the limit at once, only
433-
// the cell that actually transitions the dispatch active→complete
434-
// emits the event — otherwise the user sees a toast per in-flight
435-
// cell. Cells with no owning dispatch (auto-fire) always emit.
447+
// the cell that transitions the dispatch active→complete emits the
448+
// event — otherwise the user sees a toast per in-flight cell. Cells
449+
// with no owning dispatch (auto-fire) always emit.
436450
let shouldEmit = true
437451
if (dispatchId) {
438452
const { completeDispatchIfActive } = await import('@/lib/table/dispatcher')
@@ -442,7 +456,7 @@ async function runWorkflowAndWriteTerminal(
442456
await appendTableEvent({
443457
kind: 'usageLimitReached',
444458
tableId,
445-
dispatchId: dispatchId ?? '',
459+
...(dispatchId ? { dispatchId } : {}),
446460
message:
447461
preprocess.error?.message ??
448462
'Usage limit exceeded. Please upgrade your plan to continue.',

apps/sim/lib/table/events.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,12 @@ export type TableEvent =
116116
| {
117117
/** A dispatch was stopped because the billed account is over its usage
118118
* limit. The client surfaces an upgrade prompt and redirects to billing.
119-
* Cells are intentionally left untouched (no error/cancelled marking);
120-
* the dispatch is halted via `markDispatchComplete`. */
119+
* The dispatch is halted via `markDispatchComplete` and the blocked
120+
* cells' pre-stamps are cleared so they revert to un-run. `dispatchId`
121+
* is absent for cascade/auto-fire payloads with no owning dispatch. */
121122
kind: 'usageLimitReached'
122123
tableId: string
123-
dispatchId: string
124+
dispatchId?: string
124125
message: string
125126
}
126127

0 commit comments

Comments
 (0)