@k2b/cloud 0.6.0 → 0.8.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 (349) hide show
  1. package/README.md +2 -2
  2. package/package.json +22 -11
  3. package/scripts/README.md +10 -0
  4. package/scripts/browser-performance.ts +14 -0
  5. package/scripts/build-pdf-renderer.ts +39 -0
  6. package/scripts/build.ts +20 -35
  7. package/scripts/preload.ts +16 -12
  8. package/scripts/sync-recovery-smoke.ts +22 -6
  9. package/src/_internal/app-presentation.ts +14 -2
  10. package/src/_internal/build-metadata.ts +8 -1
  11. package/src/_internal/capabilities.ts +131 -7
  12. package/src/_internal/capability-streams.ts +81 -0
  13. package/src/_internal/define-app.ts +80 -37
  14. package/src/_internal/heartbeat.ts +4 -0
  15. package/src/_internal/help-catalog.ts +27 -172
  16. package/src/_internal/help.ts +13 -76
  17. package/src/_internal/page-responses.ts +2 -0
  18. package/src/_internal/postgres-application-name.ts +30 -0
  19. package/src/_internal/process-identity.ts +15 -0
  20. package/src/_internal/process-sync.ts +1 -1
  21. package/src/_internal/readiness.ts +4 -1
  22. package/src/_internal/registry-validation.ts +29 -25
  23. package/src/_internal/registry.ts +2 -53
  24. package/src/_internal/runtime-context.ts +5 -16
  25. package/src/_internal/server-timing.ts +13 -0
  26. package/src/_internal/status-preserving-ssr.ts +18 -6
  27. package/src/_internal/web-vitals-asset.ts +10 -0
  28. package/src/access/PermissionEditor.tsx +18 -151
  29. package/src/access/PrincipalPicker.tsx +110 -0
  30. package/src/access/ResourceApiKeys.tsx +1 -1
  31. package/src/access/ui.ts +1 -0
  32. package/src/ai/admin.ts +14 -13
  33. package/src/ai/assistant-models.ts +12 -1
  34. package/src/ai/audio-format.ts +34 -0
  35. package/src/ai/audio-tool.ts +90 -0
  36. package/src/ai/browser-code-contracts.ts +201 -0
  37. package/src/ai/browser.ts +4 -0
  38. package/src/ai/capabilities.ts +55 -36
  39. package/src/ai/capability-execution.ts +47 -12
  40. package/src/ai/chat/blocks.tsx +229 -90
  41. package/src/ai/chat/builtin-tools.tsx +2 -4
  42. package/src/ai/chat/capability-result.ts +15 -0
  43. package/src/ai/chat/capability-table.tsx +2 -10
  44. package/src/ai/chat/composer-adapter.ts +83 -3
  45. package/src/ai/chat/message-actions.tsx +8 -7
  46. package/src/ai/chat/message-utils.ts +25 -1
  47. package/src/ai/chat/messages.ts +2 -0
  48. package/src/ai/chat/presentation.tsx +31 -47
  49. package/src/ai/chat/primitives.tsx +43 -1
  50. package/src/ai/chat/tool-disclosure.tsx +21 -2
  51. package/src/ai/chat/tool-groups.ts +58 -0
  52. package/src/ai/chat-quotas.ts +25 -0
  53. package/src/ai/chat-task-contracts.ts +14 -10
  54. package/src/ai/chat-tasks.ts +297 -43
  55. package/src/ai/client/controller.ts +165 -39
  56. package/src/ai/client/file-source.ts +12 -6
  57. package/src/ai/client/live-connection.ts +1 -0
  58. package/src/ai/client/projection.ts +29 -11
  59. package/src/ai/client/transport.ts +6 -0
  60. package/src/ai/code-capability-routes.ts +105 -0
  61. package/src/ai/code-capability-transport.ts +39 -0
  62. package/src/ai/code-execution.ts +53 -0
  63. package/src/ai/code-mode-skill.ts +118 -0
  64. package/src/ai/code-runtime-tools.ts +179 -0
  65. package/src/ai/code-source-contracts.ts +270 -0
  66. package/src/ai/code-source-tools.ts +188 -0
  67. package/src/ai/compaction.ts +27 -6
  68. package/src/ai/data-analysis-skill.ts +10 -0
  69. package/src/ai/default-tools.ts +99 -3
  70. package/src/ai/dictation-runtime.ts +171 -0
  71. package/src/ai/dictations.ts +188 -0
  72. package/src/ai/draft-content.ts +49 -0
  73. package/src/ai/enrich.ts +6 -0
  74. package/src/ai/executor.ts +189 -33
  75. package/src/ai/fetch-file-tool.ts +2 -2
  76. package/src/ai/file-content-version.ts +27 -0
  77. package/src/ai/file-context.ts +2 -1
  78. package/src/ai/file-media-type.ts +12 -0
  79. package/src/ai/file-reference-contracts.ts +15 -0
  80. package/src/ai/file-tools.ts +8 -1
  81. package/src/ai/files-store.ts +212 -71
  82. package/src/ai/fixtures/vision-encrypted.pdf +0 -0
  83. package/src/ai/grids-skill.ts +42 -10
  84. package/src/ai/http.ts +26 -8
  85. package/src/ai/index.ts +26 -3
  86. package/src/ai/inference-calls.ts +197 -0
  87. package/src/ai/live-events.ts +5 -1
  88. package/src/ai/live.ts +1 -1
  89. package/src/ai/markdown-pdf-tool.ts +2 -2
  90. package/src/ai/memories.ts +30 -27
  91. package/src/ai/memory-learning-runs.ts +1 -1
  92. package/src/ai/memory-learning.ts +64 -32
  93. package/src/ai/memory-tool.ts +1 -5
  94. package/src/ai/memory-workflow-evidence.ts +9 -4
  95. package/src/ai/message-queue.ts +178 -0
  96. package/src/ai/migrate.ts +189 -11
  97. package/src/ai/model-pricing.ts +43 -0
  98. package/src/ai/pdf-render-worker.ts +75 -0
  99. package/src/ai/pdf-render.fixture.ts +28 -0
  100. package/src/ai/pdf-render.ts +77 -0
  101. package/src/ai/projects-routes.ts +18 -43
  102. package/src/ai/projects.ts +33 -14
  103. package/src/ai/protocol.ts +3 -0
  104. package/src/ai/provider.ts +1 -8
  105. package/src/ai/quota-provider.ts +192 -0
  106. package/src/ai/quota-report.ts +152 -0
  107. package/src/ai/quotas-migrate.ts +45 -0
  108. package/src/ai/quotas.ts +168 -0
  109. package/src/ai/routes.ts +241 -11
  110. package/src/ai/run-timeout.ts +12 -0
  111. package/src/ai/runtime-tools.ts +7 -5
  112. package/src/ai/runtime.ts +74 -17
  113. package/src/ai/settings.ts +36 -8
  114. package/src/ai/short-id.ts +4 -5
  115. package/src/ai/skill-catalog.ts +0 -10
  116. package/src/ai/skill-search.ts +58 -0
  117. package/src/ai/skill-seeds.ts +114 -20
  118. package/src/ai/skill-tool.ts +21 -5
  119. package/src/ai/skills-routes.ts +49 -4
  120. package/src/ai/skills.ts +358 -163
  121. package/src/ai/store.ts +416 -201
  122. package/src/ai/stream.ts +5 -0
  123. package/src/ai/structured-runs.ts +4 -3
  124. package/src/ai/structured.ts +12 -1
  125. package/src/ai/system-prompt.ts +14 -2
  126. package/src/ai/task-contracts.ts +84 -0
  127. package/src/ai/task-execution.ts +92 -0
  128. package/src/ai/timeline.ts +2 -5
  129. package/src/ai/todo-contracts.ts +32 -0
  130. package/src/ai/todo-tool.ts +19 -0
  131. package/src/ai/tool-audit.ts +30 -40
  132. package/src/ai/tools.ts +6 -0
  133. package/src/ai/transcription.ts +202 -0
  134. package/src/ai/turn-timing.ts +114 -0
  135. package/src/ai/types.ts +58 -10
  136. package/src/ai/ui.tsx +3 -0
  137. package/src/ai/usage.ts +118 -118
  138. package/src/ai/vision-tool.ts +60 -10
  139. package/src/api/admin-ai-projects.ts +2 -2
  140. package/src/api/admin-ai-quotas.ts +80 -0
  141. package/src/api/admin-ai-skills.ts +48 -5
  142. package/src/api/admin-ai-usage.ts +2 -2
  143. package/src/api/admin-core-settings.ts +25 -0
  144. package/src/api/admin-identity.ts +91 -51
  145. package/src/api/admin-linux-identities.ts +10 -4
  146. package/src/api/admin-rail.ts +58 -0
  147. package/src/api/announcements.ts +14 -0
  148. package/src/api/app-approval.ts +2 -2
  149. package/src/api/auth.ts +1 -1
  150. package/src/api/capabilities.ts +324 -43
  151. package/src/api/capability-streams.ts +262 -0
  152. package/src/api/help.ts +8 -44
  153. package/src/api/index.ts +8 -2
  154. package/src/api/mcp.ts +45 -34
  155. package/src/api/me-web-vitals.ts +35 -0
  156. package/src/api/me.ts +2 -0
  157. package/src/api/search/schemas.ts +26 -15
  158. package/src/api/search.bench.ts +1 -1
  159. package/src/api/search.ts +54 -17
  160. package/src/browser/CloudResourceSearch.tsx +640 -702
  161. package/src/browser/app-approval-vault.ts +19 -7
  162. package/src/browser/command-bridge.ts +109 -0
  163. package/src/browser/command-shortcuts.ts +76 -0
  164. package/src/browser/commands.ts +67 -0
  165. package/src/browser/navigation-search.ts +48 -0
  166. package/src/browser/notifications.ts +1 -1
  167. package/src/browser/resource-picker.tsx +5 -18
  168. package/src/browser/resource-search-dialog.ts +9 -0
  169. package/src/browser/resource-search-input.ts +46 -0
  170. package/src/browser/resource-search-messages.ts +76 -40
  171. package/src/browser/resource-search.ts +7 -0
  172. package/src/browser/search-bridge.ts +102 -0
  173. package/src/browser/search-commands.ts +59 -0
  174. package/src/browser/search.ts +13 -0
  175. package/src/browser/spotlight-position.ts +208 -0
  176. package/src/browser/testing.ts +6 -0
  177. package/src/browser/web-vitals.ts +25 -0
  178. package/src/capabilities/claims.ts +179 -0
  179. package/src/capabilities/client.ts +34 -0
  180. package/src/capabilities/command-link.ts +16 -0
  181. package/src/capabilities/executions.ts +298 -0
  182. package/src/capabilities/migrate.ts +81 -0
  183. package/src/capabilities/server.ts +116 -35
  184. package/src/capabilities/stream-body.ts +16 -0
  185. package/src/capabilities/streams.ts +28 -0
  186. package/src/cli/admin/ai-quotas.ts +161 -0
  187. package/src/cli/admin/ai-skills.ts +64 -0
  188. package/src/cli/admin/ai-usage.ts +53 -30
  189. package/src/cli/admin/app-credentials.ts +77 -0
  190. package/src/cli/admin/data.ts +17 -0
  191. package/src/cli/admin/index.ts +10 -1
  192. package/src/cli/admin/instance.ts +2 -2
  193. package/src/cli/admin/linux.ts +4 -2
  194. package/src/cli/admin/sync.ts +1 -1
  195. package/src/cli/capabilities.ts +42 -0
  196. package/src/cli/capability-streams.ts +25 -0
  197. package/src/cli/index.ts +3 -2
  198. package/src/config/define-env.ts +97 -0
  199. package/src/config/env.ts +147 -51
  200. package/src/config/index.ts +3 -1
  201. package/src/contracts/app.ts +23 -3
  202. package/src/contracts/capabilities.ts +75 -5
  203. package/src/contracts/capability-streams.ts +46 -0
  204. package/src/contracts/commands.ts +49 -0
  205. package/src/contracts/index.ts +5 -2
  206. package/src/contracts/rail-admin.ts +38 -0
  207. package/src/contracts/rail-preferences.ts +16 -0
  208. package/src/contracts/registry.ts +5 -27
  209. package/src/contracts/settings-types.ts +1 -1
  210. package/src/contracts/shared.ts +18 -5
  211. package/src/contracts/web-vitals.ts +27 -0
  212. package/src/desktop/index.ts +1 -0
  213. package/src/desktop/solid.tsx +1 -1
  214. package/src/index.ts +8 -6
  215. package/src/server/index.ts +3 -0
  216. package/src/server/middleware/auth.ts +3 -2
  217. package/src/server/middleware/route-template.ts +9 -3
  218. package/src/server/middleware/runtime.ts +2 -1
  219. package/src/server/middleware/settings.ts +17 -11
  220. package/src/server/ratelimit.ts +1 -1
  221. package/src/server/services/access-revision.ts +10 -0
  222. package/src/server/services/access.ts +3 -1
  223. package/src/server/services/index.ts +1 -0
  224. package/src/services/account-category-policy.ts +9 -0
  225. package/src/services/account-lifecycle/audit.ts +1 -1
  226. package/src/services/accounts/app.ts +12 -2
  227. package/src/services/accounts/email-write.ts +23 -0
  228. package/src/services/accounts/groups.ts +15 -4
  229. package/src/services/accounts/identities.ts +249 -0
  230. package/src/services/accounts/identity-reconciliation.ts +87 -0
  231. package/src/services/accounts/ipa-data.ts +2 -2
  232. package/src/services/accounts/local-groups.ts +2 -2
  233. package/src/services/accounts/notification-sender.ts +12 -2
  234. package/src/services/accounts/posix.ts +45 -29
  235. package/src/services/accounts/users.ts +2 -2
  236. package/src/services/announcements/index.ts +69 -8
  237. package/src/services/app-approval.ts +11 -10
  238. package/src/services/audit/index.ts +1 -1
  239. package/src/services/cache-fill.ts +35 -0
  240. package/src/services/help/index.ts +109 -0
  241. package/src/services/help/maintenance.ts +25 -0
  242. package/src/services/help/store.ts +180 -0
  243. package/src/services/help/types.ts +46 -0
  244. package/src/services/identity/invocation-actor.ts +2 -2
  245. package/src/services/identity/key-config.ts +8 -5
  246. package/src/services/identity/key-ring.ts +1 -1
  247. package/src/services/identity/runtime-config.ts +6 -5
  248. package/src/services/index.ts +15 -0
  249. package/src/services/ipa/posix.ts +1 -1
  250. package/src/services/ipa/sync.ts +1 -1
  251. package/src/services/ipa/users.ts +268 -221
  252. package/src/services/legal-consent.ts +1 -1
  253. package/src/services/logging/index.ts +15 -13
  254. package/src/services/logging/trace.ts +2 -1
  255. package/src/services/logging/web-vitals.ts +81 -0
  256. package/src/services/mandates/index.ts +21 -4
  257. package/src/services/mandates/policy.ts +165 -2
  258. package/src/services/notifications/batches.ts +125 -57
  259. package/src/services/notifications/browser.ts +12 -0
  260. package/src/services/notifications/index.ts +0 -2
  261. package/src/services/notifications/platform.ts +0 -4
  262. package/src/services/oauth-tokens.ts +3 -3
  263. package/src/services/pdf/gotenberg.ts +131 -72
  264. package/src/services/pdf/index.ts +11 -8
  265. package/src/services/providers/local/users.ts +25 -16
  266. package/src/services/public-http.ts +101 -0
  267. package/src/services/rail-shortcuts.ts +95 -0
  268. package/src/services/rail-snapshot.ts +53 -0
  269. package/src/services/request-cache-redis.ts +29 -0
  270. package/src/services/session/index.ts +15 -2
  271. package/src/services/session/user.ts +62 -37
  272. package/src/services/settings/app.ts +5 -1
  273. package/src/services/settings/core-settings.ts +26 -35
  274. package/src/services/settings/defaults.ts +5 -0
  275. package/src/services/settings/index.ts +3 -3
  276. package/src/services/settings/store.ts +58 -84
  277. package/src/services/webauthn.ts +1 -1
  278. package/src/shared/ai-costs.ts +42 -0
  279. package/src/shared/ai-platform-prompt.ts +1 -1
  280. package/src/shared/ai-quotas.ts +136 -0
  281. package/src/shared/ai-usage.ts +6 -16
  282. package/src/shared/app-presentation.ts +21 -15
  283. package/src/shared/capability-messages.ts +14 -0
  284. package/src/shared/help.ts +6 -42
  285. package/src/shared/index.ts +3 -0
  286. package/src/shared/markdown/extensions/info-blocks.ts +1 -1
  287. package/src/ssr/AdminLayout.tsx +7 -5
  288. package/src/ssr/AdminSidebar.tsx +30 -16
  289. package/src/ssr/AppLaunchpad.island.tsx +33 -136
  290. package/src/ssr/AppLaunchpadPanel.tsx +181 -0
  291. package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
  292. package/src/ssr/GlobalSearchDialog.tsx +167 -20
  293. package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
  294. package/src/ssr/Layout.tsx +17 -6
  295. package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
  296. package/src/ssr/LayoutFooter.tsx +1 -1
  297. package/src/ssr/LayoutHeader.tsx +46 -14
  298. package/src/ssr/LayoutHelp.tsx +22 -23
  299. package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
  300. package/src/ssr/LayoutRail.tsx +17 -9
  301. package/src/ssr/MinimalLayout.tsx +1 -1
  302. package/src/ssr/MobileNavigation.tsx +71 -0
  303. package/src/ssr/MobileProfileActions.tsx +49 -0
  304. package/src/ssr/ProfilePreferences.island.tsx +3 -36
  305. package/src/ssr/RailApps.island.tsx +2 -2
  306. package/src/ssr/RailEditor.tsx +17 -2
  307. package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
  308. package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
  309. package/src/ssr/admin-navigation.ts +11 -1
  310. package/src/ssr/help.ts +29 -0
  311. package/src/ssr/islands/SearchBar.tsx +67 -0
  312. package/src/ssr/islands/index.ts +3 -1
  313. package/src/ssr/layout-context.ts +2 -2
  314. package/src/ssr/layout-help-search.ts +15 -0
  315. package/src/ssr/mobile-menu-history.ts +70 -0
  316. package/src/ssr/platform-messages.ts +16 -8
  317. package/src/ssr/profile-actions.ts +42 -0
  318. package/src/ssr/rail-context.ts +8 -4
  319. package/src/ssr/rail-messages.ts +2 -0
  320. package/src/ssr/rail-navigation.ts +3 -3
  321. package/src/ssr/workspace-navigation.ts +75 -0
  322. package/src/styles/effects.css +6 -6
  323. package/src/styles/global.css +54 -0
  324. package/src/styles/input.css +43 -41
  325. package/src/styles/resource-search.css +505 -0
  326. package/src/styles/utilities-feedback.css +13 -0
  327. package/src/styles/utilities-layout.css +1 -5
  328. package/src/styles/utilities-navigation.css +3 -39
  329. package/src/types/ambient.d.ts +5 -0
  330. package/src/workflows/ai/index.ts +2 -2
  331. package/src/workflows/ai/runtime.ts +19 -90
  332. package/src/workflows/ai/store.ts +2 -0
  333. package/src/workflows/ai/types.ts +6 -82
  334. package/src/workflows/ai-actions.ts +16 -13
  335. package/src/workflows/definition.ts +10 -5
  336. package/src/workflows/store/actions.ts +44 -20
  337. package/src/workflows/store/index.ts +4 -2
  338. package/src/workflows/store/runs.ts +36 -0
  339. package/src/workflows/store/worker-pool.ts +55 -0
  340. package/src/workflows/store/worker-runtime.ts +90 -0
  341. package/src/ai/kit-skill.ts +0 -22
  342. package/src/ai/usage-migrate.ts +0 -81
  343. package/src/browser/notification-target.ts +0 -27
  344. package/src/contracts/notification-live.ts +0 -66
  345. package/src/server/services/freeipa/test-certificates.ts +0 -44
  346. package/src/services/notifications/live.ts +0 -50
  347. package/src/services/session/test-fixture.ts +0 -18
  348. package/src/ssr/BrowserNotifications.island.tsx +0 -135
  349. package/src/ssr/islands/SearchBar.island.tsx +0 -80
