@k2b/cloud 0.43.0 → 0.45.0

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 (308) hide show
  1. package/package.json +11 -2
  2. package/src/_internal/app-access-request.ts +13 -0
  3. package/src/_internal/app-access-resources.ts +141 -0
  4. package/src/_internal/app-event-catalog.ts +40 -0
  5. package/src/_internal/app-event-schemas.ts +127 -0
  6. package/src/_internal/application-package.ts +40 -0
  7. package/src/_internal/built-in-app-ids.json +27 -0
  8. package/src/_internal/capabilities.ts +158 -12
  9. package/src/_internal/capability-origin.ts +4 -0
  10. package/src/_internal/capability-streams.ts +7 -5
  11. package/src/_internal/define-app.ts +57 -6
  12. package/src/_internal/object-store-area.ts +9 -0
  13. package/src/_internal/object-store-config.ts +101 -0
  14. package/src/_internal/object-store-key.ts +14 -0
  15. package/src/_internal/process-identity.ts +7 -2
  16. package/src/_internal/registry-validation.ts +11 -1
  17. package/src/_internal/registry.ts +8 -1
  18. package/src/_internal/runtime-context.ts +8 -1
  19. package/src/access/AppAccessEditor.tsx +2 -0
  20. package/src/access/GroupCoverage.tsx +20 -5
  21. package/src/access/PermissionEditor.tsx +20 -5
  22. package/src/access/PrincipalAvatar.tsx +30 -21
  23. package/src/access/PrincipalPicker.tsx +14 -4
  24. package/src/access/ResourceApiKeys.tsx +3 -4
  25. package/src/access/app-access-messages.ts +6 -0
  26. package/src/access/messages.ts +4 -4
  27. package/src/account/EntitySearch.tsx +12 -0
  28. package/src/account/ProfilePictureDialog.tsx +699 -0
  29. package/src/account/profile-picture-messages.ts +269 -0
  30. package/src/account/ui.ts +7 -0
  31. package/src/ai/admin.ts +3 -0
  32. package/src/ai/app-frame.ts +12 -0
  33. package/src/ai/application-tasks.ts +101 -0
  34. package/src/ai/assistant-models.ts +126 -64
  35. package/src/ai/capabilities.ts +19 -4
  36. package/src/ai/chart-block.ts +63 -30
  37. package/src/ai/chat/chart-block.tsx +12 -3
  38. package/src/ai/chat/composer-adapter.ts +8 -7
  39. package/src/ai/chat/message-actions.tsx +1 -1
  40. package/src/ai/chat/message-utils.ts +2 -2
  41. package/src/ai/chat/presentation.tsx +22 -1
  42. package/src/ai/chat-quotas.ts +5 -4
  43. package/src/ai/chat-tasks.ts +2 -2
  44. package/src/ai/client/controller.ts +11 -10
  45. package/src/ai/client/projection.ts +3 -3
  46. package/src/ai/compaction.ts +3 -3
  47. package/src/ai/credentials.ts +13 -114
  48. package/src/ai/default-tool-contracts.ts +4 -3
  49. package/src/ai/dictation-runtime.ts +10 -6
  50. package/src/ai/enrich.ts +2 -2
  51. package/src/ai/executor.ts +40 -25
  52. package/src/ai/http.ts +17 -29
  53. package/src/ai/index.ts +12 -5
  54. package/src/ai/inference-calls.ts +5 -4
  55. package/src/ai/maintenance.ts +1 -1
  56. package/src/ai/memory-learning-runs.ts +5 -5
  57. package/src/ai/memory-learning.ts +3 -3
  58. package/src/ai/message-queue.ts +1 -2
  59. package/src/ai/migrate.ts +75 -42
  60. package/src/ai/model-access-migrate.ts +3 -17
  61. package/src/ai/model-access.ts +44 -85
  62. package/src/ai/model-card.tsx +134 -0
  63. package/src/ai/model-catalog.ts +359 -0
  64. package/src/ai/model-chooser-messages.ts +88 -0
  65. package/src/ai/model-chooser.tsx +577 -0
  66. package/src/ai/model-delete.ts +234 -0
  67. package/src/ai/model-fixture.ts +39 -0
  68. package/src/ai/model-icon.tsx +36 -0
  69. package/src/ai/model-overview.ts +53 -0
  70. package/src/ai/model-usage.ts +45 -0
  71. package/src/ai/openai-api-messages.ts +0 -4
  72. package/src/ai/openai-api.ts +26 -9
  73. package/src/ai/personal-agent.ts +1 -1
  74. package/src/ai/prefs.ts +12 -10
  75. package/src/ai/project-tool.ts +4 -4
  76. package/src/ai/projects-authorization.ts +53 -0
  77. package/src/ai/projects-routes.ts +58 -47
  78. package/src/ai/projects.ts +110 -88
  79. package/src/ai/protocol.ts +2 -2
  80. package/src/ai/provider-connections.ts +161 -0
  81. package/src/ai/provider-errors.ts +55 -6
  82. package/src/ai/provider-fetch.ts +4 -0
  83. package/src/ai/provider.ts +1 -1
  84. package/src/ai/quota-limits.ts +36 -0
  85. package/src/ai/quota-provider.ts +4 -2
  86. package/src/ai/quota-report.ts +7 -4
  87. package/src/ai/quotas.ts +19 -8
  88. package/src/ai/recent-resources.ts +9 -3
  89. package/src/ai/request-headers.ts +14 -87
  90. package/src/ai/resource-refs.ts +6 -2
  91. package/src/ai/routes.ts +47 -48
  92. package/src/ai/runtime.ts +50 -22
  93. package/src/ai/settings.ts +93 -580
  94. package/src/ai/skill-tool.ts +4 -4
  95. package/src/ai/skills-routes.ts +27 -26
  96. package/src/ai/skills.ts +120 -81
  97. package/src/ai/store.ts +45 -23
  98. package/src/ai/stream.ts +1 -1
  99. package/src/ai/structured-runs.ts +2 -2
  100. package/src/ai/structured.ts +21 -44
  101. package/src/ai/task-contracts.ts +25 -5
  102. package/src/ai/task-execution.ts +15 -6
  103. package/src/ai/task-model.ts +9 -0
  104. package/src/ai/transcription.ts +27 -22
  105. package/src/ai/types.ts +50 -62
  106. package/src/ai/ui.tsx +13 -0
  107. package/src/ai/usage.ts +8 -8
  108. package/src/ai/validate.ts +8 -7
  109. package/src/ai/view-block.ts +2 -2
  110. package/src/api/accounts-entities.ts +22 -3
  111. package/src/api/admin-ai-quotas.ts +34 -56
  112. package/src/api/admin-ai-skills.ts +4 -1
  113. package/src/api/admin-ai-usage.ts +1 -1
  114. package/src/api/admin-ai.ts +173 -0
  115. package/src/api/admin-core-settings.ts +68 -244
  116. package/src/api/app-approval.ts +3 -1
  117. package/src/api/app-event-delivery.ts +51 -0
  118. package/src/api/app-events.ts +123 -0
  119. package/src/api/app-resource-access.ts +48 -0
  120. package/src/api/auth/account-mismatch.ts +24 -0
  121. package/src/api/auth.ts +35 -5
  122. package/src/api/capabilities.ts +28 -4
  123. package/src/api/index.ts +7 -1
  124. package/src/api/mcp.ts +175 -2
  125. package/src/api/me.ts +6 -4
  126. package/src/api/pwa-phone.ts +75 -26
  127. package/src/api/pwa.ts +34 -8
  128. package/src/api/resource-links.ts +83 -6
  129. package/src/api/resource-resolve.ts +50 -0
  130. package/src/api/search/schemas.ts +3 -1
  131. package/src/api/search.ts +9 -7
  132. package/src/app-pwa/contracts.ts +4 -0
  133. package/src/app-pwa/core-query-error.ts +20 -0
  134. package/src/app-pwa/index.ts +1 -0
  135. package/src/app-pwa/service.ts +91 -0
  136. package/src/app-pwa/transport.ts +22 -0
  137. package/src/approvals/messages.ts +2 -2
  138. package/src/browser/cloud-id.ts +86 -0
  139. package/src/browser/resource-clipboard.ts +4 -0
  140. package/src/browser/resource-links.ts +52 -0
  141. package/src/browser/session.ts +39 -0
  142. package/src/browser/shared-text-history-validation.ts +151 -0
  143. package/src/browser/shared-text-history.ts +464 -0
  144. package/src/browser/shared-text-offline-authorized.ts +29 -0
  145. package/src/browser/shared-text-offline.ts +351 -0
  146. package/src/browser/shared-text-provider.ts +714 -0
  147. package/src/browser/shared-text-table-contract.ts +150 -0
  148. package/src/browser/shared-text.ts +19 -0
  149. package/src/capabilities/server-invoke.ts +2 -11
  150. package/src/capabilities/testing.ts +5 -2
  151. package/src/cli/access.ts +6 -5
  152. package/src/cli/admin/ai-models.ts +199 -0
  153. package/src/cli/admin/ai-quotas.ts +6 -102
  154. package/src/cli/admin/ai-usage.ts +4 -4
  155. package/src/cli/admin/app-credentials.ts +5 -3
  156. package/src/cli/admin/index.ts +5 -2
  157. package/src/cli/apps.ts +89 -26
  158. package/src/cli/capabilities.ts +22 -1
  159. package/src/cli/events.ts +320 -0
  160. package/src/config/env.ts +40 -0
  161. package/src/contracts/app-access.ts +26 -2
  162. package/src/contracts/app-details.ts +89 -0
  163. package/src/contracts/app-events.ts +51 -0
  164. package/src/contracts/app.ts +21 -5
  165. package/src/contracts/capabilities.ts +143 -3
  166. package/src/contracts/index.ts +2 -0
  167. package/src/contracts/notification-types.ts +36 -9
  168. package/src/contracts/pwa.ts +17 -1
  169. package/src/contracts/registry.ts +6 -0
  170. package/src/contracts/resource-clipboard.ts +35 -0
  171. package/src/contracts/resource-pages.ts +140 -0
  172. package/src/contracts/resource-resolution.ts +13 -0
  173. package/src/contracts/shared.ts +6 -1
  174. package/src/contracts/user-notifications.ts +2 -0
  175. package/src/events/app-event-delivery.ts +100 -0
  176. package/src/events/app-events.testing.ts +65 -0
  177. package/src/events/app-events.ts +134 -0
  178. package/src/events/core-user-created.ts +24 -0
  179. package/src/events/index.ts +9 -0
  180. package/src/events/testing.ts +2 -0
  181. package/src/index.ts +1 -1
  182. package/src/server/api/respond.ts +17 -4
  183. package/src/server/index.ts +2 -0
  184. package/src/server/middleware/app-access.ts +1 -1
  185. package/src/server/middleware/auth.ts +21 -7
  186. package/src/server/middleware/index.ts +1 -0
  187. package/src/server/middleware/invocation.ts +2 -0
  188. package/src/server/public-storage.ts +106 -0
  189. package/src/server/services/access.ts +31 -10
  190. package/src/server/services/index.ts +1 -0
  191. package/src/services/accounts/app.ts +36 -13
  192. package/src/services/accounts/entities.ts +43 -19
  193. package/src/services/accounts/group-sql.ts +23 -0
  194. package/src/services/accounts/groups.ts +19 -14
  195. package/src/services/accounts/identities.ts +70 -11
  196. package/src/services/accounts/identity-reconciliation.ts +3 -1
  197. package/src/services/accounts/local-groups.ts +69 -4
  198. package/src/services/accounts/users.ts +45 -14
  199. package/src/services/app-access.ts +25 -7
  200. package/src/services/app-details.ts +224 -0
  201. package/src/services/app-discovery.ts +2 -1
  202. package/src/services/app-event-shapes.ts +139 -0
  203. package/src/services/app-events/accept.ts +116 -0
  204. package/src/services/app-events/admin.ts +250 -0
  205. package/src/services/app-events/declarations.ts +187 -0
  206. package/src/services/app-events/deliver.ts +218 -0
  207. package/src/services/app-events.ts +92 -0
  208. package/src/services/config-conflict.ts +11 -0
  209. package/src/services/core-user-events.ts +1 -0
  210. package/src/services/freeipa-config.ts +9 -0
  211. package/src/services/geo/ledger.ts +28 -10
  212. package/src/services/geo/service.ts +43 -24
  213. package/src/services/identity/core-origin.ts +12 -0
  214. package/src/services/identity/invocation-actor.ts +20 -2
  215. package/src/services/identity/invocation-authority.ts +4 -0
  216. package/src/services/identity/invocation-token.ts +238 -56
  217. package/src/services/identity/issue-invocation.ts +6 -1
  218. package/src/services/image-metadata.ts +65 -1
  219. package/src/services/index.ts +13 -1
  220. package/src/services/ipa/sync.ts +6 -2
  221. package/src/services/ipa/users.ts +4 -1
  222. package/src/services/mandates/index.ts +2 -2
  223. package/src/services/mandates/policy.ts +20 -12
  224. package/src/services/notifications/browser.ts +4 -0
  225. package/src/services/notifications/catalog.ts +5 -2
  226. package/src/services/notifications/platform.ts +7 -2
  227. package/src/services/notifications/user.ts +18 -7
  228. package/src/services/oauth-tokens.ts +28 -2
  229. package/src/services/object-store.ts +217 -0
  230. package/src/services/providers/local/users.ts +5 -0
  231. package/src/services/pwa-devices.ts +147 -37
  232. package/src/services/session/index.ts +26 -3
  233. package/src/services/settings/core-settings.ts +71 -43
  234. package/src/services/settings/index.ts +8 -0
  235. package/src/services/settings/versions.ts +26 -0
  236. package/src/shared/ai-models.ts +274 -0
  237. package/src/shared/ai-quotas.ts +2 -2
  238. package/src/shared/ai-usage.ts +2 -2
  239. package/src/shared/email-html.ts +1 -0
  240. package/src/shared/index.ts +2 -0
  241. package/src/shared/markdown/extensions/katex.ts +8 -1
  242. package/src/shared/markdown/extensions/tables.ts +14 -1
  243. package/src/shared/markdown/index.ts +22 -11
  244. package/src/shared-text/anchors.ts +29 -0
  245. package/src/shared-text/awareness.ts +87 -0
  246. package/src/shared-text/changes.ts +27 -0
  247. package/src/shared-text/documents.ts +433 -65
  248. package/src/shared-text/events.ts +12 -2
  249. package/src/shared-text/index.ts +16 -1
  250. package/src/shared-text/log.ts +24 -5
  251. package/src/shared-text/snapshots.ts +69 -23
  252. package/src/shared-text/structural-history.ts +377 -0
  253. package/src/shared-text/table-actions.ts +432 -0
  254. package/src/shared-text/text-diff.ts +75 -0
  255. package/src/shared-text/types.ts +24 -5
  256. package/src/ssr/AdminArea.tsx +131 -0
  257. package/src/ssr/AdminLayout.tsx +9 -3
  258. package/src/ssr/AdminSidebar.tsx +7 -7
  259. package/src/ssr/AppAccessDenied.tsx +2 -0
  260. package/src/ssr/AppBadge.tsx +14 -0
  261. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  262. package/src/ssr/CloudShell.island.tsx +44 -7
  263. package/src/ssr/Layout.tsx +9 -7
  264. package/src/ssr/MobileProfileActions.tsx +2 -2
  265. package/src/ssr/ProfilePreferences.tsx +2 -1
  266. package/src/ssr/PwaLayout.tsx +16 -1
  267. package/src/ssr/PwaPartCount.island.tsx +23 -0
  268. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  269. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  270. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  271. package/src/ssr/admin-active-link.ts +16 -0
  272. package/src/ssr/admin-navigation.ts +26 -31
  273. package/src/ssr/app-badges.ts +31 -7
  274. package/src/ssr/app-launchpad.tsx +6 -1
  275. package/src/ssr/index.ts +8 -0
  276. package/src/ssr/page-trail-runtime.ts +3 -1
  277. package/src/ssr/platform-messages.ts +11 -1
  278. package/src/ssr/profile-actions.ts +3 -6
  279. package/src/ssr/pwa-messages.ts +2 -2
  280. package/src/ssr/rail-navigation.ts +5 -1
  281. package/src/ssr/settings-draft.ts +153 -0
  282. package/src/ssr/shell-data.tsx +3 -1
  283. package/src/styles/global.css +1 -0
  284. package/src/styles/profile-picture.css +270 -0
  285. package/src/styles/utilities-navigation.css +6 -0
  286. package/src/workflows/ai/policy.ts +49 -0
  287. package/src/workflows/ai/runtime.ts +131 -21
  288. package/src/workflows/ai/store.ts +283 -35
  289. package/src/workflows/ai/types.ts +8 -2
  290. package/src/workflows/ai-actions.ts +148 -120
  291. package/src/workflows/authoring.ts +31 -0
  292. package/src/workflows/contracts.ts +1 -0
  293. package/src/workflows/definition.ts +2 -0
  294. package/src/workflows/editor.ts +15 -0
  295. package/src/workflows/index.ts +2 -1
  296. package/src/workflows/language/compiler.ts +34 -3
  297. package/src/workflows/language/expressions.ts +3 -2
  298. package/src/workflows/runtime/executor.ts +134 -34
  299. package/src/workflows/runtime/index.ts +2 -0
  300. package/src/workflows/runtime/ports.ts +5 -1
  301. package/src/workflows/store/actions.ts +1 -1
  302. package/src/workflows/store/definitions.ts +5 -0
  303. package/src/workflows/store/runs.ts +5 -3
  304. package/src/workflows/store/worker.ts +7 -2
  305. package/src/ai/model-pricing.ts +0 -43
  306. package/src/ai/model-request-settings.ts +0 -98
  307. package/src/ai/settings-messages.ts +0 -129
  308. package/src/shared/ai-model-request-settings.ts +0 -21
