@sixb/pg 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +7 -6
  2. package/dist/agents/agent-storage.d.ts +2 -1
  3. package/dist/agents/agent-storage.d.ts.map +1 -1
  4. package/dist/agents/rows.d.ts +4 -11
  5. package/dist/agents/rows.d.ts.map +1 -1
  6. package/dist/agents/runs.d.ts +3 -2
  7. package/dist/agents/runs.d.ts.map +1 -1
  8. package/dist/auth-storage/service-accounts.d.ts.map +1 -1
  9. package/dist/auth-storage/shared.d.ts +0 -4
  10. package/dist/auth-storage/shared.d.ts.map +1 -1
  11. package/dist/connector-connection-storage.d.ts +9 -0
  12. package/dist/connector-connection-storage.d.ts.map +1 -0
  13. package/dist/index.d.ts +8 -5
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3086 -730
  16. package/dist/index.js.map +28 -25
  17. package/dist/latest-run-query.d.ts.map +1 -1
  18. package/dist/migrations.d.ts.map +1 -1
  19. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  20. package/dist/ontology-storage/materialization-state.d.ts +0 -2
  21. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  22. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  23. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  24. package/dist/ontology-storage/outbox.d.ts.map +1 -1
  25. package/dist/ontology-storage/shared.d.ts +3 -6
  26. package/dist/ontology-storage/shared.d.ts.map +1 -1
  27. package/dist/pg-action-run-storage.d.ts +3 -2
  28. package/dist/pg-action-run-storage.d.ts.map +1 -1
  29. package/dist/pg-ai-usage-storage.d.ts +16 -0
  30. package/dist/pg-ai-usage-storage.d.ts.map +1 -0
  31. package/dist/pg-execution-storage.d.ts +14 -0
  32. package/dist/pg-execution-storage.d.ts.map +1 -0
  33. package/dist/pg-pipeline-run-storage.d.ts +5 -2
  34. package/dist/pg-pipeline-run-storage.d.ts.map +1 -1
  35. package/dist/pg-projection-run-storage.d.ts +5 -2
  36. package/dist/pg-projection-run-storage.d.ts.map +1 -1
  37. package/dist/pg-sync-run-storage.d.ts +5 -2
  38. package/dist/pg-sync-run-storage.d.ts.map +1 -1
  39. package/dist/pg-webhook-run-storage.d.ts +5 -2
  40. package/dist/pg-webhook-run-storage.d.ts.map +1 -1
  41. package/dist/pg-workflow-run-storage.d.ts +5 -3
  42. package/dist/pg-workflow-run-storage.d.ts.map +1 -1
  43. package/dist/run-list-query.d.ts +1 -1
  44. package/dist/run-list-query.d.ts.map +1 -1
  45. package/dist/testing.d.ts +6 -0
  46. package/dist/testing.d.ts.map +1 -0
  47. package/package.json +3 -3
  48. package/src/agents/agent-storage.ts +3 -2
  49. package/src/agents/rows.ts +12 -43
  50. package/src/agents/runs.ts +29 -15
  51. package/src/auth-storage/service-accounts.ts +14 -15
  52. package/src/auth-storage/shared.ts +0 -8
  53. package/src/connector-connection-storage.ts +760 -0
  54. package/src/index.ts +32 -15
  55. package/src/latest-run-query.ts +7 -1
  56. package/src/migrations/004-executions.sql +140 -0
  57. package/src/migrations/005-workflow-executions.sql +10 -0
  58. package/src/migrations/006-narrow-ontology-source-root-index.sql +4 -0
  59. package/src/migrations/007-split-overrides.sql +153 -0
  60. package/src/migrations/008-action-executions.sql +7 -0
  61. package/src/migrations/009-agent-executions.sql +21 -0
  62. package/src/migrations/010-ai-usage-accounting-foundation.sql +76 -0
  63. package/src/migrations/011-sync-failure-record.sql +23 -0
  64. package/src/migrations/012-pipeline-failure-record.sql +47 -0
  65. package/src/migrations/013-workflow-failure-record.sql +45 -0
  66. package/src/migrations/014-agent-failure-record.sql +56 -0
  67. package/src/migrations/015-projection-failure-record.sql +20 -0
  68. package/src/migrations/016-webhook-run-failure-record.sql +19 -0
  69. package/src/migrations/017-action-failure-record.sql +65 -0
  70. package/src/migrations/018-ontology-outbox-failure-record.sql +29 -0
  71. package/src/migrations/019-webhook-delivery-failure-record.sql +27 -0
  72. package/src/migrations/020-drop-run-usage-projections.sql +8 -0
  73. package/src/migrations/021-sync-pipeline-executions.sql +50 -0
  74. package/src/migrations/022-projection-executions.sql +115 -0
  75. package/src/migrations/023-webhook-executions.sql +30 -0
  76. package/src/migrations/024-ontology-commit-executions.sql +12 -0
  77. package/src/migrations/025-connector-connections.sql +231 -0
  78. package/src/migrations.ts +72 -0
  79. package/src/ontology-storage/materialization-session.ts +25 -28
  80. package/src/ontology-storage/materialization-state.ts +159 -43
  81. package/src/ontology-storage/materialization-writer.ts +146 -75
  82. package/src/ontology-storage/materializations.ts +3 -1
  83. package/src/ontology-storage/outbox.ts +9 -2
  84. package/src/ontology-storage/shared.ts +10 -20
  85. package/src/pg-action-run-storage.ts +43 -67
  86. package/src/pg-ai-usage-storage.ts +290 -0
  87. package/src/pg-execution-storage.ts +147 -0
  88. package/src/pg-pipeline-run-storage.ts +107 -41
  89. package/src/pg-projection-run-storage.ts +159 -91
  90. package/src/pg-sync-run-storage.ts +100 -28
  91. package/src/pg-webhook-run-storage.ts +89 -57
  92. package/src/pg-workflow-run-storage.ts +97 -130
  93. package/src/run-list-query.ts +9 -4
  94. package/src/testing.ts +18 -0
  95. package/dist/pg-webhook-delivery-storage.d.ts +0 -17
  96. package/dist/pg-webhook-delivery-storage.d.ts.map +0 -1
  97. package/src/pg-webhook-delivery-storage.ts +0 -180
