Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/src/components/flows/FlowRunInspectorDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ export const FLOW_RUN_STATUS_ACCENT: Record<FlowRunStatus, string> = {
'border-coral-200 bg-coral-50 text-coral-700 dark:border-coral-500/30 dark:bg-coral-500/10 dark:text-coral-300',
// Neutral treatment, matching `WorkflowRunDetail.tsx`'s `RUN_STATUS_ACCENT.cancelled`.
cancelled: 'border-line bg-surface-muted text-content-secondary',
// Interrupted (bug B42): a run reconciled after its future was dropped
// mid-flight. Amber-leaning "worth a look" like `pending_approval`, but
// settled — it carries an `error` reason banner.
interrupted:
'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-300',
};

/** Header status dot per run status — mirrors `PHASE_STATUS_DOT`. Exported, see above. */
Expand All @@ -82,6 +87,8 @@ export const FLOW_RUN_STATUS_DOT: Record<FlowRunStatus, string> = {
pending_approval: 'bg-amber-500 animate-pulse',
failed: 'bg-coral-500',
cancelled: 'bg-surface-strong',
// Settled (no pulse) — reconciled after being dropped mid-flight (bug B42).
interrupted: 'bg-amber-500',
};

/** i18n key per run status. Exported, see above. */
Expand All @@ -92,6 +99,7 @@ export const FLOW_RUN_STATUS_KEY: Record<FlowRunStatus, string> = {
pending_approval: 'flowRuns.status.pending_approval',
failed: 'flowRuns.status.failed',
cancelled: 'flowRuns.status.cancelled',
interrupted: 'flowRuns.status.interrupted',
};