@@ -1,15 +1,22 @@
1
1
  import { StructuredOutputError } from "@k2b/nessi";
2
- import type { Job, Worker } from "@k2b/sync";
2
+ import { type Job, StoreFullError, type Worker } from "@k2b/sync";
3
3
  import { lazySync } from "../../_internal/process-sync";
4
- import { isAiSettingsError, runAiStructured } from "../../ai";
5
4
  import { AiBackgroundAdmissionError, AiBackgroundCostError } from "../../ai/inference-calls";
5
+ import { AiQuotaError } from "../../ai/quotas";
6
+ import { resolveAiWorkflowModel, runAiStructured } from "../../ai/structured";
6
7
  import { executeAiTask } from "../../ai/task-execution";
8
+ import { resolveApplicationAiTaskModel } from "../../ai/task-model";
9
+ import { isAiSettingsError } from "../../ai/validate";
10
+ import { env } from "../../config/env";
11
+ import { WorkflowAiAdmissionError, workflowAiModelHash } from "./policy";
7
12
 
8
13
  import {
9
14
  claimWorkflowAiTask,
10
15
  completeWorkflowAiTask,
16
+ expireWorkflowAiTasks,
11
17
  failWorkflowAiTask,
12
18
  getWorkflowAiTask,
19
+ heartbeatWorkflowAiTask,
13
20
  listRecoverableWorkflowAiTaskIds,
14
21
  markWorkflowAiTaskCanceledIfRequested,
15
22
  requeueWorkflowAiTask,
@@ -18,7 +25,7 @@ import {
18
25
  } from "./store";
19
26
  import type { WorkflowAiTask } from "./types";
20
27
 
21
- const JOB_ID = "cloud.workflow-ai";
28
+ export const WORKFLOW_AI_JOB_ID = "cloud.workflow-ai.v2";
22
29
  const MAX_ATTEMPTS = 3;
23
30
  const DEFAULT_CANCEL_POLL_MS = 500;
24
31
  const HEARTBEAT_INTERVAL_MS = 10_000;
@@ -28,16 +35,26 @@ type StructuredRunner = typeof runAiStructured;
28
35
  export type WorkflowAiRuntimeOptions = {
29
36
  runStructured?: StructuredRunner;
30
37
  cancelPollMs?: number;
38
+ attemptTimeoutMs?: number;
31
39
  };
32
40
 
33
41
  /** Attempt budget shared by the job declaration and the Postgres-side settle logic. */
34
- export type WorkflowAiProcessOptions = Required<WorkflowAiRuntimeOptions> & { maxAttempts: number };
42
+ export type WorkflowAiProcessOptions = Required<Omit<WorkflowAiRuntimeOptions, "attemptTimeoutMs">> & {
43
+ maxAttempts: number;
44
+ attemptTimeoutMs?: number;
45
+ };
35
46
 
36
47
  const workflowAiJob = lazySync((sync) =>
37
48
  sync.job<{ taskId: string }>({
38
- id: JOB_ID,
49
+ id: WORKFLOW_AI_JOB_ID,
50
+ whenFull: "reject",
39
51
  owner: "cloud",
40
- delivery: { ackWaitMs: 30_000, maxAttempts: MAX_ATTEMPTS, backoffMs: [1_000, 2_000] },
52
+ delivery: {
53
+ maxInFlight: env.WORKFLOW_AI_BUDGET.globalConcurrent,
54
+ ackWaitMs: 30_000,
55
+ maxAttempts: MAX_ATTEMPTS,
56
+ backoffMs: [1_000, 2_000],
57
+ },
41
58
  }),
42
59
  );
43
60
 
@@ -76,24 +93,40 @@ export const executeWorkflowAiRequest = async (task: WorkflowAiTask, runStructur
76
93
  task.request,
77
94
  {
78
95
  appId: task.appId,
79
- attribution: { workflowRunId: task.runId, stepKey: task.stepKey, userId: task.usageUserId },
80
- requestedModelId: task.modelProfileId,
96
+ attribution: {
97
+ workflowRunId: task.runId ?? undefined,
98
+ stepKey: task.stepKey,
99
+ userId: task.usageSubject?.type === "user" ? task.usageSubject.userId : task.usageUserId,
100
+ },
101
+ usageSubject: task.usageSubject,
102
+ model: { id: task.modelId },
103
+ resolveModel: async () => {
104
+ const resolved = task.runId
105
+ ? await resolveAiWorkflowModel({ id: task.modelId })
106
+ : await resolveApplicationAiTaskModel(task.usageSubject, { id: task.modelId });
107
+ if (task.modelConfigHash && workflowAiModelHash(resolved.profile) !== task.modelConfigHash)
108
+ throw new WorkflowAiAdmissionError("WORKFLOW_AI_MODEL_CHANGED");
109
+ return { ...resolved, model: task.modelRef ?? resolved.model };
110
+ },
81
111
  signal,
82
- taskPrefix: "workflow",
112
+ taskPrefix: task.runId ? "workflow" : "application",
83
113
  },
84
114
  runStructured,
85
115
  );
86
116
 
87
117
  const retryableError = (error: unknown): boolean => {
118
+ if (error instanceof WorkflowAiAdmissionError) return error.retryable;
88
119
  if (error instanceof AiBackgroundAdmissionError) return error.retryable;
89
- if (error instanceof AiBackgroundCostError) return false;
120
+ if (error instanceof AiBackgroundCostError || error instanceof AiQuotaError) return false;
90
121
  if (isAiSettingsError(error)) return false;
91
122
  if (error instanceof StructuredOutputError) return error.code === "loop_failed";
92
123
  return true;
93
124
  };
94
125
 
95
126
  const errorCode = (error: unknown): string => {
96
- if (error instanceof AiBackgroundCostError || error instanceof AiBackgroundAdmissionError) return error.code;
127
+ if (error instanceof WorkflowAiAdmissionError) return error.code;
128
+ if (error instanceof AiBackgroundCostError || error instanceof AiBackgroundAdmissionError || error instanceof AiQuotaError)
129
+ return error.code;
97
130
  if (isAiSettingsError(error)) return error.aiError.code.toUpperCase();
98
131
  if (error instanceof StructuredOutputError) return `WORKFLOW_AI_${error.code.toUpperCase()}`;
99
132
  return "WORKFLOW_AI_PROVIDER_ERROR";
@@ -113,12 +146,18 @@ export const processWorkflowAiTask = async (
113
146
  if (task.status === "canceled") return;
114
147
 
115
148
  const controller = new AbortController();
149
+ let timedOut = false;
150
+ const deadline = setTimeout(() => {
151
+ timedOut = true;
152
+ controller.abort(new Error("Workflow AI attempt deadline exceeded. Retry the workflow or inspect the provider."));
153
+ }, options.attemptTimeoutMs ?? env.WORKFLOW_AI_ATTEMPT_TIMEOUT_MS);
116
154
  let canceled = false;
117
155
  let monitorError: unknown;
118
156
  let checking = false;
119
157
  let lastHeartbeat = Date.now();
120
158
  const abortForShutdown = () => controller.abort(ctx.signal.reason ?? new Error("Workflow AI runtime stopped"));
121
159
  ctx.signal.addEventListener("abort", abortForShutdown, { once: true });
160
+ if (ctx.signal.aborted) abortForShutdown();
122
161
 
123
162
  const monitor = setInterval(() => {
124
163
  if (checking || controller.signal.aborted) return;
@@ -132,6 +171,7 @@ export const processWorkflowAiTask = async (
132
171
  }
133
172
  if (Date.now() - lastHeartbeat >= HEARTBEAT_INTERVAL_MS) {
134
173
  await ctx.heartbeat();
174
+ await heartbeatWorkflowAiTask(task.id);
135
175
  lastHeartbeat = Date.now();
136
176
  }
137
177
  } catch (error) {
@@ -144,7 +184,13 @@ export const processWorkflowAiTask = async (
144
184
  }, options.cancelPollMs);
145
185
 
146
186
  try {
147
- const result = await executeWorkflowAiRequest(task, options.runStructured, controller.signal);
187
+ controller.signal.throwIfAborted();
188
+ const aborted = new Promise<never>((_resolve, reject) => {
189
+ const onAbort = () => reject(controller.signal.reason);
190
+ if (controller.signal.aborted) onAbort();
191
+ else controller.signal.addEventListener("abort", onAbort, { once: true });
192
+ });
193
+ const result = await Promise.race([executeWorkflowAiRequest(task, options.runStructured, controller.signal), aborted]);
148
194
  if (canceled || (await workflowAiTaskCancellationRequested(task.id))) {
149
195
  await markWorkflowAiTaskCanceledIfRequested(task.id);
150
196
  return;
@@ -155,6 +201,13 @@ export const processWorkflowAiTask = async (
155
201
  await markWorkflowAiTaskCanceledIfRequested(task.id);
156
202
  return;
157
203
  }
204
+ if (timedOut) {
205
+ throw new WorkflowAiAttemptError(
206
+ "WORKFLOW_AI_TIMEOUT",
207
+ "Workflow AI attempt deadline exceeded. Retry the workflow or inspect the provider.",
208
+ true,
209
+ );
210
+ }
158
211
  if (ctx.signal.aborted || monitorError) {
159
212
  throw new WorkflowAiAttemptError("WORKFLOW_AI_INTERRUPTED", "Workflow AI task was interrupted.", true);
160
213
  }
@@ -165,12 +218,23 @@ export const processWorkflowAiTask = async (
165
218
  );
166
219
  } finally {
167
220
  clearInterval(monitor);
221
+ clearTimeout(deadline);
168
222
  ctx.signal.removeEventListener("abort", abortForShutdown);
169
223
  }
170
224
  };
171
225
 
172
226
  let activeJob: Job<{ taskId: string }> | null = null;
173
227
  let activeWorker: Worker | null = null;
228
+ let recoveryTimer: ReturnType<typeof setInterval> | null = null;
229
+ let recovery: Promise<void> | null = null;
230
+ let shutdown: AbortController | null = null;
231
+
232
+ const recover = (): Promise<void> => {
233
+ recovery ??= expireWorkflowAiTasks(env.WORKFLOW_AI_QUEUE_TIMEOUT_MS).finally(() => {
234
+ recovery = null;
235
+ });
236
+ return recovery;
237
+ };
174
238
 
175
239
  export const startWorkflowAiRuntime = async (input: WorkflowAiRuntimeOptions = {}): Promise<void> => {
176
240
  if (activeJob) return;
@@ -178,14 +242,26 @@ export const startWorkflowAiRuntime = async (input: WorkflowAiRuntimeOptions = {
178
242
  runStructured: input.runStructured ?? runAiStructured,
179
243
  maxAttempts: MAX_ATTEMPTS,
180
244
  cancelPollMs: input.cancelPollMs ?? DEFAULT_CANCEL_POLL_MS,
245
+ attemptTimeoutMs: input.attemptTimeoutMs ?? env.WORKFLOW_AI_ATTEMPT_TIMEOUT_MS,
181
246
  };
182
247
  const next = workflowAiJob();
183
248
  activeJob = next;
249
+ const controller = new AbortController();
250
+ shutdown = controller;
184
251
  try {
185
- activeWorker = await next.process({ concurrency: 1 }, async (context) => {
252
+ activeWorker = await next.process({ concurrency: env.WORKFLOW_AI_BUDGET.globalConcurrent }, async (context) => {
186
253
  try {
187
- await processWorkflowAiTask(context.input.taskId, context, options);
254
+ await processWorkflowAiTask(
255
+ context.input.taskId,
256
+ { signal: AbortSignal.any([context.signal, controller.signal]), heartbeat: () => context.heartbeat() },
257
+ options,
258
+ );
188
259
  } catch (caught) {
260
+ if (controller.signal.aborted) {
261
+ // Shutdown is not a provider failure. Persist before drain lets the process exit.
262
+ if (!(await markWorkflowAiTaskCanceledIfRequested(context.input.taskId))) await requeueWorkflowAiTask(context.input.taskId);
263
+ throw caught;
264
+ }
189
265
  const error =
190
266
  caught instanceof WorkflowAiAttemptError
191
267
  ? caught
@@ -195,21 +271,55 @@ export const startWorkflowAiRuntime = async (input: WorkflowAiRuntimeOptions = {
195
271
  // Postgres owns the terminal result, including intentional cancellation.
196
272
  }
197
273
  });
198
- for (const taskId of await listRecoverableWorkflowAiTaskIds()) await submitWorkflowAiTask(taskId);
274
+ await recover();
275
+ for (const taskId of await listRecoverableWorkflowAiTaskIds()) {
276
+ try {
277
+ await submitWorkflowAiTask(taskId);
278
+ } catch (error) {
279
+ // Submission already persists and wakes the failed task; recover the others.
280
+ console.error("Workflow AI task resubmission failed", { taskId, error });
281
+ }
282
+ }
283
+ recoveryTimer = setInterval(() => {
284
+ void recover().catch((error) => console.error("Workflow AI recovery failed", error));
285
+ }, HEARTBEAT_INTERVAL_MS);
199
286
  } catch (error) {
200
- stopWorkflowAiRuntime();
287
+ await stopWorkflowAiRuntime();
201
288
  throw error;
202
289
  }
203
290
  };
204
291
 
205
292
  export const submitWorkflowAiTask = async (taskId: string): Promise<void> => {
206
293
  if (!activeJob) throw new Error("Workflow AI runtime is not started.");
207
- await activeJob.submit({ key: `task:${taskId}`, coalesce: true, input: { taskId } });
294
+ try {
295
+ await activeJob.submit({ key: `task:${taskId}`, coalesce: true, input: { taskId } });
296
+ } catch (error) {
297
+ const failure = new WorkflowAiAttemptError(
298
+ error instanceof StoreFullError ? "WORKFLOW_AI_QUEUE_FULL" : "WORKFLOW_AI_SUBMISSION_FAILED",
299
+ "Workflow AI queue could not accept the task. Retry the workflow after capacity or connectivity recovers.",
300
+ true,
301
+ );
302
+ await failWorkflowAiTask(taskId, failure);
303
+ throw failure;
304
+ }
208
305
  };
209
306
 
210
- export const stopWorkflowAiRuntime = (): void => {
211
- activeWorker?.stop();
307
+ export const stopWorkflowAiRuntime = async (): Promise<void> => {
308
+ const worker = activeWorker;
309
+ worker?.stop();
310
+ shutdown?.abort(new Error("Workflow AI runtime stopped"));
311
+ if (recoveryTimer) clearInterval(recoveryTimer);
312
+ recoveryTimer = null;
212
313
 
213
- activeWorker = null;
214
- activeJob = null;
314
+ // Sync's existing drain budget bounds settlement; do not exit before DB requeue.
315
+ try {
316
+ await worker?.drain();
317
+ } finally {
318
+ // The recovery timer already reports its failure. Always allow a later start,
319
+ // including when drain rejects, while preserving the drain error for callers.
320
+ await recovery?.catch(() => {});
321
+ activeWorker = null;
322
+ activeJob = null;
323
+ shutdown = null;
324
+ }
215
325
  };