@@ -1,91 +1,126 @@
1
+ import { parseSixbFailure, serializeSixbFailure } from "@sixb/core/internal/errors"
2
+ import { assertWebhookRunExecution } from "@sixb/core/internal/webhook-run-storage-provider"
1
3
  import type {
4
+ ExecutionStorage,
2
5
  FinishWebhookRunInput,
6
+ GetWebhookRunByDeliveryInput,
3
7
  ListWebhookRunsInput,
4
8
  ListWebhookRunsResult,
9
+ RestartWebhookRunInput,
5
10
  StartWebhookRunInput,
6
11
  WebhookRunRecord,
7
12
  WebhookRunStorage,
8
13
  } from "@sixb/core/storage"
9
- import { WebhookRunError } from "@sixb/core/storage"
14
+ import { canRetryWebhookRun, WEBHOOK_RUN_FAILURE_CODES, WebhookRunError } from "@sixb/core/storage"
10
15
  import type { SqlParameter } from "./pg-client"
11
16
  import { appendRunListFilters, hasEmptyStatuses, queryRunList } from "./run-list-query"
12
17
  import { isUniqueViolation } from "./storage-errors"
13
18
  import { type PgStoreClient, runPgTransaction } from "./transactions"
14
19
 
15
20
  export class PgWebhookRunStorage implements WebhookRunStorage {
16
- constructor(private readonly sql: PgStoreClient) {}
21
+ constructor(
22
+ private readonly sql: PgStoreClient,
23
+ private readonly executions: ExecutionStorage
24
+ ) {}
17
25
 
18
26
  async start(input: StartWebhookRunInput): Promise<WebhookRunRecord> {
27
+ await assertWebhookRunExecution({
28
+ executions: this.executions,
29
+ projectId: input.projectId,
30
+ executionId: input.executionId,
31
+ runId: input.id,
32
+ route: input.route,
33
+ })
19
34
  try {
20
35
  const [row] = await this.sql<WebhookRunDatabaseRow[]>`
21
36
  INSERT INTO webhook_runs (
22
- project_id,
23
- id,
24
- connector_id,
25
- webhook_id,
26
- status,
27
- method,
28
- route,
29
- started_at
37
+ project_id, id, execution_id, connector_id, webhook_id, status, method, route,
38
+ started_at, request_body_bytes, request_body_sha256, idempotency_key
30
39
  ) VALUES (
31
- ${input.projectId},
32
- ${input.id},
33
- ${input.connectorId},
34
- ${input.webhookId},
35
- ${"running"},
36
- ${input.method},
37
- ${input.route},
38
- ${input.startedAt ?? new Date()}
40
+ ${input.projectId}, ${input.id}, ${input.executionId}, ${input.connectorId},
41
+ ${input.webhookId}, ${"running"}, ${input.method}, ${input.route},
42
+ ${input.startedAt ?? new Date()}, ${input.requestBodyBytes},
43
+ ${input.requestBodySha256}, ${input.idempotencyKey ?? null}
39
44
  )
40
45
  RETURNING *
41
46
  `
42
-
43
47
  return rowToWebhookRunRecord(row)
44
48
  } catch (error) {
45
49
  if (isUniqueViolation(error)) {
46
50
  throw new WebhookRunError(
47
- `[SixbPg] Webhook run '${input.id}' already exists for project '${input.projectId}'.`
51
+ `[SixbPg] Webhook run '${input.id}' or delivery key already exists for project '${input.projectId}'.`,
52
+ "duplicate_run"
48
53
  )
49
54
  }
50
-
51
55
  throw error
52
56
  }
53
57
  }
54
58
 
55
- async finish(input: FinishWebhookRunInput): Promise<WebhookRunRecord> {
59
+ async restart(input: RestartWebhookRunInput): Promise<WebhookRunRecord> {
56
60
  return runPgTransaction(this.sql, async (tx) => {
57
61
  const [existing] = await tx<WebhookRunDatabaseRow[]>`
58
62
  SELECT * FROM webhook_runs
59
63
  WHERE project_id = ${input.projectId} AND id = ${input.id}
60
64
  FOR UPDATE
61
65
  `
62
-
63
66
  if (!existing) {
64
67
  throw new WebhookRunError(
65
- `[SixbPg] Webhook run '${input.id}' not found for project '${input.projectId}'.`
68
+ `[SixbPg] Webhook run '${input.id}' not found for project '${input.projectId}'.`,
69
+ "not_found"
70
+ )
71
+ }
72
+ if (!canRetryWebhookRun(rowToWebhookRunRecord(existing))) {
73
+ throw new WebhookRunError(
74
+ `[SixbPg] Webhook run '${input.id}' cannot restart from status '${existing.status}'.`,
75
+ "invalid_transition"
76
+ )
77
+ }
78
+
79
+ const [updated] = await tx<WebhookRunDatabaseRow[]>`
80
+ UPDATE webhook_runs
81
+ SET status = ${"running"}, started_at = ${input.startedAt ?? new Date()},
82
+ finished_at = ${null}, response_status = ${null}, error = ${null}
83
+ WHERE project_id = ${input.projectId} AND id = ${input.id} AND status = ${"failed"}
84
+ RETURNING *
85
+ `
86
+ if (!updated) {
87
+ throw new WebhookRunError(
88
+ `[SixbPg] Webhook run '${input.id}' changed while restarting.`,
89
+ "invalid_transition"
66
90
  )
67
91
  }
92
+ return rowToWebhookRunRecord(updated)
93
+ })
94
+ }
68
95
 
96
+ async finish(input: FinishWebhookRunInput): Promise<WebhookRunRecord> {
97
+ return runPgTransaction(this.sql, async (tx) => {
98
+ const [existing] = await tx<WebhookRunDatabaseRow[]>`
99
+ SELECT * FROM webhook_runs
100
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
101
+ FOR UPDATE
102
+ `
103
+ if (!existing) {
104
+ throw new WebhookRunError(
105
+ `[SixbPg] Webhook run '${input.id}' not found for project '${input.projectId}'.`,
106
+ "not_found"
107
+ )
108
+ }
69
109
  if (existing.status !== "running") {
70
110
  throw new WebhookRunError(
71
- `[SixbPg] Webhook run '${input.id}' for project '${input.projectId}' is already terminal.`
111
+ `[SixbPg] Webhook run '${input.id}' for project '${input.projectId}' is already terminal.`,
112
+ "invalid_transition"
72
113
  )
73
114
  }
74
115
 
75
116
  const [updated] = await tx<WebhookRunDatabaseRow[]>`
76
117
  UPDATE webhook_runs
77
- SET
78
- status = ${input.status},
79
- finished_at = ${input.finishedAt ?? new Date()},
80
- request_body_bytes = ${input.requestBodyBytes ?? null},
81
- response_status = ${input.responseStatus ?? null},
82
- idempotency_key = ${input.idempotencyKey ?? null},
83
- delivery_claim_result = ${input.deliveryClaimResult ?? null},
84
- error = ${input.status === "succeeded" ? null : (input.error ?? null)}
118
+ SET status = ${input.status}, finished_at = ${input.finishedAt ?? new Date()},
119
+ response_status = ${input.responseStatus ?? null},
120
+ error = ${input.status === "succeeded" ? null : serializeSixbFailure(input.error, WEBHOOK_RUN_FAILURE_CODES)}::text::jsonb
85
121
  WHERE project_id = ${input.projectId} AND id = ${input.id}
86
122
  RETURNING *
87
123
  `
88
-
89
124
  return rowToWebhookRunRecord(updated)
90
125
  })
91
126
  }
@@ -95,38 +130,38 @@ export class PgWebhookRunStorage implements WebhookRunStorage {
95
130
  SELECT * FROM webhook_runs
96
131
  WHERE project_id = ${params.projectId} AND id = ${params.id}
97
132
  `
133
+ return row ? rowToWebhookRunRecord(row) : null
134
+ }
98
135
 
136
+ async getByDelivery(input: GetWebhookRunByDeliveryInput): Promise<WebhookRunRecord | null> {
137
+ const [row] = await this.sql<WebhookRunDatabaseRow[]>`
138
+ SELECT * FROM webhook_runs
139
+ WHERE project_id = ${input.projectId}
140
+ AND connector_id = ${input.connectorId}
141
+ AND webhook_id = ${input.webhookId}
142
+ AND idempotency_key = ${input.idempotencyKey}
143
+ `
99
144
  return row ? rowToWebhookRunRecord(row) : null
100
145
  }
101
146
 
102
147
  async list(input: ListWebhookRunsInput): Promise<ListWebhookRunsResult> {
103
- if (hasEmptyStatuses(input)) {
104
- return {
105
- runs: [],
106
- hasMore: false,
107
- total: 0,
108
- }
109
- }
148
+ if (hasEmptyStatuses(input)) return { runs: [], hasMore: false, total: 0 }
110
149
 
111
150
  const whereClauses = ["project_id = $1"]
112
151
  const params: SqlParameter[] = [input.projectId]
113
152
  let index = 2
114
-
115
153
  if (input.connectorId) {
116
154
  whereClauses.push(`connector_id = $${index++}`)
117
155
  params.push(input.connectorId)
118
156
  }
119
-
120
157
  if (input.webhookId) {
121
158
  whereClauses.push(`webhook_id = $${index++}`)
122
159
  params.push(input.webhookId)
123
160
  }
124
-
125
161
  if (input.idempotencyKey) {
126
162
  whereClauses.push(`idempotency_key = $${index++}`)
127
163
  params.push(input.idempotencyKey)
128
164
  }
129
-
130
165
  index = appendRunListFilters(whereClauses, params, index, input)
131
166
 
132
167
  const { rows, total, hasMore } = await queryRunList<WebhookRunDatabaseRow>({
@@ -139,12 +174,7 @@ export class PgWebhookRunStorage implements WebhookRunStorage {
139
174
  limit: input.limit,
140
175
  offset: input.offset,
141
176
  })
142
-
143
- return {
144
- runs: rows.map(rowToWebhookRunRecord),
145
- hasMore,
146
- total,
147
- }
177
+ return { runs: rows.map(rowToWebhookRunRecord), hasMore, total }
148
178
  }
149
179
  }
150
180
 
@@ -152,6 +182,7 @@ function rowToWebhookRunRecord(row: WebhookRunDatabaseRow): WebhookRunRecord {
152
182
  return {
153
183
  id: row.id,
154
184
  projectId: row.project_id,
185
+ executionId: row.execution_id,
155
186
  connectorId: row.connector_id,
156
187
  webhookId: row.webhook_id,
157
188
  status: row.status,
@@ -159,17 +190,18 @@ function rowToWebhookRunRecord(row: WebhookRunDatabaseRow): WebhookRunRecord {
159
190
  route: row.route,
160
191
  startedAt: new Date(row.started_at),
161
192
  finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
162
- requestBodyBytes: row.request_body_bytes != null ? Number(row.request_body_bytes) : undefined,
193
+ requestBodyBytes: Number(row.request_body_bytes),
194
+ requestBodySha256: row.request_body_sha256,
163
195
  responseStatus: row.response_status != null ? Number(row.response_status) : undefined,
164
196
  idempotencyKey: row.idempotency_key ?? undefined,
165
- deliveryClaimResult: row.delivery_claim_result ?? undefined,
166
- error: row.error ?? undefined,
197
+ error: row.error === null ? undefined : parseSixbFailure(row.error, WEBHOOK_RUN_FAILURE_CODES),
167
198
  }
168
199
  }
169
200
 
170
201
  interface WebhookRunDatabaseRow {
171
202
  readonly project_id: string
172
203
  readonly id: string
204
+ readonly execution_id: string
173
205
  readonly connector_id: string
174
206
  readonly webhook_id: string
175
207
  readonly status: WebhookRunRecord["status"]
@@ -177,9 +209,9 @@ interface WebhookRunDatabaseRow {
177
209
  readonly route: string
178
210
  readonly started_at: Date | string
179
211
  readonly finished_at: Date | string | null
180
- readonly request_body_bytes: number | string | null
212
+ readonly request_body_bytes: number | string
213
+ readonly request_body_sha256: string
181
214
  readonly response_status: number | string | null
182
215
  readonly idempotency_key: string | null
183
- readonly delivery_claim_result: WebhookRunRecord["deliveryClaimResult"] | null
184
- readonly error: string | null
216
+ readonly error: unknown | null
185
217
  }
@@ -1,10 +1,17 @@
1
- import type { Principal, WorkflowRunSource } from "@sixb/core"
1
+ import type { JsonValue } from "@sixb/core"
2
+ import { normalizeRequesterGroupIds } from "@sixb/core/internal/auth"
3
+ import { parseSixbFailure, serializeSixbFailure } from "@sixb/core/internal/errors"
4
+ import {
5
+ assertWorkflowAgentNodeRunExecution,
6
+ assertWorkflowRunExecution,
7
+ } from "@sixb/core/internal/workflow-run-storage-provider"
2
8
  import type { WorkflowIOSnapshot } from "@sixb/core/internal/workflows"
3
9
  import type {
4
10
  CancelWorkflowAgentNodeRunInput,
5
11
  ConfirmWorkflowAgentNodeRunExecutionOwnershipInput,
6
12
  ConfirmWorkflowRunExecutionOwnershipInput,
7
13
  CreateWorkflowAgentNodeRunInput,
14
+ ExecutionStorage,
8
15
  FinishWorkflowAgentNodeRunInput,
9
16
  FinishWorkflowNodeRunInput,
10
17
  FinishWorkflowRunInput,
@@ -32,7 +39,11 @@ import type {
32
39
  WorkflowRunRecord,
33
40
  WorkflowRunStorage,
34
41
  } from "@sixb/core/storage"
35
- import { WorkflowRunError } from "@sixb/core/storage"
42
+ import {
43
+ AGENT_RUN_FAILURE_CODES,
44
+ WORKFLOW_RUN_FAILURE_CODES,
45
+ WorkflowRunError,
46
+ } from "@sixb/core/storage"
36
47
  import { queryLatestRunsByOwnerId } from "./latest-run-query"
37
48
  import type { SqlParameter } from "./pg-client"
38
49
  import { appendRunListFilters, hasEmptyStatuses, queryRunList } from "./run-list-query"
@@ -43,39 +54,47 @@ export class PgWorkflowRunStorage implements WorkflowRunStorage {
43
54
  readonly nodes: PgWorkflowNodeRunStorage
44
55
  readonly agentNodes: PgWorkflowAgentNodeRunStorage
45
56
 
46
- constructor(private readonly sql: PgStoreClient) {
57
+ constructor(
58
+ private readonly sql: PgStoreClient,
59
+ private readonly executions: ExecutionStorage
60
+ ) {
47
61
  this.nodes = new PgWorkflowNodeRunStorage(sql)
48
- this.agentNodes = new PgWorkflowAgentNodeRunStorage(sql)
62
+ this.agentNodes = new PgWorkflowAgentNodeRunStorage(sql, executions)
49
63
  }
50
64
 
51
65
  async queue(input: QueueWorkflowRunInput): Promise<WorkflowRunRecord> {
52
66
  const queuedAt = input.queuedAt ?? new Date()
67
+ await assertWorkflowRunExecution({
68
+ executions: this.executions,
69
+ projectId: input.projectId,
70
+ executionId: input.executionId,
71
+ runId: input.id,
72
+ workflowId: input.workflowId,
73
+ })
53
74
 
54
75
  try {
55
76
  const [row] = await this.sql<WorkflowRunDatabaseRow[]>`
56
77
  INSERT INTO workflow_runs (
57
78
  project_id,
58
79
  id,
80
+ execution_id,
59
81
  workflow_id,
60
82
  status,
61
83
  input,
62
84
  queued_at,
63
85
  started_at,
64
- source,
65
- requested_by_principal_type,
66
- requested_by_principal_id,
86
+ requester_group_ids,
67
87
  attempt
68
88
  ) VALUES (
69
89
  ${input.projectId},
70
90
  ${input.id},
91
+ ${input.executionId},
71
92
  ${input.workflowId},
72
93
  ${"queued"},
73
94
  ${serializeRecord(input.input)}::text::jsonb,
74
95
  ${queuedAt},
75
96
  ${queuedAt},
76
- ${input.source ? JSON.stringify(input.source) : null}::text::jsonb,
77
- ${input.requestedByPrincipal?.type ?? "system"},
78
- ${input.requestedByPrincipal?.id ?? "system"},
97
+ ${JSON.stringify(normalizeRequesterGroupIds(input.requesterGroupIds))}::text::jsonb,
79
98
  ${0}
80
99
  )
81
100
  RETURNING *
@@ -85,7 +104,7 @@ export class PgWorkflowRunStorage implements WorkflowRunStorage {
85
104
  } catch (error) {
86
105
  if (isUniqueViolation(error)) {
87
106
  throw new WorkflowRunError(
88
- `[SixbPg] Workflow run '${input.id}' already exists for project '${input.projectId}'.`
107
+ `[SixbPg] Workflow run '${input.id}' or execution '${input.executionId}' is already linked for project '${input.projectId}'.`
89
108
  )
90
109
  }
91
110
 
@@ -102,83 +121,29 @@ export class PgWorkflowRunStorage implements WorkflowRunStorage {
102
121
  FOR UPDATE
103
122
  `
104
123
 
105
- if (existing) {
106
- if (existing.status !== "queued") {
107
- throw new WorkflowRunError(
108
- `[SixbPg] Workflow run '${input.id}' already exists for project '${input.projectId}'.`
109
- )
110
- }
111
-
112
- if (existing.workflow_id !== input.workflowId) {
113
- throw new WorkflowRunError(
114
- `[SixbPg] Workflow run '${input.id}' workflow '${input.workflowId}' does not match existing workflow '${existing.workflow_id}'.`
115
- )
116
- }
117
-
118
- const [updated] = await tx<WorkflowRunDatabaseRow[]>`
119
- UPDATE workflow_runs
120
- SET
121
- status = ${"running"},
122
- input = ${serializeRecord(input.input)}::text::jsonb,
123
- started_at = ${startedAt},
124
- finished_at = ${null},
125
- error = ${null},
126
- source = COALESCE(
127
- source,
128
- ${input.source ? JSON.stringify(input.source) : null}::text::jsonb
129
- ),
130
- attempt = attempt + 1,
131
- execution_token = ${input.execution?.token ?? null},
132
- execution_queue_lease_expires_at = ${input.execution?.queueLeaseExpiresAt ?? null}
133
- WHERE project_id = ${input.projectId} AND id = ${input.id}
134
- RETURNING *
135
- `
136
-
137
- return rowToWorkflowRunRecord(updated)
124
+ if (!existing || existing.status !== "queued") {
125
+ throw new WorkflowRunError(
126
+ existing
127
+ ? `[SixbPg] Workflow run '${input.id}' cannot start from status '${existing.status}'.`
128
+ : `[SixbPg] Workflow run '${input.id}' not found for project '${input.projectId}'.`
129
+ )
138
130
  }
139
131
 
140
- try {
141
- const [row] = await tx<WorkflowRunDatabaseRow[]>`
142
- INSERT INTO workflow_runs (
143
- project_id,
144
- id,
145
- workflow_id,
146
- status,
147
- input,
148
- started_at,
149
- source,
150
- requested_by_principal_type,
151
- requested_by_principal_id,
152
- attempt,
153
- execution_token,
154
- execution_queue_lease_expires_at
155
- ) VALUES (
156
- ${input.projectId},
157
- ${input.id},
158
- ${input.workflowId},
159
- ${"running"},
160
- ${serializeRecord(input.input)}::text::jsonb,
161
- ${startedAt},
162
- ${input.source ? JSON.stringify(input.source) : null}::text::jsonb,
163
- ${input.requestedByPrincipal?.type ?? "system"},
164
- ${input.requestedByPrincipal?.id ?? "system"},
165
- ${1},
166
- ${input.execution?.token ?? null},
167
- ${input.execution?.queueLeaseExpiresAt ?? null}
168
- )
169
- RETURNING *
170
- `
171
-
172
- return rowToWorkflowRunRecord(row)
173
- } catch (error) {
174
- if (isUniqueViolation(error)) {
175
- throw new WorkflowRunError(
176
- `[SixbPg] Workflow run '${input.id}' already exists for project '${input.projectId}'.`
177
- )
178
- }
132
+ const [updated] = await tx<WorkflowRunDatabaseRow[]>`
133
+ UPDATE workflow_runs
134
+ SET
135
+ status = ${"running"},
136
+ started_at = ${startedAt},
137
+ finished_at = ${null},
138
+ error = ${null},
139
+ attempt = attempt + 1,
140
+ execution_token = ${input.execution?.token ?? null},
141
+ execution_queue_lease_expires_at = ${input.execution?.queueLeaseExpiresAt ?? null}
142
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
143
+ RETURNING *
144
+ `
179
145
 
180
- throw error
181
- }
146
+ return rowToWorkflowRunRecord(updated)
182
147
  })
183
148
  }
184
149
 
@@ -258,7 +223,7 @@ export class PgWorkflowRunStorage implements WorkflowRunStorage {
258
223
  status = ${input.status},
259
224
  finished_at = ${input.finishedAt ?? new Date()},
260
225
  output = ${null},
261
- error = ${input.error ?? null},
226
+ error = ${input.error === undefined ? null : serializeSixbFailure(input.error, WORKFLOW_RUN_FAILURE_CODES)}::text::jsonb,
262
227
  execution_token = ${null},
263
228
  execution_queue_lease_expires_at = ${null}
264
229
  WHERE project_id = ${input.projectId} AND id = ${input.id}
@@ -416,16 +381,43 @@ export class PgWorkflowRunStorage implements WorkflowRunStorage {
416
381
  }
417
382
 
418
383
  export class PgWorkflowAgentNodeRunStorage implements WorkflowAgentNodeRunStorage {
419
- constructor(private readonly sql: PgStoreClient) {}
384
+ constructor(
385
+ private readonly sql: PgStoreClient,
386
+ private readonly executions: ExecutionStorage
387
+ ) {}
420
388
 
421
389
  async create(input: CreateWorkflowAgentNodeRunInput): Promise<WorkflowAgentNodeRunRecord> {
390
+ const [parent] = await this.sql<{ execution_id: string }[]>`
391
+ SELECT workflow_runs.execution_id
392
+ FROM workflow_node_runs
393
+ JOIN workflow_runs
394
+ ON workflow_runs.project_id = workflow_node_runs.project_id
395
+ AND workflow_runs.id = workflow_node_runs.workflow_run_id
396
+ WHERE workflow_node_runs.project_id = ${input.projectId}
397
+ AND workflow_node_runs.id = ${input.nodeRunId}
398
+ AND workflow_node_runs.node_type = ${"agent"}
399
+ `
400
+ if (!parent) {
401
+ throw new WorkflowRunError(
402
+ `[SixbPg] Agent workflow node run '${input.nodeRunId}' was not found.`
403
+ )
404
+ }
405
+ await assertWorkflowAgentNodeRunExecution({
406
+ executions: this.executions,
407
+ projectId: input.projectId,
408
+ executionId: input.executionId,
409
+ nodeRunId: input.nodeRunId,
410
+ agentId: input.agentId,
411
+ workflowExecutionId: parent.execution_id,
412
+ })
413
+
422
414
  try {
423
415
  const [row] = await this.sql<WorkflowAgentNodeRunDatabaseRow[]>`
424
416
  INSERT INTO workflow_agent_node_runs (
425
- project_id, node_run_id, agent_id, status, prompt, attempt, created_at
417
+ project_id, node_run_id, execution_id, agent_id, status, prompt, attempt, created_at
426
418
  ) VALUES (
427
- ${input.projectId}, ${input.nodeRunId}, ${input.agentId}, ${"queued"},
428
- ${input.prompt}, ${0}, ${input.createdAt ?? new Date()}
419
+ ${input.projectId}, ${input.nodeRunId}, ${input.executionId}, ${input.agentId},
420
+ ${"queued"}, ${input.prompt}, ${0}, ${input.createdAt ?? new Date()}
429
421
  ) RETURNING *
430
422
  `
431
423
  return rowToWorkflowAgentNodeRunRecord(row)
@@ -445,8 +437,6 @@ export class PgWorkflowAgentNodeRunStorage implements WorkflowAgentNodeRunStorag
445
437
  const [updated] = await tx<WorkflowAgentNodeRunDatabaseRow[]>`
446
438
  UPDATE workflow_agent_node_runs SET
447
439
  status = ${"running"},
448
- execution_principal_type = ${input.executionPrincipal?.type ?? null},
449
- execution_principal_id = ${input.executionPrincipal?.id ?? null},
450
440
  model_id = ${input.modelId ?? null},
451
441
  attempt = ${1},
452
442
  execution_token = ${input.execution.token},
@@ -504,10 +494,9 @@ export class PgWorkflowAgentNodeRunStorage implements WorkflowAgentNodeRunStorag
504
494
  status = ${input.status},
505
495
  model_id = COALESCE(${input.modelId ?? null}, model_id),
506
496
  finish_reason = ${input.finishReason ?? null},
507
- usage = ${input.usage ? JSON.stringify(input.usage) : null}::text::jsonb,
508
497
  trace = ${input.trace ? JSON.stringify(input.trace) : null}::text::jsonb,
509
498
  diagnostics = ${input.diagnostics ? JSON.stringify(input.diagnostics) : null}::text::jsonb,
510
- error = ${input.status === "succeeded" ? null : (input.error ?? null)},
499
+ error = ${input.status === "succeeded" || input.error === undefined ? null : serializeSixbFailure(input.error, AGENT_RUN_FAILURE_CODES)}::text::jsonb,
511
500
  execution_token = ${null},
512
501
  execution_queue_lease_expires_at = ${null},
513
502
  completed_at = ${input.completedAt ?? new Date()}
@@ -538,7 +527,7 @@ export class PgWorkflowAgentNodeRunStorage implements WorkflowAgentNodeRunStorag
538
527
  const [updated] = await tx<WorkflowAgentNodeRunDatabaseRow[]>`
539
528
  UPDATE workflow_agent_node_runs SET
540
529
  status = ${"cancelled"},
541
- error = ${input.error ?? null},
530
+ error = ${input.error === undefined ? null : serializeSixbFailure(input.error, AGENT_RUN_FAILURE_CODES)}::text::jsonb,
542
531
  execution_token = ${null},
543
532
  execution_queue_lease_expires_at = ${null},
544
533
  completed_at = ${input.completedAt ?? new Date()}
@@ -698,7 +687,7 @@ export class PgWorkflowNodeRunStorage implements WorkflowNodeRunStorage {
698
687
  status = ${input.status},
699
688
  finished_at = ${input.finishedAt ?? new Date()},
700
689
  output = ${null},
701
- error = ${input.error ?? null}
690
+ error = ${input.error === undefined ? null : serializeSixbFailure(input.error, WORKFLOW_RUN_FAILURE_CODES)}::text::jsonb
702
691
  WHERE project_id = ${input.projectId} AND id = ${input.id}
703
692
  RETURNING *
704
693
  `
@@ -824,6 +813,7 @@ function rowToWorkflowRunRecord(row: WorkflowRunDatabaseRow): WorkflowRunRecord
824
813
  return {
825
814
  id: row.id,
826
815
  projectId: row.project_id,
816
+ executionId: row.execution_id,
827
817
  workflowId: row.workflow_id,
828
818
  status: row.status,
829
819
  input: parseRecord(row.input),
@@ -831,12 +821,8 @@ function rowToWorkflowRunRecord(row: WorkflowRunDatabaseRow): WorkflowRunRecord
831
821
  queuedAt: row.queued_at ? new Date(row.queued_at) : undefined,
832
822
  startedAt: new Date(row.started_at),
833
823
  finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
834
- error: row.error ?? undefined,
835
- source: parseSource(row.source),
836
- requestedByPrincipal: {
837
- type: row.requested_by_principal_type,
838
- id: row.requested_by_principal_id,
839
- },
824
+ error: row.error === null ? undefined : parseSixbFailure(row.error, WORKFLOW_RUN_FAILURE_CODES),
825
+ requesterGroupIds: parseJson(row.requester_group_ids),
840
826
  attempt: Number(row.attempt),
841
827
  ...(row.execution_token && row.execution_queue_lease_expires_at
842
828
  ? {
@@ -849,14 +835,6 @@ function rowToWorkflowRunRecord(row: WorkflowRunDatabaseRow): WorkflowRunRecord
849
835
  }
850
836
  }
851
837
 
852
- function parseSource(value: WorkflowRunSource | string | null): WorkflowRunSource | undefined {
853
- if (value === null || value === undefined) {
854
- return undefined
855
- }
856
-
857
- return typeof value === "string" ? (JSON.parse(value) as WorkflowRunSource) : value
858
- }
859
-
860
838
  function rowToWorkflowNodeRunRecord(row: WorkflowNodeRunDatabaseRow): WorkflowNodeRunRecord {
861
839
  return {
862
840
  id: row.id,
@@ -872,7 +850,7 @@ function rowToWorkflowNodeRunRecord(row: WorkflowNodeRunDatabaseRow): WorkflowNo
872
850
  startedAt: new Date(row.started_at),
873
851
  finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
874
852
  output: row.output ? parseRecord(row.output) : undefined,
875
- error: row.error ?? undefined,
853
+ error: row.error === null ? undefined : parseSixbFailure(row.error, WORKFLOW_RUN_FAILURE_CODES),
876
854
  }
877
855
  }
878
856
 
@@ -896,6 +874,7 @@ function canFinishWorkflowRun(
896
874
  interface WorkflowRunDatabaseRow {
897
875
  project_id: string
898
876
  id: string
877
+ execution_id: string
899
878
  workflow_id: string
900
879
  status: WorkflowRunRecord["status"]
901
880
  input: WorkflowIOSnapshot | string
@@ -903,10 +882,8 @@ interface WorkflowRunDatabaseRow {
903
882
  queued_at: Date | string | null
904
883
  started_at: Date | string
905
884
  finished_at: Date | string | null
906
- error: string | null
907
- source: WorkflowRunSource | string | null
908
- requested_by_principal_type: Principal["type"]
909
- requested_by_principal_id: string
885
+ error: JsonValue | null
886
+ requester_group_ids: string[] | string
910
887
  attempt: number | string
911
888
  execution_token: string | null
912
889
  execution_queue_lease_expires_at: Date | string | null
@@ -926,23 +903,21 @@ interface WorkflowNodeRunDatabaseRow {
926
903
  started_at: Date | string
927
904
  finished_at: Date | string | null
928
905
  output: WorkflowIOSnapshot | string | null
929
- error: string | null
906
+ error: JsonValue | null
930
907
  }
931
908
 
932
909
  interface WorkflowAgentNodeRunDatabaseRow {
933
910
  project_id: string
934
911
  node_run_id: string
912
+ execution_id: string
935
913
  agent_id: string
936
914
  status: WorkflowAgentNodeRunRecord["status"]
937
915
  prompt: string
938
- execution_principal_type: "serviceAccount" | null
939
- execution_principal_id: string | null
940
916
  model_id: string | null
941
917
  finish_reason: WorkflowAgentNodeRunRecord["finishReason"] | null
942
- usage: WorkflowAgentNodeRunRecord["usage"] | string | null
943
918
  trace: WorkflowAgentNodeRunRecord["trace"] | string | null
944
919
  diagnostics: WorkflowAgentNodeRunRecord["diagnostics"] | string | null
945
- error: string | null
920
+ error: JsonValue | null
946
921
  attempt: number | string
947
922
  execution_token: string | null
948
923
  execution_queue_lease_expires_at: Date | string | null
@@ -992,23 +967,15 @@ function rowToWorkflowAgentNodeRunRecord(
992
967
  return {
993
968
  projectId: row.project_id,
994
969
  nodeRunId: row.node_run_id,
970
+ executionId: row.execution_id,
995
971
  agentId: row.agent_id,
996
972
  status: row.status,
997
973
  prompt: row.prompt,
998
- ...(row.execution_principal_type && row.execution_principal_id
999
- ? {
1000
- executionPrincipal: {
1001
- type: row.execution_principal_type,
1002
- id: row.execution_principal_id,
1003
- },
1004
- }
1005
- : {}),
1006
974
  ...(row.model_id ? { modelId: row.model_id } : {}),
1007
975
  ...(row.finish_reason ? { finishReason: row.finish_reason } : {}),
1008
- ...(row.usage ? { usage: parseJson(row.usage) } : {}),
1009
976
  ...(row.trace ? { trace: parseJson(row.trace) } : {}),
1010
977
  ...(row.diagnostics ? { diagnostics: parseJson(row.diagnostics) } : {}),
1011
- ...(row.error ? { error: row.error } : {}),
978
+ ...(row.error ? { error: parseSixbFailure(row.error, AGENT_RUN_FAILURE_CODES) } : {}),
1012
979
  attempt: Number(row.attempt),
1013
980
  ...(row.execution_token && row.execution_queue_lease_expires_at
1014
981
  ? {