package/src/ai/routes.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type Context, Hono } from "hono";
2
+ import { bodyLimit } from "hono/body-limit";
2
3
  import { z } from "zod";
3
4
  import { listCapabilities } from "../_internal/registry";
4
5
  import {
@@ -15,11 +16,16 @@ import {
15
16
  respond,
16
17
  v,
17
18
  } from "../server";
19
+ import { logger } from "../services/logging";
18
20
  import { coreSettings } from "../services/settings/api";
19
21
  import type { AiToolApprovalContext } from "./approvals";
20
22
  import { assistantAiSettingsState, listAssistantAiModels, selectAssistantAiModelId } from "./assistant-models";
23
+ import { AI_AUDIO_MAX_BYTES } from "./audio-format";
21
24
  import { buildAiCapabilityCatalog } from "./capabilities";
25
+ import { getAiChatQuotas } from "./chat-quotas";
22
26
  import { createConfiguredDefaultCloudAiTools } from "./default-tools";
27
+ import { enqueueAiDictation } from "./dictation-runtime";
28
+ import { AiDictationConflict, AiDictationListSchema, AiDictationStartSchema, aiDictations } from "./dictations";
23
29
  import { aiProjectFilePathFromMount } from "./file-mount";
24
30
  import { AI_FILES_MAX_FILE_BYTES_DEFAULT, aiFileStore, decodeAiFileContent, guessAiMediaType, normalizeAiFilePath } from "./files-store";
25
31
  import {
@@ -41,6 +47,15 @@ import { aiMaintenanceJobs } from "./maintenance";
41
47
  import { AI_MEMORY_CONTENT_MAX_CHARS, aiMemories } from "./memories";
42
48
  import { aiMemoryLearningRuns } from "./memory-learning-runs";
43
49
  import { createCloudAiMemoryTool } from "./memory-tool";
50
+ import {
51
+ AiMessageQueueConflict,
52
+ editQueuedMessage,
53
+ enqueueChatMessage,
54
+ listQueuedMessages,
55
+ queuedMessageAccepted,
56
+ updateQueuedMessage,
57
+ } from "./message-queue";
58
+ import { aiModelAccess } from "./model-access";
44
59
  import { personalAiModelPolicy } from "./personal-agent";
45
60
  import { aiActorUser, aiPrefsUserId, aiUserPrefs } from "./prefs";
46
61
  import { aiProjects } from "./projects";
@@ -51,9 +66,11 @@ import {
51
66
  AiTurnActionSchema,
52
67
  abortAiTurn,
53
68
  listPendingAiTurnActions,
69
+ prepareAiChatTurn,
54
70
  submitAiChatTurn,
55
71
  submitAiCompaction,
56
72
  submitAiTurnAction,
73
+ wakeAiMessageQueue,
57
74
  } from "./runtime";
58
75
  import { readAiSettingsState, selectAiModelProfile } from "./settings";
59
76
  import { AI_SHORT_ID_PATTERN } from "./short-id";
@@ -64,6 +81,7 @@ import { aiConversations } from "./store";
64
81
  import { createAiConversationStreamResponse, loadAiStreamState } from "./stream";
65
82
  import { composeAiSystemPrompt } from "./system-prompt";
66
83
  import { aiToolPromptHints } from "./tools";
84
+ import { resolveAiAudioModel } from "./transcription";
67
85
  import type { AiConversation, AiModelPolicy, AiTurn } from "./types";
68
86
 
69
87
  /** Everything a resolved, authorized request needs to run against the shared runtime. */
@@ -102,6 +120,10 @@ const ConversationListQuerySchema = z
102
120
  .enum(["true", "false"])
103
121
  .transform((value) => value === "true")
104
122
  .optional(),
123
+ done: z
124
+ .enum(["true", "false"])
125
+ .transform((value) => value === "true")
126
+ .optional(),
105
127
  status: z.enum(["running", "needs_attention", "failed", "unread"]).optional(),
106
128
  projectId: z.string().regex(AI_SHORT_ID_PATTERN).optional(),
107
129
  unassigned: z
@@ -123,6 +145,7 @@ const ConversationMetadataInputSchema = z.object({
123
145
  });
124
146
 
125
147
  const ConversationProjectInputSchema = z.object({
148
+ onlyUnassigned: z.boolean().optional(),
126
149
  projectId: z.string().regex(AI_SHORT_ID_PATTERN).nullable(),
127
150
  });
128
151
 
@@ -212,12 +235,13 @@ const rememberLastUsedModel = (actor: RequestActor, modelProfileId: string | nul
212
235
  };
213
236
 
214
237
  const conversationDetail = async (conversation: AiConversation, subject: AccessSubject) => {
215
- const [state, timeline] = await Promise.all([
238
+ const [state, timeline, publicView] = await Promise.all([
216
239
  loadAiStreamState(conversation),
217
240
  aiConversations.listConversationTimeline({ conversationId: conversation.id }),
241
+ publicConversationFor(conversation, subject),
218
242
  ]);
219
243
  return {
220
- conversation: await publicConversationFor(conversation, subject),
244
+ conversation: publicView,
221
245
  messages: state.messages.map((message) => ({ ...message, id: message.shortId })),
222
246
  hasMoreMessages: state.hasMoreMessages ?? false,
223
247
  activeTurn: state.activeTurn,
@@ -284,6 +308,10 @@ export const aiRoutes = (() => {
284
308
  .get("/status", async (c) => {
285
309
  return respond(c, ok(await assistantAiSettingsState(c.get("accessSubject"))));
286
310
  })
311
+ .get("/quotas", async (c) => {
312
+ c.header("Cache-Control", "no-store");
313
+ return c.json(await getAiChatQuotas(c.get("accessSubject")));
314
+ })
287
315
  .get("/models", async (c) => {
288
316
  return respond(c, ok(await listAssistantAiModels(c.get("accessSubject"))));
289
317
  })
@@ -428,6 +456,7 @@ export const aiRoutes = (() => {
428
456
  ownerUserId: ctx.ownerUserId,
429
457
  search: query.q,
430
458
  archived: query.archived,
459
+ done: query.done,
431
460
  status: query.status,
432
461
  projectId: project?.id,
433
462
  unassigned: query.unassigned,
@@ -448,6 +477,7 @@ export const aiRoutes = (() => {
448
477
  ownerUserId: ctx.ownerUserId,
449
478
  search: query.q,
450
479
  archived: query.archived,
480
+ done: query.done,
451
481
  status: query.status,
452
482
  projectId: project?.id,
453
483
  unassigned: query.unassigned,
@@ -552,7 +582,7 @@ export const aiRoutes = (() => {
552
582
  if (ctx instanceof Response) return ctx;
553
583
  const conversation = await loadConversation(c, ctx);
554
584
  if (!conversation) return notFound(c);
555
- const messageId = c.req.param("messageId");
585
+ const messageId = c.req.param("messageId") ?? "";
556
586
  if (!messageId) return respond(c, fail(err.badInput("Invalid message id.")));
557
587
  const input = c.req.valid("json");
558
588
  const feedback = await aiConversations.setMessageFeedback({
@@ -571,7 +601,7 @@ export const aiRoutes = (() => {
571
601
  if (ctx instanceof Response) return ctx;
572
602
  const conversation = await loadConversation(c, ctx);
573
603
  if (!conversation) return notFound(c);
574
- const messageId = c.req.param("messageId");
604
+ const messageId = c.req.param("messageId") ?? "";
575
605
  if (!messageId) return respond(c, fail(err.badInput("Invalid message id.")));
576
606
  return (await aiConversations.clearMessageFeedback({
577
607
  conversationId: conversation.id,
@@ -650,8 +680,10 @@ export const aiRoutes = (() => {
650
680
  conversationId: conversation.id,
651
681
  ownerUserId: ctx.ownerUserId,
652
682
  projectId: project?.id ?? null,
683
+ onlyUnassigned: body.onlyUnassigned,
653
684
  });
654
685
  if (!result.ok) {
686
+ if (result.reason === "already_assigned") return respond(c, fail(err.conflict("This chat already belongs to a Project.")));
655
687
  if (result.reason === "active_turn") {
656
688
  return respond(c, fail(err.conflict("Wait for the active turn before changing the Project.")));
657
689
  }
@@ -683,6 +715,22 @@ export const aiRoutes = (() => {
683
715
  });
684
716
  return updated ? respond(c, ok(await publicConversationFor(updated, c.get("accessSubject")))) : notFound(c);
685
717
  })
718
+ .put("/conversations/:conversationId/done", v("json", z.object({ done: z.boolean().nullable() })), async (c) => {
719
+ const ctx = await resolveContext(c);
720
+ if (ctx instanceof Response) return ctx;
721
+ const conversation = await loadConversation(c, ctx);
722
+ if (!conversation) return notFound(c);
723
+ const result = await aiConversations.setConversationDone({
724
+ conversationId: conversation.id,
725
+ ownerUserId: ctx.ownerUserId,
726
+ done: c.req.valid("json").done,
727
+ });
728
+ if (!result.ok)
729
+ return result.reason === "active_turn"
730
+ ? respond(c, fail(err.conflict("Stop the current response before marking this chat done.")))
731
+ : notFound(c);
732
+ return respond(c, ok(await publicConversationFor(result.conversation, c.get("accessSubject"))));
733
+ })
686
734
  .post("/conversations/:conversationId/archive", async (c) => {
687
735
  const ctx = await resolveContext(c);
688
736
  if (ctx instanceof Response) return ctx;
@@ -720,6 +768,43 @@ export const aiRoutes = (() => {
720
768
  });
721
769
  return viewed ? respond(c, ok({ ok: true })) : notFound(c);
722
770
  })
771
+ .get("/conversations/:conversationId/queue", async (c) => {
772
+ const ctx = await resolveContext(c);
773
+ if (ctx instanceof Response) return ctx;
774
+ const conversation = await loadConversation(c, ctx);
775
+ return conversation ? respond(c, ok(await listQueuedMessages(conversation.id))) : notFound(c);
776
+ })
777
+ .patch(
778
+ "/conversations/:conversationId/queue/:messageId",
779
+ v("json", z.object({ text: z.string().trim().min(1).max(20000) }).strict()),
780
+ async (c) => {
781
+ const ctx = await resolveContext(c);
782
+ if (ctx instanceof Response) return ctx;
783
+ const conversation = await loadConversation(c, ctx);
784
+ if (!conversation || !z.uuid().safeParse(c.req.param("messageId")).success) return notFound(c);
785
+ return (await editQueuedMessage(conversation.id, c.req.param("messageId") ?? "", c.req.valid("json").text))
786
+ ? respond(c, ok({ ok: true }))
787
+ : notFound(c);
788
+ },
789
+ )
790
+ .delete("/conversations/:conversationId/queue/:messageId", async (c) => {
791
+ const ctx = await resolveContext(c);
792
+ if (ctx instanceof Response) return ctx;
793
+ const conversation = await loadConversation(c, ctx);
794
+ if (!conversation || !z.uuid().safeParse(c.req.param("messageId")).success) return notFound(c);
795
+ if (!(await updateQueuedMessage(conversation.id, c.req.param("messageId") ?? "", "cancel"))) return notFound(c);
796
+ wakeAiMessageQueue(conversation.id);
797
+ return respond(c, ok({ ok: true }));
798
+ })
799
+ .post("/conversations/:conversationId/queue/:messageId/retry", async (c) => {
800
+ const ctx = await resolveContext(c);
801
+ if (ctx instanceof Response) return ctx;
802
+ const conversation = await loadConversation(c, ctx);
803
+ if (!conversation || !z.uuid().safeParse(c.req.param("messageId")).success) return notFound(c);
804
+ if (!(await updateQueuedMessage(conversation.id, c.req.param("messageId") ?? "", "retry"))) return notFound(c);
805
+ wakeAiMessageQueue(conversation.id);
806
+ return respond(c, ok({ ok: true }));
807
+ })
723
808
  .put("/conversations/:conversationId/draft", v("json", AiSaveConversationDraftInputSchema), async (c) => {
724
809
  const ctx = await resolveContext(c);
725
810
  if (ctx instanceof Response) return ctx;
@@ -749,6 +834,7 @@ export const aiRoutes = (() => {
749
834
  const conversation = await loadConversation(c, ctx);
750
835
  if (!conversation) return notFound(c);
751
836
  const body = c.req.valid("json");
837
+ if (body.queueId && (await queuedMessageAccepted(conversation.id, body.queueId))) return respond(c, ok({ queued: true }));
752
838
  if (conversation.draft.revision !== body.draftRevision) {
753
839
  return respond(c, fail(err.conflict("The conversation draft changed in another session.")));
754
840
  }
@@ -756,6 +842,7 @@ export const aiRoutes = (() => {
756
842
  const resourceParts = conversation.draft.content.filter((part) => part.type === "resource");
757
843
  const turnContent = conversation.draft.content.map((part) => {
758
844
  if (part.type === "text") return { type: "text" as const, text: part.text };
845
+ if (part.type === "project-file") return { type: "text" as const, text: `Project file: ${part.path}` };
759
846
  if (part.type === "file") {
760
847
  return { type: "attachment" as const, path: part.path, mediaType: part.mediaType, size: part.size };
761
848
  }
@@ -768,8 +855,8 @@ export const aiRoutes = (() => {
768
855
  const project = conversation.projectId ? await aiProjects.snapshot(conversation.projectId, c.get("accessSubject")) : null;
769
856
  if (conversation.projectId && !project) return respond(c, fail(err.notFound("Project")));
770
857
  try {
771
- const result = await submitAiChatTurn({
772
- assistantChat: true,
858
+ const submission = {
859
+ assistantChat: true as const,
773
860
  conversationId: conversation.id,
774
861
  chatId: conversation.shortId,
775
862
  input,
@@ -780,7 +867,7 @@ export const aiRoutes = (() => {
780
867
  modelPolicy: ctx.modelPolicy,
781
868
  project: project ?? undefined,
782
869
  clientToolIds: body.clientToolIds,
783
- toolSource: { kind: "default", appTools: true },
870
+ toolSource: { kind: "default" as const, appTools: true },
784
871
  toolApprovalContext: ctx.toolApprovalContext,
785
872
  expectedDraftRevision: body.draftRevision,
786
873
  expectedProjectId: conversation.projectId,
@@ -788,7 +875,13 @@ export const aiRoutes = (() => {
788
875
  expectedFiles: conversation.draft.content.flatMap((part) =>
789
876
  part.type === "file" ? [{ path: part.path, version: part.version }] : [],
790
877
  ),
791
- });
878
+ };
879
+ if (body.queueId) {
880
+ await enqueueChatMessage(body.queueId, await prepareAiChatTurn(submission), conversation.draft.content);
881
+ wakeAiMessageQueue(conversation.id);
882
+ return respond(c, ok({ queued: true }));
883
+ }
884
+ const result = await submitAiChatTurn(submission);
792
885
  rememberLastUsedModel(ctx.actor, result.turn.modelProfileId);
793
886
  return respond(
794
887
  c,
@@ -803,6 +896,11 @@ export const aiRoutes = (() => {
803
896
  201,
804
897
  );
805
898
  } catch (error) {
899
+ if (
900
+ error instanceof AiMessageQueueConflict ||
901
+ (error instanceof Error && error.message === "Queued messages must be sent first.")
902
+ )
903
+ return respond(c, fail(err.conflict(error.message)));
806
904
  return toAiErrorResponse(c, error);
807
905
  }
808
906
  })
@@ -832,7 +930,7 @@ export const aiRoutes = (() => {
832
930
  if (ctx instanceof Response) return ctx;
833
931
  const conversation = await loadConversation(c, ctx);
834
932
  if (!conversation) return notFound(c);
835
- const messageId = c.req.param("messageId");
933
+ const messageId = c.req.param("messageId") ?? "";
836
934
 
837
935
  const messages = await aiConversations.listMessages({ conversationId: conversation.id });
838
936
  const target = messages.find((m) => m.shortId === messageId);
@@ -881,6 +979,7 @@ export const aiRoutes = (() => {
881
979
  truncateFromSeq: target.seq,
882
980
  fileSnapshot: originalRunConfig?.kind !== "compact" ? originalRunConfig?.files : undefined,
883
981
  retrySourceTurnId: target.loopId ?? undefined,
982
+ clientToolIds: originalRunConfig?.kind !== "compact" ? originalRunConfig?.clientToolIds : undefined,
884
983
  });
885
984
  rememberLastUsedModel(ctx.actor, result.turn.modelProfileId);
886
985
  return respond(
@@ -904,7 +1003,7 @@ export const aiRoutes = (() => {
904
1003
  if (ctx instanceof Response) return ctx;
905
1004
  const conversation = await loadConversation(c, ctx);
906
1005
  if (!conversation) return notFound(c);
907
- const messageId = c.req.param("messageId");
1006
+ const messageId = c.req.param("messageId") ?? "";
908
1007
 
909
1008
  const messages = await aiConversations.listMessages({ conversationId: conversation.id });
910
1009
  const target = messages.find((m) => m.shortId === messageId);
@@ -1029,6 +1128,130 @@ export const aiRoutes = (() => {
1029
1128
  const files = await aiFileStore.list({ conversationId: conversation.id, prefix: c.req.valid("query").prefix ?? "/" });
1030
1129
  return respond(c, ok({ files, totalBytes: await aiFileStore.totalBytes(conversation.id) }));
1031
1130
  })
1131
+ .post("/conversations/:conversationId/dictations", bodyLimit({ maxSize: AI_AUDIO_MAX_BYTES + 65_536 }), async (c) => {
1132
+ const ctx = await resolveContext(c);
1133
+ if (ctx instanceof Response) return ctx;
1134
+ const conversation = await loadConversation(c, ctx);
1135
+ if (!conversation) return notFound(c);
1136
+ try {
1137
+ const form = await c.req.formData();
1138
+ const file = form.get("file");
1139
+ if (!(file instanceof File)) return respond(c, fail(err.badInput("Attach one audio recording.")));
1140
+ const options = AiDictationStartSchema.parse({
1141
+ operationId: form.get("operationId"),
1142
+ language: form.get("language") ?? undefined,
1143
+ });
1144
+ const dictation = await aiDictations.start({
1145
+ ...options,
1146
+ conversationId: conversation.id,
1147
+ userId: ctx.ownerUserId,
1148
+ bytes: new Uint8Array(await file.arrayBuffer()),
1149
+ resolveModel: async () => {
1150
+ const model = await resolveAiAudioModel({ allowedDataBoundaries: ctx.modelPolicy.allowedDataBoundaries });
1151
+ await aiModelAccess.assertAllowed(model.profile.id, c.get("accessSubject"));
1152
+ return model;
1153
+ },
1154
+ });
1155
+ await enqueueAiDictation(dictation.id).catch(() => undefined); // The DB sweep heals failed delivery.
1156
+ return respond(c, ok(dictation));
1157
+ } catch (error) {
1158
+ return respond(
1159
+ c,
1160
+ fail(
1161
+ error instanceof AiDictationConflict
1162
+ ? err.conflict(error.message)
1163
+ : err.badInput(error instanceof Error ? error.message : "Recording upload failed."),
1164
+ ),
1165
+ );
1166
+ }
1167
+ })
1168
+ .post("/conversations/:conversationId/dictations/discard", v("json", z.object({ operationId: z.uuid() })), async (c) => {
1169
+ const ctx = await resolveContext(c);
1170
+ if (ctx instanceof Response) return ctx;
1171
+ const conversation = await loadConversation(c, ctx);
1172
+ if (!conversation) return notFound(c);
1173
+ return respond(
1174
+ c,
1175
+ ok(
1176
+ await aiDictations.discardOperation({
1177
+ conversationId: conversation.id,
1178
+ userId: ctx.ownerUserId,
1179
+ operationId: c.req.valid("json").operationId,
1180
+ }),
1181
+ ),
1182
+ );
1183
+ })
1184
+ .get("/conversations/:conversationId/dictations", v("query", AiDictationListSchema), async (c) => {
1185
+ const ctx = await resolveContext(c);
1186
+ if (ctx instanceof Response) return ctx;
1187
+ const conversation = await loadConversation(c, ctx);
1188
+ if (!conversation) return notFound(c);
1189
+ return respond(
1190
+ c,
1191
+ ok(await aiDictations.list({ ...c.req.valid("query"), conversationId: conversation.id, userId: ctx.ownerUserId })),
1192
+ );
1193
+ })
1194
+ .get("/conversations/:conversationId/dictations/:dictationId", async (c) => {
1195
+ const ctx = await resolveContext(c);
1196
+ if (ctx instanceof Response) return ctx;
1197
+ const conversation = await loadConversation(c, ctx);
1198
+ if (!conversation) return notFound(c);
1199
+ const result = await aiDictations.get({
1200
+ conversationId: conversation.id,
1201
+ userId: ctx.ownerUserId,
1202
+ id: z.string().regex(AI_SHORT_ID_PATTERN).parse(c.req.param("dictationId")),
1203
+ });
1204
+ return result ? respond(c, ok(result)) : notFound(c);
1205
+ })
1206
+ .post("/conversations/:conversationId/dictations/:dictationId/apply", v("json", AiSaveConversationDraftInputSchema), async (c) => {
1207
+ const ctx = await resolveContext(c);
1208
+ if (ctx instanceof Response) return ctx;
1209
+ const conversation = await loadConversation(c, ctx);
1210
+ if (!conversation) return notFound(c);
1211
+ try {
1212
+ return respond(
1213
+ c,
1214
+ ok(
1215
+ await aiDictations.apply({
1216
+ ...c.req.valid("json"),
1217
+ conversationId: conversation.id,
1218
+ userId: ctx.ownerUserId,
1219
+ id: z.string().regex(AI_SHORT_ID_PATTERN).parse(c.req.param("dictationId")),
1220
+ }),
1221
+ ),
1222
+ );
1223
+ } catch (error) {
1224
+ return respond(
1225
+ c,
1226
+ fail(
1227
+ error instanceof AiDictationConflict
1228
+ ? err.conflict(error.message)
1229
+ : err.badInput(error instanceof Error ? error.message : "Dictation could not be inserted."),
1230
+ ),
1231
+ );
1232
+ }
1233
+ })
1234
+ .post(
1235
+ "/conversations/:conversationId/dictations/:dictationId/action",
1236
+ v("json", z.object({ action: z.enum(["discard", "retry"]) })),
1237
+ async (c) => {
1238
+ const ctx = await resolveContext(c);
1239
+ if (ctx instanceof Response) return ctx;
1240
+ const conversation = await loadConversation(c, ctx);
1241
+ if (!conversation) return notFound(c);
1242
+ const input = {
1243
+ conversationId: conversation.id,
1244
+ userId: ctx.ownerUserId,
1245
+ id: z.string().regex(AI_SHORT_ID_PATTERN).parse(c.req.param("dictationId")),
1246
+ };
1247
+ if (c.req.valid("json").action === "discard") await aiDictations.discard(input);
1248
+ else {
1249
+ await aiDictations.retry(input);
1250
+ await enqueueAiDictation(input.id).catch(() => undefined);
1251
+ }
1252
+ return respond(c, ok({ success: true }));
1253
+ },
1254
+ )
1032
1255
  .post("/conversations/:conversationId/files", async (c) => {
1033
1256
  const ctx = await resolveContext(c);
1034
1257
  if (ctx instanceof Response) return ctx;
@@ -1042,7 +1265,9 @@ export const aiRoutes = (() => {
1042
1265
  return respond(c, fail(err.badInput(`File exceeds the ${Math.floor(AI_FILES_MAX_FILE_BYTES_DEFAULT / (1024 * 1024))} MB limit`)));
1043
1266
  }
1044
1267
  const name = (file.name || "upload").replaceAll("/", "_").replaceAll("\\", "_").replaceAll("\0", "").slice(0, 160) || "upload";
1045
- const path = normalizeAiFilePath(`/${name}`);
1268
+ const directory = form?.get("directory") ?? "/";
1269
+ if (typeof directory !== "string") return respond(c, fail(err.badInput("Invalid directory")));
1270
+ const path = normalizeAiFilePath(`${directory.replace(/\/$/, "")}/${name}`);
1046
1271
  if (!path) return respond(c, fail(err.badInput("Invalid file name")));
1047
1272
  if (aiProjectFilePathFromMount(path) !== null) return respond(c, fail(err.badInput("The /project namespace is reserved.")));
1048
1273
 
@@ -1055,6 +1280,11 @@ export const aiRoutes = (() => {
1055
1280
  });
1056
1281
  return respond(c, ok({ file: stat }));
1057
1282
  } catch (error) {
1283
+ logger("ai:files").warn("Conversation upload failed", {
1284
+ code: "file_upload_failed",
1285
+ conversationId: conversation.id,
1286
+ errorType: error instanceof Error ? error.name : "unknown",
1287
+ });
1058
1288
  return respond(c, fail(err.badInput(error instanceof Error ? error.message : "Upload failed")));
1059
1289
  }
1060
1290
  })
@@ -0,0 +1,12 @@
1
+ /** Preserve why execution stopped instead of reporting an operator deadline as a user abort. */
2
+ export class AiRunTimeout extends Error {
3
+ constructor(readonly budgetMs: number | null) {
4
+ super("AI_RUN_TIMEOUT");
5
+ }
6
+ messageFor(locale?: string): string {
7
+ const minutes = this.budgetMs ? this.budgetMs / 60_000 : null;
8
+ return locale?.startsWith("de")
9
+ ? `Laufzeitlimit${minutes ? ` von ${minutes} Minuten` : ""} erreicht. Du kannst die Aufgabe mit einer neuen Nachricht fortsetzen.`
10
+ : `Run time limit${minutes ? ` of ${minutes} minutes` : ""} reached. You can continue the task with a new message.`;
11
+ }
12
+ }
@@ -1,12 +1,19 @@
1
+ export { CloudAiTranscribeAudioInputSchema, CloudAiTranscribeAudioOutputSchema, createCloudAiTranscribeAudioTool } from "./audio-tool";
1
2
  export {
2
3
  CLOUD_AI_DEFERRED_BUILTIN_TOOL_NAMES,
3
4
  createCloudAiCardTool,
5
+ createCloudAiCodeTools,
4
6
  createCloudAiLocalBashTool,
5
7
  createCloudAiSurveyTool,
6
8
  createCloudAiTextEditorTool,
7
9
  createConfiguredDefaultCloudAiTools,
8
10
  createDefaultCloudAiTools,
9
11
  } from "./default-tools";
12
+ export {
13
+ CloudAiFetchFileInputSchema,
14
+ CloudAiFetchFileOutputSchema,
15
+ createCloudAiFetchFileTool,
16
+ } from "./fetch-file-tool";
10
17
  export {
11
18
  CloudAiCalculateInputSchema,
12
19
  CloudAiCalculateOutputSchema,
@@ -26,11 +33,6 @@ export {
26
33
  evaluateAiDate,
27
34
  evaluateAiMath,
28
35
  } from "./file-tools";
29
- export {
30
- CloudAiFetchFileInputSchema,
31
- CloudAiFetchFileOutputSchema,
32
- createCloudAiFetchFileTool,
33
- } from "./fetch-file-tool";
34
36
  export {
35
37
  CloudAiMarkdownToPdfInputSchema,
36
38
  CloudAiMarkdownToPdfOutputSchema,