function formatTimestamp(value: string | null | undefined): string | null {
Expand Down
24 changes: 24 additions & 0 deletions app/src/hooks/__tests__/useFlowRunPoller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,30 @@ describe('useFlowRunPoller', () => {
expect(getFlowRun).toHaveBeenCalledTimes(1);
});

it('stops polling once the run is interrupted', async () => {
// Bug B42: an `interrupted` run (reconciled after being dropped mid-flight)
// is terminal — the poller must not loop forever on it.
getFlowRun.mockResolvedValue(
makeRun({
status: 'interrupted',
error: 'Run interrupted before completion',
finished_at: '2026-01-01T00:01:00Z',
})
);
const { result } = renderHook(() => useFlowRunPoller('thread-1'));

await act(async () => {
await vi.advanceTimersByTimeAsync(0);
});
expect(result.current.run?.status).toBe('interrupted');
expect(getFlowRun).toHaveBeenCalledTimes(1);

await act(async () => {
await vi.advanceTimersByTimeAsync(10_000);
});
expect(getFlowRun).toHaveBeenCalledTimes(1);
});

it('stops polling once the run fails', async () => {
getFlowRun.mockResolvedValue(makeRun({ status: 'failed', error: 'boom' }));
const { result } = renderHook(() => useFlowRunPoller('thread-1'));
Expand Down
9 changes: 7 additions & 2 deletions app/src/hooks/useFlowRunPoller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@
*
* `pending_approval` is explicitly NOT terminal — a paused run still needs
* live status so the drawer reflects an approval elsewhere resolving it.
* `completed_with_warnings` (run honesty, PR2) and `cancelled` are terminal,
* same as `completed`/`failed`.
* `completed_with_warnings` (run honesty, PR2), `cancelled`, and `interrupted`
* (bug B42 — reconciled after being dropped mid-flight) are terminal, same as
* `completed`/`failed`.
*/
import debug from 'debug';
import { useEffect, useRef, useState } from 'react';
Expand All @@ -51,6 +52,10 @@ const TERMINAL = new Set<FlowRunStatus>([
'completed_with_warnings',
'failed',
'cancelled',
// Reconciled after its future was dropped mid-flight (bug B42) — a settled
// terminal state. Without this the poller would loop forever on a run that
// never leaves `interrupted`.
'interrupted',
]);

function isTerminal(run: FlowRun | null): boolean {
Expand Down
3 changes: 3 additions & 0 deletions app/src/hooks/useFlowRunsLiveRefresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ const TERMINAL_STATUSES = new Set<FlowRunStatus>([
'completed_with_warnings',
'failed',
'cancelled',
// Reconciled after its future was dropped mid-flight (bug B42) — settled, so
// the list's active-run backstop poll can quiesce once every run is terminal.
'interrupted',
]);

/** Trailing debounce window for a burst of `flow:run_progress` events. */
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4041,6 +4041,7 @@ const messages: TranslationMap = {
'flowRuns.status.pending_approval': 'بانتظار الموافقة',
'flowRuns.status.failed': 'فشل',
'flowRuns.status.cancelled': 'ملغى',
'flowRuns.status.interrupted': 'تمت المقاطعة',

'flows.page.title': 'سير العمل',
'flows.page.description': 'أتمتة محفوظة يمكنك تفعيلها وتشغيلها ومتابعتها.',
Expand All @@ -4066,6 +4067,7 @@ const messages: TranslationMap = {
'flows.allRuns.status.pending_approval': 'بانتظار الموافقة',
'flows.allRuns.status.failed': 'فشل',
'flows.allRuns.status.cancelled': 'أُلغي',
'flows.allRuns.status.interrupted': 'تمت المقاطعة',
'flows.list.minutesAgo': 'منذ {count} دقيقة',
'flows.list.hoursAgo': 'منذ {count} ساعة',
'flows.list.daysAgo': 'منذ {count} يوم',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/bn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4136,6 +4136,7 @@ const messages: TranslationMap = {
'flowRuns.status.pending_approval': 'অনুমোদনের অপেক্ষায়',
'flowRuns.status.failed': 'ব্যর্থ',
'flowRuns.status.cancelled': 'বাতিল করা হয়েছে',
'flowRuns.status.interrupted': 'বিঘ্নিত',

'flows.page.title': 'ওয়ার্কফ্লো',
'flows.page.description': 'সংরক্ষিত অটোমেশন যা আপনি সক্ষম, চালাতে এবং পর্যবেক্ষণ করতে পারেন।',
Expand All @@ -4162,6 +4163,7 @@ const messages: TranslationMap = {
'flows.allRuns.status.pending_approval': 'অনুমোদনের অপেক্ষায়',
'flows.allRuns.status.failed': 'ব্যর্থ',
'flows.allRuns.status.cancelled': 'বাতিল',
'flows.allRuns.status.interrupted': 'বিঘ্নিত',
'flows.list.minutesAgo': '{count} মিনিট আগে',
'flows.list.hoursAgo': '{count} ঘণ্টা আগে',
'flows.list.daysAgo': '{count} দিন আগে',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4251,6 +4251,7 @@ const messages: TranslationMap = {
'flowRuns.status.pending_approval': 'Wartet auf Genehmigung',
'flowRuns.status.failed': 'Fehlgeschlagen',
'flowRuns.status.cancelled': 'Abgebrochen',
'flowRuns.status.interrupted': 'Unterbrochen',

'flows.page.title': 'Workflows',
'flows.page.description':
Expand Down Expand Up @@ -4278,6 +4279,7 @@ const messages: TranslationMap = {
'flows.allRuns.status.pending_approval': 'Warten auf Freigabe',
'flows.allRuns.status.failed': 'Fehlgeschlagen',
'flows.allRuns.status.cancelled': 'Abgebrochen',
'flows.allRuns.status.interrupted': 'Unterbrochen',
'flows.list.minutesAgo': 'vor {count} Min.',
'flows.list.hoursAgo': 'vor {count} Std.',
'flows.list.daysAgo': 'vor {count} Tagen',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4799,6 +4799,7 @@ const en: TranslationMap = {
'flowRuns.status.pending_approval': 'Awaiting approval',
'flowRuns.status.failed': 'Failed',
'flowRuns.status.cancelled': 'Cancelled',
'flowRuns.status.interrupted': 'Interrupted',

// ── Workflows list page + nav tab (B5a): the `flows::` domain's
// discoverable hub at /flows. Distinct from the legacy SKILL.md
Expand Down Expand Up @@ -4845,6 +4846,7 @@ const en: TranslationMap = {
'flows.allRuns.status.pending_approval': 'Pending approval',
'flows.allRuns.status.failed': 'Failed',
'flows.allRuns.status.cancelled': 'Cancelled',
'flows.allRuns.status.interrupted': 'Interrupted',
'flows.list.minutesAgo': '{count}m ago',
'flows.list.hoursAgo': '{count}h ago',
'flows.list.daysAgo': '{count}d ago',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4208,6 +4208,7 @@ const messages: TranslationMap = {
'flowRuns.status.pending_approval': 'Esperando aprobación',
'flowRuns.status.failed': 'Fallido',
'flowRuns.status.cancelled': 'Cancelado',
'flowRuns.status.interrupted': 'Interrumpido',

'flows.page.title': 'Flujos de trabajo',
'flows.page.description':
Expand All @@ -4234,6 +4235,7 @@ const messages: TranslationMap = {
'flows.allRuns.status.pending_approval': 'Pendiente de aprobación',
'flows.allRuns.status.failed': 'Fallido',
'flows.allRuns.status.cancelled': 'Cancelado',
'flows.allRuns.status.interrupted': 'Interrumpido',
'flows.list.minutesAgo': 'hace {count} min',
'flows.list.hoursAgo': 'hace {count} h',
'flows.list.daysAgo': 'hace {count} d',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4233,6 +4233,7 @@ const messages: TranslationMap = {
'flowRuns.status.pending_approval': "En attente d'approbation",
'flowRuns.status.failed': 'Échoué',
'flowRuns.status.cancelled': 'Annulé',
'flowRuns.status.interrupted': 'Interrompu',

'flows.page.title': 'Workflows',
'flows.page.description':
Expand Down Expand Up @@ -4261,6 +4262,7 @@ const messages: TranslationMap = {
'flows.allRuns.status.pending_approval': 'En attente d’approbation',
'flows.allRuns.status.failed': 'Échoué',
'flows.allRuns.status.cancelled': 'Annulé',
'flows.allRuns.status.interrupted': 'Interrompu',
'flows.list.minutesAgo': 'il y a {count} min',
'flows.list.hoursAgo': 'il y a {count} h',
'flows.list.daysAgo': 'il y a {count} j',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/hi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4135,6 +4135,7 @@ const messages: TranslationMap = {
'flowRuns.status.pending_approval': 'अनुमोदन की प्रतीक्षा में',
'flowRuns.status.failed': 'विफल',
'flowRuns.status.cancelled': 'रद्द किया गया',
'flowRuns.status.interrupted': 'बाधित',

'flows.page.title': 'वर्कफ़्लो',
'flows.page.description': 'सहेजे गए ऑटोमेशन जिन्हें आप सक्षम, चला और मॉनिटर कर सकते हैं।',
Expand All @@ -4160,6 +4161,7 @@ const messages: TranslationMap = {
'flows.allRuns.status.pending_approval': 'अनुमोदन लंबित',
'flows.allRuns.status.failed': 'विफल',
'flows.allRuns.status.cancelled': 'रद्द',
'flows.allRuns.status.interrupted': 'बाधित',
'flows.list.minutesAgo': '{count} मिनट पहले',
'flows.list.hoursAgo': '{count} घंटे पहले',
'flows.list.daysAgo': '{count} दिन पहले',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4151,6 +4151,7 @@ const messages: TranslationMap = {
'flowRuns.status.pending_approval': 'Menunggu persetujuan',
'flowRuns.status.failed': 'Gagal',
'flowRuns.status.cancelled': 'Dibatalkan',
'flowRuns.status.interrupted': 'Terhenti',

'flows.page.title': 'Alur Kerja',
'flows.page.description': 'Otomatisasi tersimpan yang dapat Anda aktifkan, jalankan, dan pantau.',
Expand All @@ -4177,6 +4178,7 @@ const messages: TranslationMap = {
'flows.allRuns.status.pending_approval': 'Menunggu persetujuan',
'flows.allRuns.status.failed': 'Gagal',
'flows.allRuns.status.cancelled': 'Dibatalkan',
'flows.allRuns.status.interrupted': 'Terhenti',
'flows.list.minutesAgo': '{count} menit lalu',
'flows.list.hoursAgo': '{count} jam lalu',
'flows.list.daysAgo': '{count} hari lalu',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4206,6 +4206,7 @@ const messages: TranslationMap = {
'flowRuns.status.pending_approval': 'In attesa di approvazione',
'flowRuns.status.failed': 'Non riuscito',
'flowRuns.status.cancelled': 'Annullato',
'flowRuns.status.interrupted': 'Interrotto',

'flows.page.title': 'Flussi di lavoro',
'flows.page.description': 'Automazioni salvate che puoi abilitare, eseguire e monitorare.',
Expand All @@ -4231,6 +4232,7 @@ const messages: TranslationMap = {
'flows.allRuns.status.pending_approval': 'In attesa di approvazione',
'flows.allRuns.status.failed': 'Non riuscito',
'flows.allRuns.status.cancelled': 'Annullato',
'flows.allRuns.status.interrupted': 'Interrotto',
'flows.list.minutesAgo': '{count} min fa',
'flows.list.hoursAgo': '{count} h fa',
'flows.list.daysAgo': '{count} g fa',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4094,6 +4094,7 @@ const messages: TranslationMap = {
'flowRuns.status.pending_approval': '승인 대기 중',
'flowRuns.status.failed': '실패',
'flowRuns.status.cancelled': '취소됨',
'flowRuns.status.interrupted': '중단됨',

'flows.page.title': '워크플로',
'flows.page.description': '활성화, 실행, 모니터링할 수 있는 저장된 자동화입니다.',
Expand All @@ -4118,6 +4119,7 @@ const messages: TranslationMap = {
'flows.allRuns.status.pending_approval': '승인 대기 중',
'flows.allRuns.status.failed': '실패',
'flows.allRuns.status.cancelled': '취소됨',
'flows.allRuns.status.interrupted': '중단됨',
'flows.list.minutesAgo': '{count}분 전',
'flows.list.hoursAgo': '{count}시간 전',
'flows.list.daysAgo': '{count}일 전',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4190,6 +4190,7 @@ const messages: TranslationMap = {
'flowRuns.status.pending_approval': 'Oczekuje na zatwierdzenie',
'flowRuns.status.failed': 'Niepowodzenie',
'flowRuns.status.cancelled': 'Anulowano',
'flowRuns.status.interrupted': 'Przerwano',

'flows.page.title': 'Przepływy pracy',
'flows.page.description':
Expand Down Expand Up @@ -4217,6 +4218,7 @@ const messages: TranslationMap = {
'flows.allRuns.status.pending_approval': 'Oczekuje na zatwierdzenie',
'flows.allRuns.status.failed': 'Niepowodzenie',
'flows.allRuns.status.cancelled': 'Anulowano',
'flows.allRuns.status.interrupted': 'Przerwano',
'flows.list.minutesAgo': '{count} min temu',
'flows.list.hoursAgo': '{count} godz. temu',
'flows.list.daysAgo': '{count} dni temu',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4198,6 +4198,7 @@ const messages: TranslationMap = {
'flowRuns.status.pending_approval': 'Aguardando aprovação',
'flowRuns.status.failed': 'Falhou',
'flowRuns.status.cancelled': 'Cancelado',
'flowRuns.status.interrupted': 'Interrompido',

'flows.page.title': 'Fluxos de trabalho',
'flows.page.description': 'Automações salvas que você pode habilitar, executar e monitorar.',
Expand All @@ -4224,6 +4225,7 @@ const messages: TranslationMap = {
'flows.allRuns.status.pending_approval': 'Aguardando aprovação',
'flows.allRuns.status.failed': 'Falhou',
'flows.allRuns.status.cancelled': 'Cancelado',
'flows.allRuns.status.interrupted': 'Interrompido',
'flows.list.minutesAgo': 'há {count} min',
'flows.list.hoursAgo': 'há {count} h',
'flows.list.daysAgo': 'há {count} d',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4171,6 +4171,7 @@ const messages: TranslationMap = {
'flowRuns.status.pending_approval': 'Ожидает подтверждения',
'flowRuns.status.failed': 'Не удалось',
'flowRuns.status.cancelled': 'Отменено',
'flowRuns.status.interrupted': 'Прервано',

'flows.page.title': 'Рабочие процессы',
'flows.page.description':
Expand Down Expand Up @@ -4199,6 +4200,7 @@ const messages: TranslationMap = {
'flows.allRuns.status.pending_approval': 'Ожидает одобрения',
'flows.allRuns.status.failed': 'Ошибка',
'flows.allRuns.status.cancelled': 'Отменено',
'flows.allRuns.status.interrupted': 'Прервано',
'flows.list.minutesAgo': '{count} мин назад',
'flows.list.hoursAgo': '{count} ч назад',
'flows.list.daysAgo': '{count} дн назад',
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3920,6 +3920,7 @@ const messages: TranslationMap = {
'flowRuns.status.pending_approval': '等待批准',
'flowRuns.status.failed': '失败',
'flowRuns.status.cancelled': '已取消',
'flowRuns.status.interrupted': '已中断',

'flows.page.title': '工作流',
'flows.page.description': '已保存的自动化流程,可启用、运行并监控。',
Expand All @@ -3944,6 +3945,7 @@ const messages: TranslationMap = {
'flows.allRuns.status.pending_approval': '等待批准',
'flows.allRuns.status.failed': '失败',
'flows.allRuns.status.cancelled': '已取消',
'flows.allRuns.status.interrupted': '已中断',
'flows.list.minutesAgo': '{count}分钟前',
'flows.list.hoursAgo': '{count}小时前',
'flows.list.daysAgo': '{count}天前',
Expand Down
1 change: 1 addition & 0 deletions app/src/pages/WorkflowRunsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const STATUS_CLASS: Record<FlowRunStatus, string> = {
pending_approval: 'bg-amber-500/15 text-amber-700 dark:text-amber-300',
failed: 'bg-coral-500/15 text-coral-700 dark:text-coral-300',
cancelled: 'bg-content-faint/15 text-content-secondary',
interrupted: 'bg-amber-500/15 text-amber-700 dark:text-amber-300',
};

export default function WorkflowRunsPage() {
Expand Down
19 changes: 18 additions & 1 deletion app/src/services/api/flowsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ export type FlowRunStatus =
| 'completed_with_warnings'
| 'pending_approval'
| 'failed'
| 'cancelled';
| 'cancelled'
// A run whose future was dropped mid-flight (harness tool abort, chat turn
// end, timeout, or an app restart), reconciled to a terminal state by the
// core's `RunRowFinalizer` drop-guard or its boot-time orphan sweep (bug
// B42). Carries a human `error` reason; rendered as a settled, non-active run.
| 'interrupted';
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/** One reconstructed step of a persisted `FlowRun` (`src/openhuman/flows/types.rs::FlowRunStep`). */
export interface FlowRunStep {
Expand Down Expand Up @@ -87,10 +92,22 @@ export interface FlowRun {
thread_id: string;
status: FlowRunStatus;
started_at: string;
/**
* RFC3339 timestamp stamped when the run settled — set for every terminal
* status, including `'interrupted'` (the drop-guard / boot sweep stamps it
* exactly like a normal terminal write). `null`/absent only while the run is
* still `'running'`.
*/
finished_at?: string | null;
steps: FlowRunStep[];
/** Node ids paused awaiting approval when `status === 'pending_approval'`. */
pending_approvals: string[];
/**
* Human-readable failure reason. Set for `'failed'` runs and for
* `'interrupted'` ones (where it carries the reconciliation reason — tool
* abort / turn end / app restart), so the UI can surface *why* a run stopped
* rather than showing a bare terminal state.
*/
error?: string | null;
}

Expand Down
6 changes: 6 additions & 0 deletions src/core/runtime/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,12 @@ impl CoreRuntime {
if self.services.cron {
services::spawn_cron_service();
}
// Flow-run boot reconciliation is selected by the flows *domain*, not by
// a background service — runs can be started without cron in the
// ServiceSet, so their orphans must be reconcilable without it too.
if self.ctx.domains().flows {
services::spawn_flows_boot_reconcile();
}
if self.services.channels {
services::spawn_channels_service();
}
Expand Down
Loading
Loading