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
24 changes: 20 additions & 4 deletions apps/docs/openapi-v2-logs.json
Original file line number Diff line number Diff line change
Expand Up @@ -640,8 +640,16 @@
},
"status": {
"type": "string",
"enum": ["pending", "running", "redacting", "completed", "failed", "cancelled"],
"description": "Current execution status. `redacting` is transient while run output is scrubbed."
"enum": [
"pending",
"running",
"paused",
"redacting",
"completed",
"failed",
"cancelled"
],
"description": "Current execution status. `redacting` is transient while run output is scrubbed. `paused` is reported when a resume attempt did not run to completion and the run is waiting to be resumed again."
},
"level": {
"type": "string",
Expand Down Expand Up @@ -1028,8 +1036,16 @@
},
"status": {
"type": "string",
"enum": ["pending", "running", "redacting", "completed", "failed", "cancelled"],
"description": "Current execution status. `redacting` is transient while run output is scrubbed."
"enum": [
"pending",
"running",
"paused",
"redacting",
"completed",
"failed",
"cancelled"
],
"description": "Current execution status. `redacting` is transient while run output is scrubbed. `paused` is reported when a resume attempt did not run to completion and the run is waiting to be resumed again."
},
"level": {
"type": "string",
Expand Down
10 changes: 5 additions & 5 deletions apps/docs/openapi-v2-workflows.json
Original file line number Diff line number Diff line change
Expand Up @@ -3923,13 +3923,13 @@
"enum": [
"pending",
"running",
"paused",
"redacting",
"completed",
"failed",
"cancelled",
"paused"
"cancelled"
],
"description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed."
"description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` means the run is not executing and is waiting to be resumed: either held at a human-in-the-loop pause point, or left paused because a resume attempt did not run to completion. The status alone does not say which. On the single-run response `paused.automaticResumeWaitingReason` distinguishes them: it is recorded whenever a resume attempt fails and cleared once a resume succeeds, so a null value means the run is waiting on human input. When the failure is not retryable or the automatic retries are exhausted, the reason is prefixed `Automatic resume requires manual intervention: `. Run-list items carry no `paused` object, so the two cases are indistinguishable there."
},
"trigger": {
"type": "string",
Expand Down Expand Up @@ -4063,14 +4063,14 @@
"enum": [
"pending",
"running",
"paused",
"redacting",
"completed",
"failed",
"cancelled",
"paused",
"queued"
],
"description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed."
"description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` means the run is not executing and is waiting to be resumed: either held at a human-in-the-loop pause point, or left paused because a resume attempt did not run to completion. The status alone does not say which. On the single-run response `paused.automaticResumeWaitingReason` distinguishes them: it is recorded whenever a resume attempt fails and cleared once a resume succeeds, so a null value means the run is waiting on human input. When the failure is not retryable or the automatic retries are exhausted, the reason is prefixed `Automatic resume requires manual intervention: `. Run-list items carry no `paused` object, so the two cases are indistinguishable there."
},
"trigger": {
"anyOf": [
Expand Down
15 changes: 15 additions & 0 deletions apps/sim/app/api/v2/logs/[runId]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ describe('GET /api/v2/logs/[runId]', () => {
})
})

it('serves a run whose persisted status is paused', async () => {
mocks.execute.mockResolvedValue({
log: { ...log, status: 'paused' },
workflowFolderPath: '/agents',
executionData: { traceSpans: [], finalOutput: null },
})

const response = await GET(new NextRequest('http://localhost:3000/api/v2/logs/run-1'), {
params: Promise.resolve({ runId: 'run-1' }),
})

expect(response.status).toBe(200)
expect((await response.json()).data).toMatchObject({ runId: 'run-1', status: 'paused' })
})

it('conceals canonical workspace authorization as log not-found', async () => {
mocks.execute.mockRejectedValueOnce(new NoWorkspaceAccessError())

Expand Down
18 changes: 18 additions & 0 deletions apps/sim/app/api/v2/logs/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,24 @@ describe('GET /api/v2/logs', () => {
})
})

it('serves a run whose persisted status is paused', async () => {
mocks.execute.mockResolvedValue({
items: [{ log: { ...log, status: 'paused' }, executionData: null }],
nextCursor: null,
includeFullDetails: false,
includeFinalOutput: false,
includeTraceSpans: false,
})

const response = await GET(
new NextRequest(`http://localhost:3000/api/v2/logs?workspaceId=${WORKSPACE_ID}`)
)
const body = await response.json()

expect(response.status).toBe(200)
expect(body.data[0]).toMatchObject({ runId: 'run-1', status: 'paused' })
})

it('rejects malformed cursors after admission and before protected reads', async () => {
const response = await GET(
new NextRequest(
Expand Down
26 changes: 26 additions & 0 deletions apps/sim/lib/api/contracts/v2/log-status.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest'
import { v2LogStatusSchema } from '@/lib/api/contracts/v2/logs'
import { PERSISTED_WORKFLOW_EXECUTION_STATUSES } from '@/lib/logs/types'

/**
* Both log endpoints pass `workflow_execution_logs.status` through verbatim — unlike the
* run endpoints there is no `paused` overlay and no `queued` — so any drift between the
* reported enum and the persisted list 500s a whole page of results.
*/
describe('v2 log status schema', () => {
it('publishes exactly the persisted statuses', () => {
expect(v2LogStatusSchema.options).toEqual([
'pending',
'running',
'paused',
'redacting',
'completed',
'failed',
'cancelled',
])
})

it('stays derived from the persisted status list', () => {
expect(v2LogStatusSchema.options).toEqual([...PERSISTED_WORKFLOW_EXECUTION_STATUSES])
})
})
31 changes: 9 additions & 22 deletions apps/sim/lib/api/contracts/v2/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
v2FolderPathSchema,
v2TimestampSchema,
} from '@/lib/api/contracts/v2/shared'
import type { PersistedWorkflowExecutionStatus } from '@/lib/logs/types'
import { PERSISTED_WORKFLOW_EXECUTION_STATUSES } from '@/lib/logs/types'

/**
* v2 logs contracts. The query schemas are reused verbatim from v1 (the request
Expand All @@ -23,29 +23,16 @@ const v2LogCostSchema = z
.nullable()
.describe('Cost charged for the run, or null when unavailable.')
/**
* Every status the execution logger can persist, including the transient
* `redacting` state written while a finished run's output is scrubbed. The
* column is free text, so a value missing here fails the response parse and
* turns a single row into a 500 for the whole page. `_ExhaustiveLogStatus`
* makes a future addition to the persisted union a compile error instead.
* Both log endpoints pass `workflow_execution_logs.status` through verbatim, so the
* reported set is exactly the persisted set — a value missing here fails the response
* parse, and because list validation is whole-page one such row turns an entire page
* into a 500.
*/
const V2_LOG_STATUSES = [
'pending',
'running',
'redacting',
'completed',
'failed',
'cancelled',
] as const satisfies readonly PersistedWorkflowExecutionStatus[]

type AssertNever<T extends never> = T
type _ExhaustiveLogStatus = AssertNever<
Exclude<PersistedWorkflowExecutionStatus, (typeof V2_LOG_STATUSES)[number]>
>

export const v2LogStatusSchema = z
.enum(V2_LOG_STATUSES)
.describe('Current execution status. `redacting` is transient while run output is scrubbed.')
.enum(PERSISTED_WORKFLOW_EXECUTION_STATUSES)
.describe(
'Current execution status. `redacting` is transient while run output is scrubbed. `paused` is reported when a resume attempt did not run to completion and the run is waiting to be resumed again.'
)

/** Execution `files` is a per-run jsonb array of attachment metadata. */
const v2LogFilesSchema = z
Expand Down
55 changes: 34 additions & 21 deletions apps/sim/lib/api/contracts/v2/workflow-run-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,49 @@ import {
v2WorkflowRunStatusFilterSchema,
v2WorkflowRunStatusValueSchema,
} from '@/lib/api/contracts/v2/workflows'
import type { PersistedWorkflowExecutionStatus } from '@/lib/logs/types'
import { PERSISTED_WORKFLOW_EXECUTION_STATUSES } from '@/lib/logs/types'

/**
* The runtime mirror of the persisted union. `satisfies` keeps it honest against
* `PersistedWorkflowExecutionStatus`, and the `AssertNever` gate in the contract keeps
* that union honest against the reported enums, so a status added to the execution logger
* fails compilation in both places before it can 500 a response parse.
* Both run endpoints report `workflow_execution_logs.status`, overlaid with `paused` from
* `paused_executions`, so every reported value lands in the persisted set. These tests
* guard the two ways that can break: the derivation being replaced by a hand-maintained
* list again, and a status being added to the persisted set without anyone confirming it
* belongs on the public wire (and regenerating the OpenAPI specs).
*/
const PERSISTED_STATUSES = [
'pending',
'running',
'redacting',
'completed',
'failed',
'cancelled',
] as const satisfies readonly PersistedWorkflowExecutionStatus[]

describe('v2 workflow run status schemas', () => {
it.each(PERSISTED_STATUSES)('reports the persisted status %s on both run endpoints', (status) => {
expect(v2WorkflowRunListStatusValueSchema.parse(status)).toBe(status)
expect(v2WorkflowRunStatusValueSchema.parse(status)).toBe(status)
it('publishes exactly the persisted statuses on the run list', () => {
expect(v2WorkflowRunListStatusValueSchema.options).toEqual([
'pending',
'running',
'paused',
'redacting',
'completed',
'failed',
'cancelled',
])
})

it('reports the paused overlay on both run endpoints', () => {
expect(v2WorkflowRunListStatusValueSchema.parse('paused')).toBe('paused')
expect(v2WorkflowRunStatusValueSchema.parse('paused')).toBe('paused')
it('stays derived from the persisted status list', () => {
expect(v2WorkflowRunListStatusValueSchema.options).toEqual([
...PERSISTED_WORKFLOW_EXECUTION_STATUSES,
])
expect(v2WorkflowRunStatusValueSchema.options).toEqual([
...PERSISTED_WORKFLOW_EXECUTION_STATUSES,
'queued',
])
})

it('reports queued only where the job queue is consulted', () => {
expect(v2WorkflowRunStatusValueSchema.parse('queued')).toBe('queued')
expect(v2WorkflowRunStatusValueSchema.options).toEqual([
'pending',
'running',
'paused',
'redacting',
'completed',
'failed',
'cancelled',
'queued',
])
expect(v2WorkflowRunListStatusValueSchema.safeParse('queued').success).toBe(false)
})

Expand Down
44 changes: 12 additions & 32 deletions apps/sim/lib/api/contracts/v2/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
workflowIdParamsSchema,
} from '@/lib/api/contracts/workflows'
import { MAX_WORKFLOW_EXECUTION_TIMEOUT_SECONDS } from '@/lib/billing/execution-timeout-defaults'
import type { PersistedWorkflowExecutionStatus } from '@/lib/logs/types'
import { PERSISTED_WORKFLOW_EXECUTION_STATUSES } from '@/lib/logs/types'

export const V2_WORKFLOW_RUN_ID_HEADER = 'X-Run-Id'

Expand Down Expand Up @@ -982,48 +982,28 @@ export const v2ResumeWorkflowContract = defineRouteContract({
},
})

/**
* Every status the execution logger can persist into `workflow_execution_logs.status`,
* including the transient `redacting` state written while a finished run's output is
* scrubbed. The column is free text and both run endpoints pass it straight through, so
* a value missing here fails the response parse — and because list validation is
* whole-page, one such row turns an entire page into a 500. `_ExhaustiveRunStatus` makes
* a future addition to the persisted union a compile error instead.
*/
const V2_PERSISTED_RUN_STATUSES = [
'pending',
'running',
'redacting',
'completed',
'failed',
'cancelled',
] as const satisfies readonly PersistedWorkflowExecutionStatus[]

type AssertNever<T extends never> = T
type _ExhaustiveRunStatus = AssertNever<
Exclude<PersistedWorkflowExecutionStatus, (typeof V2_PERSISTED_RUN_STATUSES)[number]>
>
const RUN_STATUS_DESCRIPTION =
'Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` means the run is not executing and is waiting to be resumed: either held at a human-in-the-loop pause point, or left paused because a resume attempt did not run to completion. The status alone does not say which. On the single-run response `paused.automaticResumeWaitingReason` distinguishes them: it is recorded whenever a resume attempt fails and cleared once a resume succeeds, so a null value means the run is waiting on human input. When the failure is not retryable or the automatic retries are exhausted, the reason is prefixed `Automatic resume requires manual intervention: `. Run-list items carry no `paused` object, so the two cases are indistinguishable there.'

/**
* The list projection overlays `paused` onto the persisted status whenever the run has a
* `paused` or `partially_resumed` row in `paused_executions`. It cannot report `queued`:
* a run that is still only in the job queue has no log row to list.
* The list projection passes `workflow_execution_logs.status` through except where it
* overlays `paused` for a run holding a `paused` or `partially_resumed` row in
* `paused_executions` — so a reported `paused` is either that overlay or the persisted
* value a failed resume attempt left behind. Both branches land in the persisted set, so the reported enum is
* derived from it — a value missing here fails the response parse, and because list
* validation is whole-page one such row turns an entire page into a 500. `queued` is not
* reportable: a run still only in the job queue has no log row to list.
*/
const V2_WORKFLOW_RUN_LIST_STATUSES = [...V2_PERSISTED_RUN_STATUSES, 'paused'] as const

const RUN_STATUS_DESCRIPTION =
'Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed.'

export const v2WorkflowRunListStatusValueSchema = z
.enum(V2_WORKFLOW_RUN_LIST_STATUSES)
.enum(PERSISTED_WORKFLOW_EXECUTION_STATUSES)
.describe(RUN_STATUS_DESCRIPTION)

/**
* The single-run read additionally consults the async job queue by deterministic job id,
* so a run accepted but not yet started reports `queued` rather than 404.
*/
export const v2WorkflowRunStatusValueSchema = z
.enum([...V2_WORKFLOW_RUN_LIST_STATUSES, 'queued'])
.enum([...PERSISTED_WORKFLOW_EXECUTION_STATUSES, 'queued'])
.describe(RUN_STATUS_DESCRIPTION)

/**
Expand Down
30 changes: 24 additions & 6 deletions apps/sim/lib/logs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,31 @@ export interface WorkflowExecutionLog {
createdAt: string
}

/**
* Every value written into `workflow_execution_logs.status`. The column is free text and
* one writer sets it through a raw `sql` CASE Drizzle cannot type-check, so this list —
* not the column type — is the only source of truth. API contracts that pass the column
* through derive their enums from it, so adding a status here widens the public wire; the
* contract tests fail until that widening is reviewed and the OpenAPI specs regenerated.
*
* `redacting` is transient while a finished run's output is scrubbed. `paused` is written
* only by `PauseResumeManager.markResumeAttemptFailed`, when a resume attempt does not run
* to completion — it failed admission, the run buffer was unavailable, the resume job could
* not be enqueued, or the attempt was cancelled. An ordinary human-in-the-loop pause
* persists `pending`.
*/
export const PERSISTED_WORKFLOW_EXECUTION_STATUSES = [
'pending',
'running',
'paused',
'redacting',
'completed',
'failed',
'cancelled',
] as const

export type PersistedWorkflowExecutionStatus =
| 'running'
| 'pending'
| 'completed'
| 'failed'
| 'cancelled'
| 'redacting'
(typeof PERSISTED_WORKFLOW_EXECUTION_STATUSES)[number]

export interface CompletedWorkflowExecutionLog extends WorkflowExecutionLog {
persistedStatus: PersistedWorkflowExecutionStatus
Expand Down
3 changes: 2 additions & 1 deletion packages/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,8 @@ export const workflowExecutionLogs = pgTable(
),

level: text('level').notNull(), // 'info' | 'error'
status: text('status').notNull().default('running'), // 'running' | 'pending' | 'completed' | 'failed' | 'cancelled'
/** See `PERSISTED_WORKFLOW_EXECUTION_STATUSES` in `apps/sim/lib/logs/types.ts`. */
status: text('status').notNull().default('running'),
trigger: text('trigger').notNull(), // 'api' | 'webhook' | 'schedule' | 'manual' | 'chat'

startedAt: timestamp('started_at').notNull(),
Expand Down
Loading