@k2b/cloud 0.6.0-rc.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 (540) hide show
  1. package/README.md +98 -0
  2. package/package.json +128 -0
  3. package/public/logo.svg +1 -0
  4. package/scripts/app-favicon.test.ts +49 -0
  5. package/scripts/app-favicon.ts +51 -0
  6. package/scripts/build.ts +205 -0
  7. package/scripts/preload.ts +164 -0
  8. package/scripts/sync-recovery-smoke.ts +200 -0
  9. package/src/_internal/app-favicon.ts +2 -0
  10. package/src/_internal/app-presentation.ts +61 -0
  11. package/src/_internal/app-workspace-state.ts +24 -0
  12. package/src/_internal/bounded-json.ts +47 -0
  13. package/src/_internal/build-metadata.ts +9 -0
  14. package/src/_internal/capabilities.ts +1118 -0
  15. package/src/_internal/define-app.ts +801 -0
  16. package/src/_internal/heartbeat.ts +167 -0
  17. package/src/_internal/help-catalog.ts +241 -0
  18. package/src/_internal/help.ts +95 -0
  19. package/src/_internal/nats-connection.ts +34 -0
  20. package/src/_internal/page-responses.ts +40 -0
  21. package/src/_internal/process-sync.ts +117 -0
  22. package/src/_internal/readiness.ts +3 -0
  23. package/src/_internal/registry-validation.ts +136 -0
  24. package/src/_internal/registry-watch.ts +52 -0
  25. package/src/_internal/registry.ts +338 -0
  26. package/src/_internal/runtime-compatibility.ts +64 -0
  27. package/src/_internal/runtime-context.ts +84 -0
  28. package/src/_internal/runtime-watcher.ts +78 -0
  29. package/src/_internal/static-assets.ts +86 -0
  30. package/src/_internal/status-preserving-ssr.ts +49 -0
  31. package/src/access/PermissionEditor.tsx +486 -0
  32. package/src/access/ResourceApiKeys.tsx +271 -0
  33. package/src/access/messages.ts +121 -0
  34. package/src/access/ui.ts +16 -0
  35. package/src/account/Avatar.tsx +28 -0
  36. package/src/account/EntitySearch.tsx +363 -0
  37. package/src/account/avatar-upload-dialog.tsx +197 -0
  38. package/src/account/avatar-upload.ts +149 -0
  39. package/src/account/ui.ts +3 -0
  40. package/src/ai/admin.ts +47 -0
  41. package/src/ai/approval-routes.ts +130 -0
  42. package/src/ai/approvals.ts +139 -0
  43. package/src/ai/assistant-models.ts +61 -0
  44. package/src/ai/attachments.ts +40 -0
  45. package/src/ai/browser.ts +92 -0
  46. package/src/ai/capabilities.ts +714 -0
  47. package/src/ai/capability-execution.ts +151 -0
  48. package/src/ai/chat/blocks.render.test.tsx +1108 -0
  49. package/src/ai/chat/blocks.tsx +730 -0
  50. package/src/ai/chat/builtin-tools.tsx +428 -0
  51. package/src/ai/chat/capability-table.tsx +87 -0
  52. package/src/ai/chat/composer-adapter.ts +169 -0
  53. package/src/ai/chat/file-tools.tsx +81 -0
  54. package/src/ai/chat/message-actions.tsx +370 -0
  55. package/src/ai/chat/message-utils.ts +479 -0
  56. package/src/ai/chat/messages.ts +43 -0
  57. package/src/ai/chat/presentation.tsx +395 -0
  58. package/src/ai/chat/primitives.tsx +3 -0
  59. package/src/ai/chat/tool-disclosure.tsx +41 -0
  60. package/src/ai/chat/turn-navigator-utils.ts +28 -0
  61. package/src/ai/chat/turn-navigator.tsx +164 -0
  62. package/src/ai/chat/user-message.tsx +149 -0
  63. package/src/ai/chat/visual-tools.tsx +435 -0
  64. package/src/ai/chat/web-tools.tsx +202 -0
  65. package/src/ai/chat-task-contracts.ts +113 -0
  66. package/src/ai/chat-tasks.ts +952 -0
  67. package/src/ai/client/controller.ts +1211 -0
  68. package/src/ai/client/file-source.ts +95 -0
  69. package/src/ai/client/live-connection.ts +140 -0
  70. package/src/ai/client/projection.ts +178 -0
  71. package/src/ai/client/transport.ts +111 -0
  72. package/src/ai/compaction.ts +182 -0
  73. package/src/ai/credentials.ts +124 -0
  74. package/src/ai/default-tool-contracts.ts +108 -0
  75. package/src/ai/default-tools.ts +127 -0
  76. package/src/ai/enrich.ts +280 -0
  77. package/src/ai/executor.ts +1624 -0
  78. package/src/ai/fetch-file-tool.ts +313 -0
  79. package/src/ai/file-context.ts +92 -0
  80. package/src/ai/file-media-type.ts +28 -0
  81. package/src/ai/file-mount.ts +16 -0
  82. package/src/ai/file-tools.ts +566 -0
  83. package/src/ai/files-store.ts +390 -0
  84. package/src/ai/firecrawl-tools.ts +305 -0
  85. package/src/ai/grids-skill.ts +126 -0
  86. package/src/ai/http.ts +265 -0
  87. package/src/ai/index.ts +337 -0
  88. package/src/ai/kit-skill.ts +22 -0
  89. package/src/ai/limits.ts +5 -0
  90. package/src/ai/live-events.ts +289 -0
  91. package/src/ai/live-messages.ts +45 -0
  92. package/src/ai/live-outbox.ts +102 -0
  93. package/src/ai/live-routes.ts +412 -0
  94. package/src/ai/live.ts +2 -0
  95. package/src/ai/maintenance.ts +302 -0
  96. package/src/ai/markdown-pdf-tool.ts +95 -0
  97. package/src/ai/memories.ts +605 -0
  98. package/src/ai/memory-learning-runs.ts +222 -0
  99. package/src/ai/memory-learning.ts +619 -0
  100. package/src/ai/memory-tool.ts +120 -0
  101. package/src/ai/memory-workflow-evidence.ts +158 -0
  102. package/src/ai/migrate.ts +2082 -0
  103. package/src/ai/model-access-migrate.ts +30 -0
  104. package/src/ai/model-access.ts +191 -0
  105. package/src/ai/personal-agent.ts +6 -0
  106. package/src/ai/prefs.ts +72 -0
  107. package/src/ai/project-tool.ts +94 -0
  108. package/src/ai/projects-routes.ts +277 -0
  109. package/src/ai/projects.ts +902 -0
  110. package/src/ai/protocol.ts +279 -0
  111. package/src/ai/provider.ts +73 -0
  112. package/src/ai/public-projection.ts +25 -0
  113. package/src/ai/resource-markers.ts +31 -0
  114. package/src/ai/resource-refs.ts +70 -0
  115. package/src/ai/routes.ts +1131 -0
  116. package/src/ai/runtime-tools.ts +39 -0
  117. package/src/ai/runtime.ts +544 -0
  118. package/src/ai/settings.ts +529 -0
  119. package/src/ai/short-id.ts +45 -0
  120. package/src/ai/skill-catalog.ts +73 -0
  121. package/src/ai/skill-format.ts +145 -0
  122. package/src/ai/skill-seeds.ts +430 -0
  123. package/src/ai/skill-tool.ts +81 -0
  124. package/src/ai/skills-routes.ts +143 -0
  125. package/src/ai/skills.ts +934 -0
  126. package/src/ai/solid.ts +27 -0
  127. package/src/ai/store.ts +3113 -0
  128. package/src/ai/stream.ts +308 -0
  129. package/src/ai/structured-runs.ts +66 -0
  130. package/src/ai/structured.ts +186 -0
  131. package/src/ai/system-prompt.ts +170 -0
  132. package/src/ai/task-prompt.ts +22 -0
  133. package/src/ai/timeline.ts +148 -0
  134. package/src/ai/tool-audit.ts +177 -0
  135. package/src/ai/tool-result-budget.ts +16 -0
  136. package/src/ai/tools.ts +155 -0
  137. package/src/ai/types.ts +967 -0
  138. package/src/ai/ui.tsx +39 -0
  139. package/src/ai/usage-migrate.ts +81 -0
  140. package/src/ai/usage.ts +315 -0
  141. package/src/ai/validate.ts +54 -0
  142. package/src/ai/vision-tool.ts +79 -0
  143. package/src/api/accounts-entities.ts +174 -0
  144. package/src/api/admin-ai-projects.ts +74 -0
  145. package/src/api/admin-ai-skills.ts +76 -0
  146. package/src/api/admin-ai-usage.ts +42 -0
  147. package/src/api/admin-core-settings.ts +508 -0
  148. package/src/api/admin-identity.ts +148 -0
  149. package/src/api/admin-lifecycle.ts +206 -0
  150. package/src/api/admin-linux-identities.ts +66 -0
  151. package/src/api/announcements.ts +131 -0
  152. package/src/api/app-approval.ts +129 -0
  153. package/src/api/apps.ts +51 -0
  154. package/src/api/auth/schemas.ts +54 -0
  155. package/src/api/auth.ts +370 -0
  156. package/src/api/capabilities.ts +658 -0
  157. package/src/api/help-messages.ts +19 -0
  158. package/src/api/help.ts +78 -0
  159. package/src/api/index.ts +108 -0
  160. package/src/api/mcp.ts +744 -0
  161. package/src/api/me-rail.ts +45 -0
  162. package/src/api/me.ts +753 -0
  163. package/src/api/search/schemas.ts +79 -0
  164. package/src/api/search.bench.ts +185 -0
  165. package/src/api/search.ts +407 -0
  166. package/src/api/sync-ops.ts +79 -0
  167. package/src/api/widgets.ts +143 -0
  168. package/src/browser/CloudResourceSearch.behavior.test.tsx +74 -0
  169. package/src/browser/CloudResourceSearch.tsx +791 -0
  170. package/src/browser/app-approval-vault.ts +305 -0
  171. package/src/browser/app-approval.ts +234 -0
  172. package/src/browser/live-websocket.ts +268 -0
  173. package/src/browser/locale-preference.ts +25 -0
  174. package/src/browser/notification-target.ts +27 -0
  175. package/src/browser/notification-timeout.ts +14 -0
  176. package/src/browser/notifications.ts +181 -0
  177. package/src/browser/resource-clipboard.ts +70 -0
  178. package/src/browser/resource-picker.tsx +39 -0
  179. package/src/browser/resource-search-messages.ts +55 -0
  180. package/src/browser/resource-search.ts +27 -0
  181. package/src/capabilities/client.ts +149 -0
  182. package/src/capabilities/response.ts +39 -0
  183. package/src/capabilities/server.ts +215 -0
  184. package/src/capabilities/signals.ts +4 -0
  185. package/src/capabilities/testing.ts +12 -0
  186. package/src/capabilities/types.ts +32 -0
  187. package/src/cli/access.ts +499 -0
  188. package/src/cli/account.ts +382 -0
  189. package/src/cli/admin/account-administration.ts +45 -0
  190. package/src/cli/admin/account-categories.ts +49 -0
  191. package/src/cli/admin/ai-usage.ts +118 -0
  192. package/src/cli/admin/app-sign-in.ts +49 -0
  193. package/src/cli/admin/data.ts +343 -0
  194. package/src/cli/admin/documentation.ts +34 -0
  195. package/src/cli/admin/gateway.ts +155 -0
  196. package/src/cli/admin/index.ts +61 -0
  197. package/src/cli/admin/instance.ts +250 -0
  198. package/src/cli/admin/jobs.ts +223 -0
  199. package/src/cli/admin/legal.ts +122 -0
  200. package/src/cli/admin/linux.ts +53 -0
  201. package/src/cli/admin/logs.ts +324 -0
  202. package/src/cli/admin/metrics.ts +142 -0
  203. package/src/cli/admin/nats.ts +167 -0
  204. package/src/cli/admin/notifications.ts +516 -0
  205. package/src/cli/admin/shared.ts +113 -0
  206. package/src/cli/admin/sync.ts +188 -0
  207. package/src/cli/admin/telemetry.ts +314 -0
  208. package/src/cli/admin/webhooks.ts +222 -0
  209. package/src/cli/admin/workflows.ts +367 -0
  210. package/src/cli/apps.ts +40 -0
  211. package/src/cli/capabilities.ts +239 -0
  212. package/src/cli/commands.ts +604 -0
  213. package/src/cli/index.ts +61 -0
  214. package/src/cli/locale.ts +31 -0
  215. package/src/cli/output.ts +58 -0
  216. package/src/clients/core.ts +20 -0
  217. package/src/config/env.ts +54 -0
  218. package/src/config/index.ts +6 -0
  219. package/src/contracts/account-categories.ts +62 -0
  220. package/src/contracts/announcements.ts +121 -0
  221. package/src/contracts/app-approval.ts +197 -0
  222. package/src/contracts/app.ts +162 -0
  223. package/src/contracts/capabilities.ts +484 -0
  224. package/src/contracts/index.ts +14 -0
  225. package/src/contracts/notification-live.ts +66 -0
  226. package/src/contracts/notification-types.ts +235 -0
  227. package/src/contracts/posix.ts +69 -0
  228. package/src/contracts/profile.ts +85 -0
  229. package/src/contracts/rail-preferences.ts +53 -0
  230. package/src/contracts/registry.ts +110 -0
  231. package/src/contracts/resource-clipboard.ts +33 -0
  232. package/src/contracts/settings-types.ts +98 -0
  233. package/src/contracts/shared.ts +408 -0
  234. package/src/contracts/user-notifications.ts +101 -0
  235. package/src/contracts/widgets.ts +183 -0
  236. package/src/desktop/index.ts +704 -0
  237. package/src/desktop/solid.tsx +938 -0
  238. package/src/index.ts +58 -0
  239. package/src/server/actor.ts +41 -0
  240. package/src/server/api/index.ts +2 -0
  241. package/src/server/api/respond.ts +96 -0
  242. package/src/server/api-client.ts +54 -0
  243. package/src/server/app-context.ts +39 -0
  244. package/src/server/help.ts +187 -0
  245. package/src/server/index.ts +89 -0
  246. package/src/server/locale.ts +55 -0
  247. package/src/server/middleware/auth.ts +386 -0
  248. package/src/server/middleware/index.ts +28 -0
  249. package/src/server/middleware/invocation.ts +55 -0
  250. package/src/server/middleware/middleware.ts +65 -0
  251. package/src/server/middleware/openapi.ts +126 -0
  252. package/src/server/middleware/rate-limit.ts +125 -0
  253. package/src/server/middleware/request-logger.ts +41 -0
  254. package/src/server/middleware/route-template.ts +46 -0
  255. package/src/server/middleware/runtime.ts +22 -0
  256. package/src/server/middleware/settings.ts +55 -0
  257. package/src/server/middleware/validator.ts +49 -0
  258. package/src/server/middleware/workload.ts +22 -0
  259. package/src/server/ratelimit.ts +151 -0
  260. package/src/server/request-cookies.ts +18 -0
  261. package/src/server/services/access.ts +644 -0
  262. package/src/server/services/freeipa/client.ts +137 -0
  263. package/src/server/services/freeipa/index.ts +9 -0
  264. package/src/server/services/freeipa/session.ts +123 -0
  265. package/src/server/services/freeipa/test-certificates.ts +44 -0
  266. package/src/server/services/freeipa/tls.ts +44 -0
  267. package/src/server/services/freeipa/transport.ts +116 -0
  268. package/src/server/services/freeipa/util.ts +59 -0
  269. package/src/server/services/geo.ts +198 -0
  270. package/src/server/services/index.ts +48 -0
  271. package/src/server/services/pagination.ts +22 -0
  272. package/src/server/services/services.ts +13 -0
  273. package/src/server/time.ts +33 -0
  274. package/src/services/account-category-policy.ts +51 -0
  275. package/src/services/account-lifecycle/audit.ts +41 -0
  276. package/src/services/account-lifecycle/index.ts +940 -0
  277. package/src/services/account-lifecycle/ipa-backfill.ts +152 -0
  278. package/src/services/account-lifecycle/notification-sender.ts +13 -0
  279. package/src/services/account-lifecycle/scheduler.ts +506 -0
  280. package/src/services/account-model.ts +22 -0
  281. package/src/services/accounts/app.ts +1671 -0
  282. package/src/services/accounts/authz.ts +44 -0
  283. package/src/services/accounts/avatar.ts +120 -0
  284. package/src/services/accounts/base-group.ts +11 -0
  285. package/src/services/accounts/base-user.ts +46 -0
  286. package/src/services/accounts/duplicate-emails.ts +82 -0
  287. package/src/services/accounts/entities.ts +622 -0
  288. package/src/services/accounts/group-sql.ts +106 -0
  289. package/src/services/accounts/groups.ts +272 -0
  290. package/src/services/accounts/index.ts +14 -0
  291. package/src/services/accounts/ipa-data.ts +66 -0
  292. package/src/services/accounts/lifecycle.ts +2 -0
  293. package/src/services/accounts/local-groups.ts +499 -0
  294. package/src/services/accounts/model.ts +129 -0
  295. package/src/services/accounts/notification-sender.ts +31 -0
  296. package/src/services/accounts/posix.ts +386 -0
  297. package/src/services/accounts/request-policy.ts +11 -0
  298. package/src/services/accounts/switching.ts +126 -0
  299. package/src/services/accounts/users.ts +745 -0
  300. package/src/services/announcements/index.ts +224 -0
  301. package/src/services/app-approval.ts +498 -0
  302. package/src/services/audit/index.ts +438 -0
  303. package/src/services/auth-flows/index.ts +15 -0
  304. package/src/services/auth-flows/ipa.ts +197 -0
  305. package/src/services/auth-flows/magic-link.ts +185 -0
  306. package/src/services/auth-flows/notification-sender.ts +10 -0
  307. package/src/services/auth-flows/password-reset.ts +268 -0
  308. package/src/services/auth-flows/proxy-return.ts +49 -0
  309. package/src/services/document-extraction.ts +148 -0
  310. package/src/services/freeipa-config.ts +179 -0
  311. package/src/services/gateway.ts +186 -0
  312. package/src/services/identity/constants.ts +18 -0
  313. package/src/services/identity/index.ts +52 -0
  314. package/src/services/identity/invocation-actor.ts +120 -0
  315. package/src/services/identity/invocation-authority.ts +53 -0
  316. package/src/services/identity/invocation-operations.ts +15 -0
  317. package/src/services/identity/invocation-token.ts +276 -0
  318. package/src/services/identity/key-config.ts +41 -0
  319. package/src/services/identity/key-ring.ts +530 -0
  320. package/src/services/identity/metrics.ts +29 -0
  321. package/src/services/identity/routes.ts +18 -0
  322. package/src/services/identity/runtime-config.ts +88 -0
  323. package/src/services/identity/session-token.ts +152 -0
  324. package/src/services/identity/signer-refresh.ts +44 -0
  325. package/src/services/identity/workload-auth.ts +64 -0
  326. package/src/services/index.ts +187 -0
  327. package/src/services/ipa/auth.ts +216 -0
  328. package/src/services/ipa/connection.ts +57 -0
  329. package/src/services/ipa/effective-groups.ts +70 -0
  330. package/src/services/ipa/groups.ts +722 -0
  331. package/src/services/ipa/guard.ts +17 -0
  332. package/src/services/ipa/index.ts +17 -0
  333. package/src/services/ipa/posix.ts +19 -0
  334. package/src/services/ipa/profile.ts +132 -0
  335. package/src/services/ipa/runtime.ts +28 -0
  336. package/src/services/ipa/search.ts +154 -0
  337. package/src/services/ipa/service-account.ts +15 -0
  338. package/src/services/ipa/sync-planning.ts +100 -0
  339. package/src/services/ipa/sync.ts +862 -0
  340. package/src/services/ipa/users.ts +806 -0
  341. package/src/services/legal-consent.ts +76 -0
  342. package/src/services/logging/index.ts +477 -0
  343. package/src/services/logging/redaction.ts +20 -0
  344. package/src/services/logging/trace.ts +1231 -0
  345. package/src/services/mandates/index.ts +855 -0
  346. package/src/services/mandates/policy.ts +85 -0
  347. package/src/services/network-security.ts +31 -0
  348. package/src/services/notifications/batches.ts +722 -0
  349. package/src/services/notifications/browser.ts +252 -0
  350. package/src/services/notifications/catalog.ts +113 -0
  351. package/src/services/notifications/channels.ts +104 -0
  352. package/src/services/notifications/dispatcher.ts +139 -0
  353. package/src/services/notifications/email.ts +118 -0
  354. package/src/services/notifications/index.ts +633 -0
  355. package/src/services/notifications/live.ts +50 -0
  356. package/src/services/notifications/observability.ts +407 -0
  357. package/src/services/notifications/platform.ts +421 -0
  358. package/src/services/notifications/runtime.ts +120 -0
  359. package/src/services/notifications/user.ts +248 -0
  360. package/src/services/notifications/web-push-transport.ts +135 -0
  361. package/src/services/oauth-tokens.ts +209 -0
  362. package/src/services/outbox.ts +187 -0
  363. package/src/services/pdf/gotenberg.ts +290 -0
  364. package/src/services/pdf/index.ts +43 -0
  365. package/src/services/pdf/markdown.ts +209 -0
  366. package/src/services/pdf/template-preview.ts +122 -0
  367. package/src/services/postgres.ts +76 -0
  368. package/src/services/providers/index.ts +27 -0
  369. package/src/services/providers/local/auth.ts +51 -0
  370. package/src/services/providers/local/index.ts +4 -0
  371. package/src/services/providers/local/users.ts +263 -0
  372. package/src/services/rail-preferences.ts +27 -0
  373. package/src/services/runtime-lifecycle.ts +191 -0
  374. package/src/services/secrets.ts +10 -0
  375. package/src/services/service-account-credentials.ts +891 -0
  376. package/src/services/service-accounts.ts +184 -0
  377. package/src/services/session/index.ts +236 -0
  378. package/src/services/session/test-fixture.ts +18 -0
  379. package/src/services/session/user.ts +144 -0
  380. package/src/services/settings/api.ts +61 -0
  381. package/src/services/settings/app.ts +86 -0
  382. package/src/services/settings/core-settings.ts +796 -0
  383. package/src/services/settings/crypto.ts +69 -0
  384. package/src/services/settings/defaults.ts +374 -0
  385. package/src/services/settings/index.ts +215 -0
  386. package/src/services/settings/namespace.ts +9 -0
  387. package/src/services/settings/snapshot.ts +49 -0
  388. package/src/services/settings/store.ts +241 -0
  389. package/src/services/settings/templates.ts +4 -0
  390. package/src/services/sync-ops.ts +403 -0
  391. package/src/services/topic-cursor.ts +22 -0
  392. package/src/services/weather/forecast.ts +323 -0
  393. package/src/services/weather/geo.ts +119 -0
  394. package/src/services/weather/index.ts +67 -0
  395. package/src/services/weather/location.ts +24 -0
  396. package/src/services/weather/locations.ts +133 -0
  397. package/src/services/weather/migrate.ts +45 -0
  398. package/src/services/weather/settings.ts +66 -0
  399. package/src/services/weather/short-id.ts +57 -0
  400. package/src/services/weather/types.ts +61 -0
  401. package/src/services/weather/ui.ts +50 -0
  402. package/src/services/webauthn.ts +385 -0
  403. package/src/shared/account-action-notice.ts +70 -0
  404. package/src/shared/account-display.ts +15 -0
  405. package/src/shared/account-session.ts +15 -0
  406. package/src/shared/ai-platform-prompt.ts +134 -0
  407. package/src/shared/ai-usage.ts +49 -0
  408. package/src/shared/app-presentation.ts +48 -0
  409. package/src/shared/app-url.ts +12 -0
  410. package/src/shared/branding.ts +14 -0
  411. package/src/shared/capability-messages.ts +70 -0
  412. package/src/shared/email-html.ts +54 -0
  413. package/src/shared/format.ts +91 -0
  414. package/src/shared/help.ts +68 -0
  415. package/src/shared/icons.ts +400 -0
  416. package/src/shared/index.ts +54 -0
  417. package/src/shared/locale.ts +48 -0
  418. package/src/shared/login-method.ts +44 -0
  419. package/src/shared/markdown/client.ts +130 -0
  420. package/src/shared/markdown/extensions/code.ts +105 -0
  421. package/src/shared/markdown/extensions/guided-help.ts +100 -0
  422. package/src/shared/markdown/extensions/images.ts +79 -0
  423. package/src/shared/markdown/extensions/info-blocks.ts +97 -0
  424. package/src/shared/markdown/extensions/katex.ts +120 -0
  425. package/src/shared/markdown/extensions/links.ts +43 -0
  426. package/src/shared/markdown/extensions/mark.ts +48 -0
  427. package/src/shared/markdown/extensions/sub-sup.ts +60 -0
  428. package/src/shared/markdown/extensions/tables.ts +109 -0
  429. package/src/shared/markdown/extensions/task-list.ts +53 -0
  430. package/src/shared/markdown/formula.ts +1187 -0
  431. package/src/shared/markdown/index.ts +193 -0
  432. package/src/shared/markdown/shared.ts +36 -0
  433. package/src/shared/mock-cover.ts +130 -0
  434. package/src/shared/network-address.ts +106 -0
  435. package/src/shared/redirect.ts +56 -0
  436. package/src/shared/template-rendering.ts +140 -0
  437. package/src/shared/theme.ts +68 -0
  438. package/src/shared/time.ts +13 -0
  439. package/src/ssr/AdminLayout.tsx +53 -0
  440. package/src/ssr/AdminSidebar.tsx +50 -0
  441. package/src/ssr/AppLaunchpad.island.tsx +236 -0
  442. package/src/ssr/AppWorkspaceController.island.tsx +21 -0
  443. package/src/ssr/BrowserNotifications.island.tsx +135 -0
  444. package/src/ssr/GlobalAnnouncements.island.tsx +142 -0
  445. package/src/ssr/GlobalSearchDialog.behavior.test.tsx +96 -0
  446. package/src/ssr/GlobalSearchDialog.tsx +35 -0
  447. package/src/ssr/GlobalSearchHelpDialog.tsx +104 -0
  448. package/src/ssr/GlobalSearchTrigger.island.tsx +55 -0
  449. package/src/ssr/HotkeysHelpRail.island.tsx +55 -0
  450. package/src/ssr/Layout.render.test.tsx +148 -0
  451. package/src/ssr/Layout.tsx +292 -0
  452. package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
  453. package/src/ssr/LayoutFooter.tsx +22 -0
  454. package/src/ssr/LayoutHeader.tsx +81 -0
  455. package/src/ssr/LayoutHelp.tsx +869 -0
  456. package/src/ssr/LayoutPreferences.island.tsx +36 -0
  457. package/src/ssr/LayoutRail.tsx +58 -0
  458. package/src/ssr/MinimalLayout.tsx +46 -0
  459. package/src/ssr/PageError.tsx +29 -0
  460. package/src/ssr/ProfilePreferences.island.tsx +92 -0
  461. package/src/ssr/RailApps.island.tsx +61 -0
  462. package/src/ssr/RailEditor.tsx +293 -0
  463. package/src/ssr/RegisteredHelpDocuments.island.tsx +3 -0
  464. package/src/ssr/TimezoneCookie.island.tsx +23 -0
  465. package/src/ssr/admin-active-link.ts +47 -0
  466. package/src/ssr/admin-navigation.ts +78 -0
  467. package/src/ssr/app-appearance.ts +23 -0
  468. package/src/ssr/app-navigation.ts +41 -0
  469. package/src/ssr/help-messages.ts +104 -0
  470. package/src/ssr/index.ts +14 -0
  471. package/src/ssr/islands/SearchBar.island.tsx +80 -0
  472. package/src/ssr/islands/index.ts +16 -0
  473. package/src/ssr/layout-context.ts +17 -0
  474. package/src/ssr/layout-help-markdown.ts +9 -0
  475. package/src/ssr/layout-help-navigation.ts +24 -0
  476. package/src/ssr/layout-help-url.ts +5 -0
  477. package/src/ssr/layout-runtime.ts +22 -0
  478. package/src/ssr/page-error-messages.ts +25 -0
  479. package/src/ssr/platform-messages.ts +135 -0
  480. package/src/ssr/preference-controller.ts +44 -0
  481. package/src/ssr/profile-preferences-messages.ts +29 -0
  482. package/src/ssr/rail-context.ts +21 -0
  483. package/src/ssr/rail-messages.ts +66 -0
  484. package/src/ssr/rail-navigation.ts +60 -0
  485. package/src/ssr/runtime.ts +31 -0
  486. package/src/ssr/url-filter.ts +127 -0
  487. package/src/styles/base-popover.css +36 -0
  488. package/src/styles/effects.css +851 -0
  489. package/src/styles/global.css +268 -0
  490. package/src/styles/input.css +58 -0
  491. package/src/styles/tokens.css +230 -0
  492. package/src/styles/utilities-buttons.css +405 -0
  493. package/src/styles/utilities-data.css +33 -0
  494. package/src/styles/utilities-feedback.css +232 -0
  495. package/src/styles/utilities-layout.css +603 -0
  496. package/src/styles/utilities-markdown-editor.css +525 -0
  497. package/src/styles/utilities-markdown-table.css +234 -0
  498. package/src/styles/utilities-navigation.css +1054 -0
  499. package/src/types/ambient.d.ts +24 -0
  500. package/src/workflows/ai/index.ts +17 -0
  501. package/src/workflows/ai/runtime.ts +286 -0
  502. package/src/workflows/ai/store.ts +314 -0
  503. package/src/workflows/ai/types.ts +111 -0
  504. package/src/workflows/ai-actions.ts +269 -0
  505. package/src/workflows/authoring.ts +88 -0
  506. package/src/workflows/builtins.ts +193 -0
  507. package/src/workflows/contracts.ts +280 -0
  508. package/src/workflows/definition.ts +334 -0
  509. package/src/workflows/editor.ts +113 -0
  510. package/src/workflows/index.ts +71 -0
  511. package/src/workflows/language/binder.ts +45 -0
  512. package/src/workflows/language/canonical.ts +29 -0
  513. package/src/workflows/language/compiler.ts +396 -0
  514. package/src/workflows/language/expressions.ts +34 -0
  515. package/src/workflows/language/index.ts +20 -0
  516. package/src/workflows/language/references.ts +55 -0
  517. package/src/workflows/language/schema.ts +134 -0
  518. package/src/workflows/language/strict-yaml.ts +131 -0
  519. package/src/workflows/manifest.ts +24 -0
  520. package/src/workflows/module.ts +12 -0
  521. package/src/workflows/runtime/coordinator.ts +115 -0
  522. package/src/workflows/runtime/executor.ts +1003 -0
  523. package/src/workflows/runtime/index.ts +60 -0
  524. package/src/workflows/runtime/instant.ts +8 -0
  525. package/src/workflows/runtime/ports.ts +213 -0
  526. package/src/workflows/runtime/schedule.ts +203 -0
  527. package/src/workflows/runtime/trigger.ts +37 -0
  528. package/src/workflows/runtime/wake.ts +13 -0
  529. package/src/workflows/store/actions.ts +438 -0
  530. package/src/workflows/store/budget.ts +145 -0
  531. package/src/workflows/store/definitions.ts +345 -0
  532. package/src/workflows/store/events.ts +383 -0
  533. package/src/workflows/store/index.ts +107 -0
  534. package/src/workflows/store/observability.ts +594 -0
  535. package/src/workflows/store/runs.ts +975 -0
  536. package/src/workflows/store/transaction.ts +23 -0
  537. package/src/workflows/store/worker.ts +439 -0
  538. package/src/workflows/testing/conformance.ts +72 -0
  539. package/src/workflows/testing/index.ts +11 -0
  540. package/src/workflows/testing/process-fixtures.ts +275 -0
@@ -0,0 +1,1131 @@
1
+ import { type Context, Hono } from "hono";
2
+ import { z } from "zod";
3
+ import { listCapabilities } from "../_internal/registry";
4
+ import {
5
+ type AccessSubject,
6
+ type ApiErrorResponse,
7
+ type AuthContext,
8
+ auth,
9
+ err,
10
+ fail,
11
+ getLocale,
12
+ ok,
13
+ type RequestActor,
14
+ rateLimit,
15
+ respond,
16
+ v,
17
+ } from "../server";
18
+ import { coreSettings } from "../services/settings/api";
19
+ import type { AiToolApprovalContext } from "./approvals";
20
+ import { assistantAiSettingsState, listAssistantAiModels, selectAssistantAiModelId } from "./assistant-models";
21
+ import { buildAiCapabilityCatalog } from "./capabilities";
22
+ import { createConfiguredDefaultCloudAiTools } from "./default-tools";
23
+ import { aiProjectFilePathFromMount } from "./file-mount";
24
+ import { AI_FILES_MAX_FILE_BYTES_DEFAULT, aiFileStore, decodeAiFileContent, guessAiMediaType, normalizeAiFilePath } from "./files-store";
25
+ import {
26
+ AiCompactionInputSchema,
27
+ AiCreateConversationInputSchema,
28
+ AiInitialConversationDraftInputSchema,
29
+ AiMessageFeedbackInputSchema,
30
+ AiMessageForkInputSchema,
31
+ AiMessageRetryInputSchema,
32
+ AiSaveConversationDraftInputSchema,
33
+ AiSteerInputSchema,
34
+ AiSubmitConversationDraftInputSchema,
35
+ aiInputToUserMessage,
36
+ aiTurnInputToContent,
37
+ toAiActionFailureResponse,
38
+ toAiErrorResponse,
39
+ } from "./http";
40
+ import { aiMaintenanceJobs } from "./maintenance";
41
+ import { AI_MEMORY_CONTENT_MAX_CHARS, aiMemories } from "./memories";
42
+ import { aiMemoryLearningRuns } from "./memory-learning-runs";
43
+ import { createCloudAiMemoryTool } from "./memory-tool";
44
+ import { personalAiModelPolicy } from "./personal-agent";
45
+ import { aiActorUser, aiPrefsUserId, aiUserPrefs } from "./prefs";
46
+ import { aiProjects } from "./projects";
47
+ import { projectPublicAiStoredMessages, publicAiStoredMessages } from "./public-projection";
48
+ import { aiResourceMarker } from "./resource-markers";
49
+ import { isConversationResourceCursor } from "./resource-refs";
50
+ import {
51
+ AiTurnActionSchema,
52
+ abortAiTurn,
53
+ listPendingAiTurnActions,
54
+ submitAiChatTurn,
55
+ submitAiCompaction,
56
+ submitAiTurnAction,
57
+ } from "./runtime";
58
+ import { readAiSettingsState, selectAiModelProfile } from "./settings";
59
+ import { AI_SHORT_ID_PATTERN } from "./short-id";
60
+ import { selectAiSkillCatalog } from "./skill-catalog";
61
+ import { createCloudAiLoadSkillTool, createCloudAiSearchSkillsTool } from "./skill-tool";
62
+ import { aiSkills } from "./skills";
63
+ import { aiConversations } from "./store";
64
+ import { createAiConversationStreamResponse, loadAiStreamState } from "./stream";
65
+ import { composeAiSystemPrompt } from "./system-prompt";
66
+ import { aiToolPromptHints } from "./tools";
67
+ import type { AiConversation, AiModelPolicy, AiTurn } from "./types";
68
+
69
+ /** Everything a resolved, authorized request needs to run against the shared runtime. */
70
+ type AiChatRequestContext = {
71
+ actor: RequestActor;
72
+ ownerUserId: string;
73
+ modelPolicy: AiModelPolicy;
74
+ toolApprovalContext: AiToolApprovalContext;
75
+ };
76
+
77
+ const actorUser = (actor: RequestActor) => (actor.kind === "user" ? actor.user : actor.delegatedUser);
78
+
79
+ const resolveContext = async (c: Context<AuthContext>): Promise<AiChatRequestContext | ApiErrorResponse> => {
80
+ const actor = c.get("actor");
81
+ const user = actorUser(actor);
82
+ if (!user) return respond(c, fail(err.forbidden("AI conversations require a user-backed actor.")));
83
+ return {
84
+ actor,
85
+ ownerUserId: user.id,
86
+ modelPolicy: personalAiModelPolicy,
87
+ toolApprovalContext: { actorUserId: user.id },
88
+ };
89
+ };
90
+
91
+ const retryInstruction = (mode: "retry" | "details" | "concise"): string | null => {
92
+ if (mode === "details") return "Answer the user's request again with more detail and specificity.";
93
+ if (mode === "concise") return "Answer the user's request again more concisely.";
94
+ return null;
95
+ };
96
+
97
+ const ConversationListQuerySchema = z
98
+ .object({
99
+ q: z.string().trim().max(200).optional(),
100
+ limit: z.coerce.number().int().min(1).max(50).optional(),
101
+ archived: z
102
+ .enum(["true", "false"])
103
+ .transform((value) => value === "true")
104
+ .optional(),
105
+ status: z.enum(["running", "needs_attention", "failed", "unread"]).optional(),
106
+ projectId: z.string().regex(AI_SHORT_ID_PATTERN).optional(),
107
+ unassigned: z
108
+ .enum(["true", "false"])
109
+ .transform((value) => value === "true")
110
+ .optional(),
111
+ })
112
+ .refine((input) => !(input.projectId && input.unassigned), "Choose a Project or unassigned chats, not both.");
113
+
114
+ const ConversationPageQuerySchema = ConversationListQuerySchema.extend({
115
+ page: z.coerce.number().int().min(1).optional(),
116
+ perPage: z.coerce.number().int().min(1).max(50).optional(),
117
+ });
118
+
119
+ const ConversationMetadataInputSchema = z.object({
120
+ title: z.string().trim().min(1).max(120),
121
+ description: z.string().trim().max(500).optional(),
122
+ pinned: z.boolean().optional(),
123
+ });
124
+
125
+ const ConversationProjectInputSchema = z.object({
126
+ projectId: z.string().regex(AI_SHORT_ID_PATTERN).nullable(),
127
+ });
128
+
129
+ const MessagesPageQuerySchema = z.object({
130
+ before: z.coerce.number().int().min(1).optional(),
131
+ limit: z.coerce.number().int().min(1).max(200).optional(),
132
+ });
133
+
134
+ const MessagesSearchQuerySchema = MessagesPageQuerySchema.extend({ q: z.string().trim().min(1).max(500) });
135
+ const resourcesQuerySchema = (scope: "conversation" | "user") =>
136
+ z.object({
137
+ q: z.string().trim().max(500).optional(),
138
+ cursor: z
139
+ .string()
140
+ .min(1)
141
+ .max(2_048)
142
+ .refine((value) => isConversationResourceCursor(value, scope), "Invalid resource cursor")
143
+ .optional(),
144
+ limit: z.coerce.number().int().min(1).max(100).optional(),
145
+ });
146
+ const ConversationResourcesQuerySchema = resourcesQuerySchema("conversation");
147
+ const UserResourcesQuerySchema = resourcesQuerySchema("user");
148
+
149
+ const FilesListQuerySchema = z.object({ prefix: z.string().optional() });
150
+ const FilePathQuerySchema = z.object({ path: z.string().min(1) });
151
+ const FileWriteSchema = z.object({
152
+ path: z.string().min(1),
153
+ content: z.string().max(12_000_000),
154
+ encoding: z.enum(["utf8", "base64"]).default("utf8"),
155
+ });
156
+ const FileRenameSchema = z.object({ from: z.string().min(1), to: z.string().min(1) });
157
+
158
+ const AiUserPrefsInputSchema = z.object({
159
+ memoryEnabled: z.boolean().optional(),
160
+ memoryLearningEnabled: z.boolean().optional(),
161
+ });
162
+
163
+ const MemoryListQuerySchema = z.object({
164
+ q: z.string().trim().max(200).optional(),
165
+ limit: z.coerce.number().int().min(1).max(50).optional(),
166
+ });
167
+ const MemoryLearningRunsQuerySchema = z.object({
168
+ page: z.coerce.number().int().min(1).optional(),
169
+ perPage: z.coerce.number().int().min(1).max(50).optional(),
170
+ });
171
+ const MemoryCreateSchema = z.object({
172
+ kind: z.enum(["fact", "preference"]),
173
+ content: z.string().trim().min(1).max(AI_MEMORY_CONTENT_MAX_CHARS),
174
+ priority: z.enum(["normal", "pinned"]).optional(),
175
+ });
176
+ const MemoryUpdateSchema = MemoryCreateSchema.partial().refine((value) => Object.keys(value).length > 0, "Pass a changed field.");
177
+ const MemoryIdSchema = z.string().regex(AI_SHORT_ID_PATTERN);
178
+
179
+ const notFound = (c: Context<AuthContext>) => respond(c, fail(err.notFound("Conversation")));
180
+
181
+ const publicConversation = (conversation: AiConversation, projectId: string | null = null) => ({
182
+ ...conversation,
183
+ id: conversation.shortId,
184
+ projectId: conversation.projectId ? projectId : null,
185
+ });
186
+ const publicConversations = async (conversations: AiConversation[], subject: AccessSubject) => {
187
+ const projectIds = conversations.flatMap((conversation) => (conversation.projectId ? [conversation.projectId] : []));
188
+ const shortIds = await aiProjects.resolveShortIds(projectIds, subject);
189
+ return conversations.map((conversation) =>
190
+ publicConversation(conversation, conversation.projectId ? (shortIds.get(conversation.projectId) ?? null) : null),
191
+ );
192
+ };
193
+ const publicConversationFor = async (conversation: AiConversation, subject: AccessSubject) =>
194
+ (await publicConversations([conversation], subject))[0]!;
195
+ const publicMemories = async (userId: string, memories: Awaited<ReturnType<typeof aiMemories.list>>) => {
196
+ const sourceIds = memories.flatMap((memory) => (memory.sourceConversationId ? [memory.sourceConversationId] : []));
197
+ const sourceShortIds = await aiMemories.resolveSourceConversationShortIds(userId, sourceIds);
198
+ return memories.map((memory) => ({
199
+ ...memory,
200
+ id: memory.shortId,
201
+ sourceConversationId: memory.sourceConversationId ? (sourceShortIds.get(memory.sourceConversationId) ?? null) : null,
202
+ }));
203
+ };
204
+ const publicMemory = async (userId: string, memory: Awaited<ReturnType<typeof aiMemories.create>>) =>
205
+ (await publicMemories(userId, [memory]))[0]!;
206
+
207
+ /** Fire-and-forget: remember the model this user actually ran a turn with (preselected for new chats). */
208
+ const rememberLastUsedModel = (actor: RequestActor, modelProfileId: string | null | undefined): void => {
209
+ const userId = aiPrefsUserId(actor);
210
+ if (!userId || !modelProfileId) return;
211
+ void aiUserPrefs.update(userId, { lastModelId: modelProfileId }).catch(() => undefined);
212
+ };
213
+
214
+ const conversationDetail = async (conversation: AiConversation, subject: AccessSubject) => {
215
+ const [state, timeline] = await Promise.all([
216
+ loadAiStreamState(conversation),
217
+ aiConversations.listConversationTimeline({ conversationId: conversation.id }),
218
+ ]);
219
+ return {
220
+ conversation: await publicConversationFor(conversation, subject),
221
+ messages: state.messages.map((message) => ({ ...message, id: message.shortId })),
222
+ hasMoreMessages: state.hasMoreMessages ?? false,
223
+ activeTurn: state.activeTurn,
224
+ timeline,
225
+ };
226
+ };
227
+
228
+ const publicTurn = (turn: AiTurn, conversationId: string): AiTurn => ({ ...turn, id: turn.shortId, conversationId });
229
+ const publicEnrichmentRun = (run: Awaited<ReturnType<typeof aiConversations.listEnrichmentRuns>>[number], conversationId: string) => ({
230
+ ...run,
231
+ id: `${conversationId}:${run.createdAt}`,
232
+ conversationId,
233
+ });
234
+ const publicPendingAction = (
235
+ action: Awaited<ReturnType<typeof listPendingAiTurnActions>>[number],
236
+ conversationId: string,
237
+ turnId: string,
238
+ ) => ({ ...action, conversationId, turnId });
239
+
240
+ const prepareConversationForMessageRetry = async (conversationId: string): Promise<"ready" | "busy"> => {
241
+ const active = await aiConversations.getActiveTurn({ conversationId });
242
+ if (!active) return "ready";
243
+ if (active.turn.status !== "waiting_for_action") return "busy";
244
+ await abortAiTurn({ conversationId, turnId: active.turn.id });
245
+ return (await aiConversations.getActiveTurn({ conversationId })) ? "busy" : "ready";
246
+ };
247
+
248
+ const prepareLaunchDraft = async (body: z.infer<typeof AiCreateConversationInputSchema>, subject: AccessSubject | null) => {
249
+ const content = [...(body.draft?.content ?? [])];
250
+ if (body.skills?.length && body.allowedTools && !body.allowedTools.includes("load_skill")) {
251
+ return fail(err.badInput("Linked Skills require load_skill in allowedTools."));
252
+ }
253
+ for (const name of new Set(body.skills ?? [])) {
254
+ const skill = await aiSkills.getByName(name, subject);
255
+ if (!skill?.enabled) return fail(err.badInput(`Skill is unavailable: ${name}`));
256
+ if (!content.some((part) => part.type === "resource" && part.ref.type === "core.ai.skill" && part.ref.id === skill.shortId)) {
257
+ content.push({ type: "resource", ref: { type: "core.ai.skill", id: skill.shortId }, title: skill.name, icon: "ti ti-sparkles" });
258
+ }
259
+ }
260
+ const draft = AiInitialConversationDraftInputSchema.safeParse({ content });
261
+ return draft.success ? ok(draft.data) : fail(err.badInput("Too many draft attachments or content parts."));
262
+ };
263
+
264
+ export const __aiRoutesTest = { prepareConversationForMessageRetry, prepareLaunchDraft };
265
+
266
+ export const aiRoutes = (() => {
267
+ const loadConversation = async (c: Context<AuthContext>, ctx: AiChatRequestContext, archived = false): Promise<AiConversation | null> => {
268
+ const conversationId = c.req.param("conversationId");
269
+ if (!conversationId) return null;
270
+ return aiConversations.getConversationByShortId({
271
+ shortId: conversationId,
272
+ ownerUserId: ctx.ownerUserId,
273
+ archived,
274
+ });
275
+ };
276
+
277
+ const loadTurn = (conversation: AiConversation, shortId: string | undefined): Promise<AiTurn | null> =>
278
+ shortId ? aiConversations.getTurnByShortId({ conversationId: conversation.id, shortId }) : Promise.resolve(null);
279
+
280
+ return (
281
+ new Hono<AuthContext>()
282
+ .use(rateLimit())
283
+ .use("*", auth.requireRole("authenticated"))
284
+ .get("/status", async (c) => {
285
+ return respond(c, ok(await assistantAiSettingsState(c.get("accessSubject"))));
286
+ })
287
+ .get("/models", async (c) => {
288
+ return respond(c, ok(await listAssistantAiModels(c.get("accessSubject"))));
289
+ })
290
+ .get("/prefs", async (c) => {
291
+ const ctx = await resolveContext(c);
292
+ if (ctx instanceof Response) return ctx;
293
+ const userId = aiPrefsUserId(ctx.actor);
294
+ if (!userId) return respond(c, fail(err.forbidden("AI preferences require a user context.")));
295
+ return respond(c, ok(await aiUserPrefs.get(userId)));
296
+ })
297
+ .put("/prefs", v("json", AiUserPrefsInputSchema), async (c) => {
298
+ const ctx = await resolveContext(c);
299
+ if (ctx instanceof Response) return ctx;
300
+ const userId = aiPrefsUserId(ctx.actor);
301
+ if (!userId) return respond(c, fail(err.forbidden("AI preferences require a user context.")));
302
+ return respond(c, ok(await aiUserPrefs.update(userId, c.req.valid("json"))));
303
+ })
304
+ .get("/memories", v("query", MemoryListQuerySchema), async (c) => {
305
+ const ctx = await resolveContext(c);
306
+ if (ctx instanceof Response) return ctx;
307
+ const userId = aiPrefsUserId(ctx.actor);
308
+ if (!userId) return respond(c, fail(err.forbidden("AI memories require a user context.")));
309
+ const query = c.req.valid("query");
310
+ return respond(c, ok(await publicMemories(userId, await aiMemories.list({ userId, query: query.q, limit: query.limit }))));
311
+ })
312
+ .get("/memory-learning-runs", v("query", MemoryLearningRunsQuerySchema), async (c) => {
313
+ const ctx = await resolveContext(c);
314
+ if (ctx instanceof Response) return ctx;
315
+ const userId = aiPrefsUserId(ctx.actor);
316
+ if (!userId) return respond(c, fail(err.forbidden("AI personalization activity requires a user context.")));
317
+ return respond(c, ok(await aiMemoryLearningRuns.list({ userId, ...c.req.valid("query") })));
318
+ })
319
+ .post("/memories", v("json", MemoryCreateSchema), async (c) => {
320
+ const ctx = await resolveContext(c);
321
+ if (ctx instanceof Response) return ctx;
322
+ const userId = aiPrefsUserId(ctx.actor);
323
+ if (!userId) return respond(c, fail(err.forbidden("AI memories require a user context.")));
324
+ const body = c.req.valid("json");
325
+ return respond(
326
+ c,
327
+ ok(await publicMemory(userId, await aiMemories.create({ userId, ...body, priority: body.priority ?? "pinned", source: "user" }))),
328
+ );
329
+ })
330
+ .patch("/memories/:memoryId", v("json", MemoryUpdateSchema), async (c) => {
331
+ const ctx = await resolveContext(c);
332
+ if (ctx instanceof Response) return ctx;
333
+ const userId = aiPrefsUserId(ctx.actor);
334
+ if (!userId) return respond(c, fail(err.forbidden("AI memories require a user context.")));
335
+ const memoryId = MemoryIdSchema.safeParse(c.req.param("memoryId"));
336
+ if (!memoryId.success) return respond(c, fail(err.badInput("Invalid memory id.")));
337
+ const memory = await aiMemories.updateByShortId(userId, memoryId.data, { ...c.req.valid("json"), source: "user" });
338
+ return memory ? respond(c, ok(await publicMemory(userId, memory))) : respond(c, fail(err.notFound("Memory")));
339
+ })
340
+ .delete("/memories/:memoryId", async (c) => {
341
+ const ctx = await resolveContext(c);
342
+ if (ctx instanceof Response) return ctx;
343
+ const userId = aiPrefsUserId(ctx.actor);
344
+ if (!userId) return respond(c, fail(err.forbidden("AI memories require a user context.")));
345
+ const memoryId = MemoryIdSchema.safeParse(c.req.param("memoryId"));
346
+ if (!memoryId.success) return respond(c, fail(err.badInput("Invalid memory id.")));
347
+ return (await aiMemories.deleteByShortId(userId, memoryId.data))
348
+ ? respond(c, ok({ deleted: true }))
349
+ : respond(c, fail(err.notFound("Memory")));
350
+ })
351
+ .get("/prefs/system-prompt", async (c) => {
352
+ const ctx = await resolveContext(c);
353
+ if (ctx instanceof Response) return ctx;
354
+ const user = aiActorUser(ctx.actor);
355
+ if (!user) return respond(c, fail(err.forbidden("AI preferences require a user context.")));
356
+
357
+ const prefs = await aiUserPrefs.get(user.id);
358
+ const memoryEnabled = Boolean(prefs?.memoryEnabled);
359
+ const memory = memoryEnabled ? await aiMemories.selectHot(user.id, "") : null;
360
+ const state = await readAiSettingsState();
361
+ let previewProfile: ReturnType<typeof selectAiModelProfile> | null = null;
362
+ if (state.ok && state.enabled) {
363
+ const models = await listAssistantAiModels(c.get("accessSubject"), ctx.modelPolicy);
364
+ const preferredId =
365
+ models.find((model) => model.id === prefs?.lastModelId)?.id ??
366
+ models.find((model) => model.id === state.defaultModelId)?.id ??
367
+ models[0]?.id;
368
+ if (preferredId) previewProfile = selectAiModelProfile(state, ctx.modelPolicy, preferredId);
369
+ }
370
+ const toolsSupported = Boolean(previewProfile?.capabilities.includes("tools"));
371
+ const availableSkills = toolsSupported ? (await aiSkills.list(c.get("accessSubject"))).filter((skill) => skill.enabled) : [];
372
+ const skillCatalog = selectAiSkillCatalog(availableSkills, previewProfile?.contextWindow ?? 0, "");
373
+ const tools = toolsSupported
374
+ ? [
375
+ ...(await createConfiguredDefaultCloudAiTools()),
376
+ ...(memoryEnabled ? [createCloudAiMemoryTool()] : []),
377
+ ...(availableSkills.length ? [createCloudAiLoadSkillTool(c.get("accessSubject"))] : []),
378
+ ...(skillCatalog.omitted > 0 ? [createCloudAiSearchSkillsTool(c.get("accessSubject"))] : []),
379
+ ]
380
+ : [];
381
+ const memoryToolEnabled = tools.some((tool) => tool.def.name === "memory");
382
+ const timeZone = String((await coreSettings.get<string>("app.timezone")) || "").trim() || "UTC";
383
+ const promptLocale = getLocale(c);
384
+ const prompt = composeAiSystemPrompt({
385
+ globalInstructions: state.globalInstructions,
386
+ user,
387
+ memoryEnabled,
388
+ memoryToolEnabled,
389
+ helpEnabled: toolsSupported,
390
+ toolDiscoveryEnabled: toolsSupported,
391
+ appToolsEnabled: toolsSupported,
392
+ toolHints: aiToolPromptHints(tools),
393
+ skills: skillCatalog.skills,
394
+ omittedSkillCount: skillCatalog.omitted,
395
+ memory: memory?.text,
396
+ timeZone,
397
+ locale: promptLocale,
398
+ });
399
+ return respond(c, ok({ prompt, renderedAt: new Date().toISOString() }));
400
+ })
401
+ .get("/resources", v("query", UserResourcesQuerySchema), async (c) => {
402
+ const ctx = await resolveContext(c);
403
+ if (ctx instanceof Response) return ctx;
404
+ const query = c.req.valid("query");
405
+ return respond(
406
+ c,
407
+ ok(
408
+ await aiConversations.listUserConversationResources({
409
+ ownerUserId: ctx.ownerUserId,
410
+ search: query.q,
411
+ before: query.cursor,
412
+ limit: query.limit,
413
+ }),
414
+ ),
415
+ );
416
+ })
417
+ .get("/conversations", v("query", ConversationListQuerySchema), async (c) => {
418
+ const ctx = await resolveContext(c);
419
+ if (ctx instanceof Response) return ctx;
420
+ const query = c.req.valid("query");
421
+ const project = query.projectId ? await aiProjects.getByShortId(query.projectId, c.get("accessSubject"), "read") : null;
422
+ if (query.projectId && !project) return respond(c, fail(err.notFound("Project")));
423
+ return respond(
424
+ c,
425
+ ok(
426
+ await publicConversations(
427
+ await aiConversations.listConversations({
428
+ ownerUserId: ctx.ownerUserId,
429
+ search: query.q,
430
+ archived: query.archived,
431
+ status: query.status,
432
+ projectId: project?.id,
433
+ unassigned: query.unassigned,
434
+ limit: query.limit,
435
+ }),
436
+ c.get("accessSubject"),
437
+ ),
438
+ ),
439
+ );
440
+ })
441
+ .get("/conversations/page", v("query", ConversationPageQuerySchema), async (c) => {
442
+ const ctx = await resolveContext(c);
443
+ if (ctx instanceof Response) return ctx;
444
+ const query = c.req.valid("query");
445
+ const project = query.projectId ? await aiProjects.getByShortId(query.projectId, c.get("accessSubject"), "read") : null;
446
+ if (query.projectId && !project) return respond(c, fail(err.notFound("Project")));
447
+ const page = await aiConversations.listConversationsPage({
448
+ ownerUserId: ctx.ownerUserId,
449
+ search: query.q,
450
+ archived: query.archived,
451
+ status: query.status,
452
+ projectId: project?.id,
453
+ unassigned: query.unassigned,
454
+ page: query.page ?? 1,
455
+ perPage: query.perPage ?? 20,
456
+ });
457
+ return respond(c, ok({ ...page, items: await publicConversations(page.items, c.get("accessSubject")) }));
458
+ })
459
+ .post("/conversations", v("json", AiCreateConversationInputSchema), async (c) => {
460
+ const ctx = await resolveContext(c);
461
+ if (ctx instanceof Response) return ctx;
462
+ const body = c.req.valid("json");
463
+ const project = body.projectId ? await aiProjects.getByShortId(body.projectId, c.get("accessSubject"), "read") : null;
464
+ if (body.projectId && !project) return respond(c, fail(err.notFound("Project")));
465
+ const draft = await prepareLaunchDraft(body, c.get("accessSubject"));
466
+ if (!draft.ok) return respond(c, draft);
467
+ const requestedCapabilityTools = (body.preloadTools ?? []).filter(
468
+ (requested): requested is { appId: string; kind: "query" | "action"; id: string } => "appId" in requested,
469
+ );
470
+ let catalog: ReturnType<typeof buildAiCapabilityCatalog> = [];
471
+ if (requestedCapabilityTools.length) {
472
+ try {
473
+ catalog = buildAiCapabilityCatalog(await listCapabilities());
474
+ } catch {
475
+ return respond(c, fail(err.internal("The live capability catalog is unavailable.")));
476
+ }
477
+ }
478
+ const builtInNames = new Set((await createConfiguredDefaultCloudAiTools()).map((tool) => tool.def.name));
479
+ const preloadTools = (body.preloadTools ?? []).map((requested) => {
480
+ if ("name" in requested) return builtInNames.has(requested.name) ? requested.name : null;
481
+ return (
482
+ catalog.find(
483
+ (candidate) =>
484
+ candidate.appId === requested.appId && candidate.kind === requested.kind && candidate.operation.localId === requested.id,
485
+ )?.name ?? null
486
+ );
487
+ });
488
+ const unavailable = preloadTools.findIndex((name) => !name);
489
+ if (unavailable >= 0) {
490
+ const requested = body.preloadTools![unavailable]!;
491
+ const label = "name" in requested ? requested.name : `${requested.appId}.${requested.id}`;
492
+ return respond(c, fail(err.badInput(`Tool is unavailable: ${label}`)));
493
+ }
494
+ if (body.allowedTools && preloadTools.some((name) => !body.allowedTools!.includes(name!))) {
495
+ return respond(c, fail(err.badInput("Preloaded tools must be included in allowedTools.")));
496
+ }
497
+ return respond(
498
+ c,
499
+ ok(
500
+ publicConversation(
501
+ await aiConversations.createConversation({
502
+ ownerUserId: ctx.ownerUserId,
503
+ title: body.title,
504
+ projectId: project?.id,
505
+ draft: draft.data.content,
506
+ preloadTools: preloadTools.map((name) => name!),
507
+ allowedTools: body.allowedTools,
508
+ launchedByAppId: body.launchedByAppId,
509
+ }),
510
+ project?.shortId ?? null,
511
+ ),
512
+ ),
513
+ 201,
514
+ );
515
+ })
516
+ .get("/conversations/:conversationId", async (c) => {
517
+ const ctx = await resolveContext(c);
518
+ if (ctx instanceof Response) return ctx;
519
+ const conversation = await loadConversation(c, ctx);
520
+ if (!conversation) return notFound(c);
521
+ return respond(c, ok(await conversationDetail(conversation, c.get("accessSubject"))));
522
+ })
523
+ .get("/conversations/:conversationId/messages", v("query", MessagesPageQuerySchema), async (c) => {
524
+ const ctx = await resolveContext(c);
525
+ if (ctx instanceof Response) return ctx;
526
+ const conversation = await loadConversation(c, ctx);
527
+ if (!conversation) return notFound(c);
528
+ const query = c.req.valid("query");
529
+ const page = await aiConversations.listMessagesPage({
530
+ conversationId: conversation.id,
531
+ beforeSeq: query.before,
532
+ limit: query.limit ?? 50,
533
+ });
534
+ return respond(c, ok({ ...page, messages: await publicAiStoredMessages(page.messages, conversation) }));
535
+ })
536
+ .get("/conversations/:conversationId/messages/search", v("query", MessagesSearchQuerySchema), async (c) => {
537
+ const ctx = await resolveContext(c);
538
+ if (ctx instanceof Response) return ctx;
539
+ const conversation = await loadConversation(c, ctx);
540
+ if (!conversation) return notFound(c);
541
+ const query = c.req.valid("query");
542
+ const page = await aiConversations.searchConversationMessages({
543
+ conversationId: conversation.id,
544
+ query: query.q,
545
+ beforeSeq: query.before,
546
+ limit: query.limit ?? 50,
547
+ });
548
+ return respond(c, ok({ ...page, messages: await publicAiStoredMessages(page.messages, conversation) }));
549
+ })
550
+ .put("/conversations/:conversationId/messages/:messageId/feedback", v("json", AiMessageFeedbackInputSchema), async (c) => {
551
+ const ctx = await resolveContext(c);
552
+ if (ctx instanceof Response) return ctx;
553
+ const conversation = await loadConversation(c, ctx);
554
+ if (!conversation) return notFound(c);
555
+ const messageId = c.req.param("messageId");
556
+ if (!messageId) return respond(c, fail(err.badInput("Invalid message id.")));
557
+ const input = c.req.valid("json");
558
+ const feedback = await aiConversations.setMessageFeedback({
559
+ conversationId: conversation.id,
560
+ messageShortId: messageId,
561
+ feedback: {
562
+ rating: input.rating,
563
+ reasons: input.reasons,
564
+ comment: input.comment ?? null,
565
+ },
566
+ });
567
+ return feedback ? respond(c, ok({ feedback })) : respond(c, fail(err.notFound("Assistant message")));
568
+ })
569
+ .delete("/conversations/:conversationId/messages/:messageId/feedback", async (c) => {
570
+ const ctx = await resolveContext(c);
571
+ if (ctx instanceof Response) return ctx;
572
+ const conversation = await loadConversation(c, ctx);
573
+ if (!conversation) return notFound(c);
574
+ const messageId = c.req.param("messageId");
575
+ if (!messageId) return respond(c, fail(err.badInput("Invalid message id.")));
576
+ return (await aiConversations.clearMessageFeedback({
577
+ conversationId: conversation.id,
578
+ messageShortId: messageId,
579
+ }))
580
+ ? respond(c, ok({ deleted: true }))
581
+ : respond(c, fail(err.notFound("Assistant message")));
582
+ })
583
+ .get("/conversations/:conversationId/resources", v("query", ConversationResourcesQuerySchema), async (c) => {
584
+ const ctx = await resolveContext(c);
585
+ if (ctx instanceof Response) return ctx;
586
+ const conversation = await loadConversation(c, ctx);
587
+ if (!conversation) return notFound(c);
588
+ return respond(
589
+ c,
590
+ ok(
591
+ await aiConversations.listConversationResources({
592
+ conversationId: conversation.id,
593
+ search: c.req.valid("query").q,
594
+ before: c.req.valid("query").cursor,
595
+ limit: c.req.valid("query").limit,
596
+ }),
597
+ ),
598
+ );
599
+ })
600
+ .get("/conversations/:conversationId/sources", v("query", ConversationResourcesQuerySchema), async (c) => {
601
+ const ctx = await resolveContext(c);
602
+ if (ctx instanceof Response) return ctx;
603
+ const conversation = await loadConversation(c, ctx);
604
+ if (!conversation) return notFound(c);
605
+ const query = c.req.valid("query");
606
+ return respond(
607
+ c,
608
+ ok(
609
+ await aiConversations.listConversationSources({
610
+ conversationId: conversation.id,
611
+ search: query.q,
612
+ before: query.cursor,
613
+ limit: query.limit,
614
+ }),
615
+ ),
616
+ );
617
+ })
618
+ .get("/conversations/:conversationId/timeline", async (c) => {
619
+ const ctx = await resolveContext(c);
620
+ if (ctx instanceof Response) return ctx;
621
+ const conversation = await loadConversation(c, ctx);
622
+ if (!conversation) return notFound(c);
623
+ return respond(c, ok(await aiConversations.listConversationTimeline({ conversationId: conversation.id })));
624
+ })
625
+ .patch("/conversations/:conversationId", v("json", ConversationMetadataInputSchema), async (c) => {
626
+ const ctx = await resolveContext(c);
627
+ if (ctx instanceof Response) return ctx;
628
+ const conversation = await loadConversation(c, ctx);
629
+ if (!conversation) return notFound(c);
630
+ const body = c.req.valid("json");
631
+ const updated = await aiConversations.updateConversationMetadata({
632
+ conversationId: conversation.id,
633
+ ownerUserId: ctx.ownerUserId,
634
+ title: body.title,
635
+ description: body.description,
636
+ pinned: body.pinned,
637
+ });
638
+ if (!updated) return notFound(c);
639
+ return respond(c, ok(await publicConversationFor(updated, c.get("accessSubject"))));
640
+ })
641
+ .put("/conversations/:conversationId/project", v("json", ConversationProjectInputSchema), async (c) => {
642
+ const ctx = await resolveContext(c);
643
+ if (ctx instanceof Response) return ctx;
644
+ const conversation = await loadConversation(c, ctx);
645
+ if (!conversation) return notFound(c);
646
+ const body = c.req.valid("json");
647
+ const project = body.projectId ? await aiProjects.getByShortId(body.projectId, c.get("accessSubject"), "read") : null;
648
+ if (body.projectId && !project) return respond(c, fail(err.notFound("Project")));
649
+ const result = await aiConversations.setConversationProject({
650
+ conversationId: conversation.id,
651
+ ownerUserId: ctx.ownerUserId,
652
+ projectId: project?.id ?? null,
653
+ });
654
+ if (!result.ok) {
655
+ if (result.reason === "active_turn") {
656
+ return respond(c, fail(err.conflict("Wait for the active turn before changing the Project.")));
657
+ }
658
+ return notFound(c);
659
+ }
660
+ return respond(c, ok(await publicConversationFor(result.conversation, c.get("accessSubject"))));
661
+ })
662
+ .post("/conversations/:conversationId/pin", async (c) => {
663
+ const ctx = await resolveContext(c);
664
+ if (ctx instanceof Response) return ctx;
665
+ const conversation = await loadConversation(c, ctx);
666
+ if (!conversation) return notFound(c);
667
+ const updated = await aiConversations.setConversationPinned({
668
+ conversationId: conversation.id,
669
+ ownerUserId: ctx.ownerUserId,
670
+ pinned: true,
671
+ });
672
+ return updated ? respond(c, ok(await publicConversationFor(updated, c.get("accessSubject")))) : notFound(c);
673
+ })
674
+ .delete("/conversations/:conversationId/pin", async (c) => {
675
+ const ctx = await resolveContext(c);
676
+ if (ctx instanceof Response) return ctx;
677
+ const conversation = await loadConversation(c, ctx);
678
+ if (!conversation) return notFound(c);
679
+ const updated = await aiConversations.setConversationPinned({
680
+ conversationId: conversation.id,
681
+ ownerUserId: ctx.ownerUserId,
682
+ pinned: false,
683
+ });
684
+ return updated ? respond(c, ok(await publicConversationFor(updated, c.get("accessSubject")))) : notFound(c);
685
+ })
686
+ .post("/conversations/:conversationId/archive", async (c) => {
687
+ const ctx = await resolveContext(c);
688
+ if (ctx instanceof Response) return ctx;
689
+ const conversation = await loadConversation(c, ctx);
690
+ if (!conversation) return notFound(c);
691
+ if (await aiConversations.getActiveTurn({ conversationId: conversation.id })) {
692
+ return respond(c, fail(err.conflict("Stop the current response before archiving this chat.")));
693
+ }
694
+ const archived = await aiConversations.archiveConversation({
695
+ conversationId: conversation.id,
696
+ ownerUserId: ctx.ownerUserId,
697
+ });
698
+ if (!archived) return notFound(c);
699
+ return respond(c, ok({ ok: true }));
700
+ })
701
+ .post("/conversations/:conversationId/restore", async (c) => {
702
+ const ctx = await resolveContext(c);
703
+ if (ctx instanceof Response) return ctx;
704
+ const conversation = await loadConversation(c, ctx, true);
705
+ if (!conversation) return notFound(c);
706
+ const restored = await aiConversations.restoreConversation({
707
+ conversationId: conversation.id,
708
+ ownerUserId: ctx.ownerUserId,
709
+ });
710
+ return restored ? respond(c, ok(await publicConversationFor(restored, c.get("accessSubject")))) : notFound(c);
711
+ })
712
+ .post("/conversations/:conversationId/viewed", async (c) => {
713
+ const ctx = await resolveContext(c);
714
+ if (ctx instanceof Response) return ctx;
715
+ const conversation = await loadConversation(c, ctx);
716
+ if (!conversation) return notFound(c);
717
+ const viewed = await aiConversations.markConversationViewed({
718
+ conversationId: conversation.id,
719
+ ownerUserId: ctx.ownerUserId,
720
+ });
721
+ return viewed ? respond(c, ok({ ok: true })) : notFound(c);
722
+ })
723
+ .put("/conversations/:conversationId/draft", v("json", AiSaveConversationDraftInputSchema), async (c) => {
724
+ const ctx = await resolveContext(c);
725
+ if (ctx instanceof Response) return ctx;
726
+ const conversation = await loadConversation(c, ctx);
727
+ if (!conversation) return notFound(c);
728
+ const body = c.req.valid("json");
729
+ try {
730
+ const saved = await aiConversations.saveDraft({
731
+ conversationId: conversation.id,
732
+ ownerUserId: ctx.ownerUserId,
733
+ expectedRevision: body.expectedRevision,
734
+ content: body.content,
735
+ });
736
+ if (!saved.ok) {
737
+ return saved.reason === "conflict"
738
+ ? respond(c, fail(err.conflict("The conversation draft changed in another session.")))
739
+ : notFound(c);
740
+ }
741
+ return respond(c, ok(saved.draft));
742
+ } catch (error) {
743
+ return respond(c, fail(err.badInput(error instanceof Error ? error.message : "Invalid conversation draft.")));
744
+ }
745
+ })
746
+ .post("/conversations/:conversationId/turns", v("json", AiSubmitConversationDraftInputSchema), async (c) => {
747
+ const ctx = await resolveContext(c);
748
+ if (ctx instanceof Response) return ctx;
749
+ const conversation = await loadConversation(c, ctx);
750
+ if (!conversation) return notFound(c);
751
+ const body = c.req.valid("json");
752
+ if (conversation.draft.revision !== body.draftRevision) {
753
+ return respond(c, fail(err.conflict("The conversation draft changed in another session.")));
754
+ }
755
+ if (conversation.draft.content.length === 0) return respond(c, fail(err.badInput("The conversation draft is empty.")));
756
+ const resourceParts = conversation.draft.content.filter((part) => part.type === "resource");
757
+ const turnContent = conversation.draft.content.map((part) => {
758
+ if (part.type === "text") return { type: "text" as const, text: part.text };
759
+ if (part.type === "file") {
760
+ return { type: "attachment" as const, path: part.path, mediaType: part.mediaType, size: part.size };
761
+ }
762
+ return {
763
+ type: "text" as const,
764
+ text: aiResourceMarker({ ref: part.ref, title: part.title, icon: part.icon, href: part.href }),
765
+ };
766
+ });
767
+ const { input, message } = aiInputToUserMessage(aiTurnInputToContent({ content: turnContent }));
768
+ const project = conversation.projectId ? await aiProjects.snapshot(conversation.projectId, c.get("accessSubject")) : null;
769
+ if (conversation.projectId && !project) return respond(c, fail(err.notFound("Project")));
770
+ try {
771
+ const result = await submitAiChatTurn({
772
+ assistantChat: true,
773
+ conversationId: conversation.id,
774
+ chatId: conversation.shortId,
775
+ input,
776
+ userMessage: message,
777
+ actor: ctx.actor,
778
+ locale: getLocale(c),
779
+ requestedModelId: body.modelProfileId ?? project?.defaultModelProfileId ?? undefined,
780
+ modelPolicy: ctx.modelPolicy,
781
+ project: project ?? undefined,
782
+ clientToolIds: body.clientToolIds,
783
+ toolSource: { kind: "default", appTools: true },
784
+ toolApprovalContext: ctx.toolApprovalContext,
785
+ expectedDraftRevision: body.draftRevision,
786
+ expectedProjectId: conversation.projectId,
787
+ resources: resourceParts.map((part) => ({ ref: part.ref, title: part.title, icon: part.icon, href: part.href })),
788
+ expectedFiles: conversation.draft.content.flatMap((part) =>
789
+ part.type === "file" ? [{ path: part.path, version: part.version }] : [],
790
+ ),
791
+ });
792
+ rememberLastUsedModel(ctx.actor, result.turn.modelProfileId);
793
+ return respond(
794
+ c,
795
+ ok({
796
+ turn: publicTurn(result.turn, conversation.shortId),
797
+ message: projectPublicAiStoredMessages(
798
+ [result.message],
799
+ conversation.shortId,
800
+ new Map([[result.turn.id, result.turn.shortId]]),
801
+ )[0]!,
802
+ }),
803
+ 201,
804
+ );
805
+ } catch (error) {
806
+ return toAiErrorResponse(c, error);
807
+ }
808
+ })
809
+ .post("/conversations/:conversationId/turns/:turnId/steer", v("json", AiSteerInputSchema), async (c) => {
810
+ const ctx = await resolveContext(c);
811
+ if (ctx instanceof Response) return ctx;
812
+ const conversation = await loadConversation(c, ctx);
813
+ if (!conversation) return notFound(c);
814
+ const turn = await loadTurn(conversation, c.req.param("turnId"));
815
+ if (!turn) return notFound(c);
816
+ const body = c.req.valid("json");
817
+ const result = await aiConversations.enqueueTurnSteer({
818
+ conversationId: conversation.id,
819
+ turnId: turn.id,
820
+ clientRequestId: body.clientRequestId,
821
+ text: body.message,
822
+ });
823
+ if (!result.ok) {
824
+ if (result.reason === "not_found") return respond(c, fail(err.notFound("Turn")));
825
+ if (result.reason === "not_chat") return respond(c, fail(err.badInput("Compaction turns cannot be steered.")));
826
+ return respond(c, fail(err.conflict("This turn is no longer active.")));
827
+ }
828
+ return respond(c, ok(result.steer), 201);
829
+ })
830
+ .post("/conversations/:conversationId/messages/:messageId/retry", v("json", AiMessageRetryInputSchema), async (c) => {
831
+ const ctx = await resolveContext(c);
832
+ if (ctx instanceof Response) return ctx;
833
+ const conversation = await loadConversation(c, ctx);
834
+ if (!conversation) return notFound(c);
835
+ const messageId = c.req.param("messageId");
836
+
837
+ const messages = await aiConversations.listMessages({ conversationId: conversation.id });
838
+ const target = messages.find((m) => m.shortId === messageId);
839
+ if (!target || target.kind !== "message" || target.message.role !== "user") {
840
+ return respond(c, fail(err.badInput("Retry requires a user message.")));
841
+ }
842
+ if (target.compactedAt) {
843
+ return respond(c, fail(err.badInput("This message was compacted out of the model context and cannot be retried.")));
844
+ }
845
+
846
+ const body = c.req.valid("json");
847
+ const content = body.content?.length ? aiTurnInputToContent({ content: body.content }) : target.message.content;
848
+ const { input, message } = aiInputToUserMessage(content as never);
849
+ const instruction = retryInstruction(body.mode);
850
+ const systemPrompt = instruction ?? undefined;
851
+ const originalRunConfig = target.loopId
852
+ ? await aiConversations.getTurnRunConfig({ conversationId: conversation.id, turnId: target.loopId })
853
+ : null;
854
+ const originalProject = originalRunConfig?.kind !== "compact" ? originalRunConfig?.project : undefined;
855
+ const currentProject = originalProject?.id ? await aiProjects.getByShortId(originalProject.id, c.get("accessSubject")) : null;
856
+ if (originalProject && !currentProject) return respond(c, fail(err.notFound("Project")));
857
+
858
+ try {
859
+ const requestedModelId = await selectAssistantAiModelId(
860
+ c.get("accessSubject"),
861
+ body.modelProfileId ?? originalProject?.defaultModelProfileId ?? undefined,
862
+ ctx.modelPolicy,
863
+ );
864
+ if ((await prepareConversationForMessageRetry(conversation.id)) === "busy") {
865
+ return respond(c, fail(err.conflict("Stop the active response before trying a message again.")));
866
+ }
867
+ const result = await submitAiChatTurn({
868
+ assistantChat: true,
869
+ conversationId: conversation.id,
870
+ chatId: conversation.shortId,
871
+ input,
872
+ userMessage: message,
873
+ actor: ctx.actor,
874
+ locale: getLocale(c),
875
+ requestedModelId,
876
+ modelPolicy: ctx.modelPolicy,
877
+ systemPrompt,
878
+ project: originalProject,
879
+ toolSource: { kind: "default", appTools: true },
880
+ toolApprovalContext: ctx.toolApprovalContext,
881
+ truncateFromSeq: target.seq,
882
+ fileSnapshot: originalRunConfig?.kind !== "compact" ? originalRunConfig?.files : undefined,
883
+ retrySourceTurnId: target.loopId ?? undefined,
884
+ });
885
+ rememberLastUsedModel(ctx.actor, result.turn.modelProfileId);
886
+ return respond(
887
+ c,
888
+ ok({
889
+ turn: publicTurn(result.turn, conversation.shortId),
890
+ message: projectPublicAiStoredMessages(
891
+ [result.message],
892
+ conversation.shortId,
893
+ new Map([[result.turn.id, result.turn.shortId]]),
894
+ )[0]!,
895
+ }),
896
+ 201,
897
+ );
898
+ } catch (error) {
899
+ return toAiErrorResponse(c, error);
900
+ }
901
+ })
902
+ .post("/conversations/:conversationId/messages/:messageId/fork", v("json", AiMessageForkInputSchema), async (c) => {
903
+ const ctx = await resolveContext(c);
904
+ if (ctx instanceof Response) return ctx;
905
+ const conversation = await loadConversation(c, ctx);
906
+ if (!conversation) return notFound(c);
907
+ const messageId = c.req.param("messageId");
908
+
909
+ const messages = await aiConversations.listMessages({ conversationId: conversation.id });
910
+ const target = messages.find((m) => m.shortId === messageId);
911
+ if (!target) return respond(c, fail(err.notFound("Message")));
912
+ if (target.compactedAt) {
913
+ return respond(c, fail(err.badInput("This message was compacted out of the model context and cannot be forked.")));
914
+ }
915
+
916
+ const body = c.req.valid("json");
917
+ if (conversation.projectId && !(await aiProjects.get(conversation.projectId, c.get("accessSubject"), "read"))) {
918
+ return respond(c, fail(err.notFound("Project")));
919
+ }
920
+ const forked = await aiConversations.forkConversation({
921
+ sourceConversationId: conversation.id,
922
+ throughSeq: target.seq,
923
+ ownerUserId: ctx.ownerUserId,
924
+ title: body.title ?? conversation.title,
925
+ });
926
+ return respond(c, ok(await conversationDetail(forked, c.get("accessSubject"))));
927
+ })
928
+ .get("/conversations/:conversationId/enrichment", async (c) => {
929
+ const ctx = await resolveContext(c);
930
+ if (ctx instanceof Response) return ctx;
931
+ const conversation = await loadConversation(c, ctx);
932
+ if (!conversation) return notFound(c);
933
+ const [status, runs] = await Promise.all([
934
+ aiConversations.getEnrichmentStatus({ conversationId: conversation.id }),
935
+ aiConversations.listEnrichmentRuns({ conversationId: conversation.id }),
936
+ ]);
937
+ return respond(c, ok({ status, runs: runs.map((run) => publicEnrichmentRun(run, conversation.shortId)) }));
938
+ })
939
+ .post("/conversations/:conversationId/reindex", async (c) => {
940
+ const ctx = await resolveContext(c);
941
+ if (ctx instanceof Response) return ctx;
942
+ const conversation = await loadConversation(c, ctx);
943
+ if (!conversation) return notFound(c);
944
+ await aiMaintenanceJobs.submitConversationReindex(conversation.id);
945
+ return respond(c, ok({ queued: true }), 201);
946
+ })
947
+ .post("/conversations/:conversationId/compact", v("json", AiCompactionInputSchema), async (c) => {
948
+ const ctx = await resolveContext(c);
949
+ if (ctx instanceof Response) return ctx;
950
+ const conversation = await loadConversation(c, ctx);
951
+ if (!conversation) return notFound(c);
952
+ const body = c.req.valid("json");
953
+ try {
954
+ const result = await submitAiCompaction({
955
+ conversationId: conversation.id,
956
+ actor: ctx.actor,
957
+ requestedModelId: body.modelProfileId,
958
+ modelPolicy: ctx.modelPolicy,
959
+ });
960
+ return respond(c, ok({ turn: publicTurn(result.turn, conversation.shortId) }), 201);
961
+ } catch (error) {
962
+ return toAiErrorResponse(c, error);
963
+ }
964
+ })
965
+ .post("/conversations/:conversationId/turns/:turnId/abort", async (c) => {
966
+ const ctx = await resolveContext(c);
967
+ if (ctx instanceof Response) return ctx;
968
+ const conversation = await loadConversation(c, ctx);
969
+ if (!conversation) return notFound(c);
970
+ const turn = await loadTurn(conversation, c.req.param("turnId"));
971
+ if (!turn) return notFound(c);
972
+ await abortAiTurn({ conversationId: conversation.id, turnId: turn.id });
973
+ return respond(c, ok({ ok: true }));
974
+ })
975
+ .post("/conversations/:conversationId/turns/:turnId/actions/:callId", v("json", AiTurnActionSchema), async (c) => {
976
+ const ctx = await resolveContext(c);
977
+ if (ctx instanceof Response) return ctx;
978
+ const conversation = await loadConversation(c, ctx);
979
+ if (!conversation) return notFound(c);
980
+ const turn = await loadTurn(conversation, c.req.param("turnId"));
981
+ const callId = c.req.param("callId");
982
+ if (!turn || !callId) return notFound(c);
983
+ const result = await submitAiTurnAction({
984
+ conversationId: conversation.id,
985
+ turnId: turn.id,
986
+ callId,
987
+ action: c.req.valid("json"),
988
+ toolApprovalContext: ctx.toolApprovalContext,
989
+ });
990
+ if (!result.ok) return toAiActionFailureResponse(c, result);
991
+ return respond(c, ok({ ok: true }));
992
+ })
993
+ .get("/conversations/:conversationId/pending-actions/:turnId", async (c) => {
994
+ const ctx = await resolveContext(c);
995
+ if (ctx instanceof Response) return ctx;
996
+ const conversation = await loadConversation(c, ctx);
997
+ if (!conversation) return notFound(c);
998
+ const turn = await loadTurn(conversation, c.req.param("turnId"));
999
+ if (!turn) return notFound(c);
1000
+ const actions = await listPendingAiTurnActions({ conversationId: conversation.id, turnId: turn.id });
1001
+ return respond(c, ok(actions.map((action) => publicPendingAction(action, conversation.shortId, turn.shortId))));
1002
+ })
1003
+ .get("/conversations/:conversationId/stream", async (c) => {
1004
+ const ctx = await resolveContext(c);
1005
+ if (ctx instanceof Response) return ctx;
1006
+ const conversation = await loadConversation(c, ctx);
1007
+ if (!conversation) return notFound(c);
1008
+ return createAiConversationStreamResponse({
1009
+ conversation,
1010
+ signal: c.req.raw.signal,
1011
+ // Both calls hit the database: resolveContext re-runs the app's own
1012
+ // access check, loadConversation re-checks ownership and resource
1013
+ // scope. The actor itself is a request-time snapshot, so this catches
1014
+ // a withdrawn grant but not a revoked credential — see r9358ceb.
1015
+ revalidate: async () => {
1016
+ const current = await resolveContext(c);
1017
+ if (current instanceof Response) return false;
1018
+ return Boolean(await loadConversation(c, current));
1019
+ },
1020
+ });
1021
+ })
1022
+
1023
+ // ── Conversation files ─────────────────────────────────────────────
1024
+ .get("/conversations/:conversationId/files", v("query", FilesListQuerySchema), async (c) => {
1025
+ const ctx = await resolveContext(c);
1026
+ if (ctx instanceof Response) return ctx;
1027
+ const conversation = await loadConversation(c, ctx);
1028
+ if (!conversation) return notFound(c);
1029
+ const files = await aiFileStore.list({ conversationId: conversation.id, prefix: c.req.valid("query").prefix ?? "/" });
1030
+ return respond(c, ok({ files, totalBytes: await aiFileStore.totalBytes(conversation.id) }));
1031
+ })
1032
+ .post("/conversations/:conversationId/files", async (c) => {
1033
+ const ctx = await resolveContext(c);
1034
+ if (ctx instanceof Response) return ctx;
1035
+ const conversation = await loadConversation(c, ctx);
1036
+ if (!conversation) return notFound(c);
1037
+
1038
+ const form = await c.req.formData().catch(() => null);
1039
+ const file = form?.get("file");
1040
+ if (!(file instanceof File)) return respond(c, fail(err.badInput("Missing 'file' field")));
1041
+ if (file.size > AI_FILES_MAX_FILE_BYTES_DEFAULT) {
1042
+ return respond(c, fail(err.badInput(`File exceeds the ${Math.floor(AI_FILES_MAX_FILE_BYTES_DEFAULT / (1024 * 1024))} MB limit`)));
1043
+ }
1044
+ const name = (file.name || "upload").replaceAll("/", "_").replaceAll("\\", "_").replaceAll("\0", "").slice(0, 160) || "upload";
1045
+ const path = normalizeAiFilePath(`/${name}`);
1046
+ if (!path) return respond(c, fail(err.badInput("Invalid file name")));
1047
+ if (aiProjectFilePathFromMount(path) !== null) return respond(c, fail(err.badInput("The /project namespace is reserved.")));
1048
+
1049
+ try {
1050
+ const stat = await aiFileStore.createUserUpload({
1051
+ conversationId: conversation.id,
1052
+ path,
1053
+ bytes: new Uint8Array(await file.arrayBuffer()),
1054
+ mediaType: file.type || guessAiMediaType(path),
1055
+ });
1056
+ return respond(c, ok({ file: stat }));
1057
+ } catch (error) {
1058
+ return respond(c, fail(err.badInput(error instanceof Error ? error.message : "Upload failed")));
1059
+ }
1060
+ })
1061
+ .get("/conversations/:conversationId/files/content", v("query", FilePathQuerySchema), async (c) => {
1062
+ const ctx = await resolveContext(c);
1063
+ if (ctx instanceof Response) return ctx;
1064
+ const conversation = await loadConversation(c, ctx);
1065
+ if (!conversation) return notFound(c);
1066
+ const path = normalizeAiFilePath(c.req.valid("query").path);
1067
+ if (!path) return notFound(c);
1068
+ const stored = await aiFileStore.read({ conversationId: conversation.id, path });
1069
+ if (!stored) return notFound(c);
1070
+ const filename = path.slice(path.lastIndexOf("/") + 1).replaceAll('"', "");
1071
+ return c.body(stored.bytes as unknown as ArrayBuffer, 200, {
1072
+ "Content-Type": stored.mediaType || "application/octet-stream",
1073
+ "Content-Disposition": `attachment; filename="${filename}"`,
1074
+ "Content-Length": String(stored.size),
1075
+ "Cache-Control": "private, no-store",
1076
+ });
1077
+ })
1078
+ .delete("/conversations/:conversationId/files", v("query", FilePathQuerySchema), async (c) => {
1079
+ const ctx = await resolveContext(c);
1080
+ if (ctx instanceof Response) return ctx;
1081
+ const conversation = await loadConversation(c, ctx);
1082
+ if (!conversation) return notFound(c);
1083
+ const path = normalizeAiFilePath(c.req.valid("query").path);
1084
+ if (!path) return notFound(c);
1085
+ const removed = await aiFileStore.remove({ conversationId: conversation.id, path, recursive: false });
1086
+ if (removed === 0) return notFound(c);
1087
+ return respond(c, ok({ deleted: true }));
1088
+ })
1089
+ .put("/conversations/:conversationId/files/content", v("json", FileWriteSchema), async (c) => {
1090
+ const ctx = await resolveContext(c);
1091
+ if (ctx instanceof Response) return ctx;
1092
+ const conversation = await loadConversation(c, ctx);
1093
+ if (!conversation) return notFound(c);
1094
+ const body = c.req.valid("json");
1095
+ const path = normalizeAiFilePath(body.path);
1096
+ if (!path) return respond(c, fail(err.badInput("Use an absolute conversation file path.")));
1097
+ if (aiProjectFilePathFromMount(path) !== null) return respond(c, fail(err.badInput("The /project namespace is reserved.")));
1098
+ const existing = await aiFileStore.stat({ conversationId: conversation.id, path });
1099
+ try {
1100
+ await aiFileStore.write({
1101
+ conversationId: conversation.id,
1102
+ path,
1103
+ bytes: decodeAiFileContent(body.content, body.encoding),
1104
+ mediaType: guessAiMediaType(path),
1105
+ origin: existing?.origin ?? "user",
1106
+ allowUserOverwrite: existing?.origin === "user",
1107
+ });
1108
+ } catch (error) {
1109
+ return respond(c, fail(err.badInput(error instanceof Error ? error.message : "Failed to write file")));
1110
+ }
1111
+ return respond(c, ok({ file: await aiFileStore.stat({ conversationId: conversation.id, path }) }));
1112
+ })
1113
+ .post("/conversations/:conversationId/files/rename", v("json", FileRenameSchema), async (c) => {
1114
+ const ctx = await resolveContext(c);
1115
+ if (ctx instanceof Response) return ctx;
1116
+ const conversation = await loadConversation(c, ctx);
1117
+ if (!conversation) return notFound(c);
1118
+ const body = c.req.valid("json");
1119
+ const from = normalizeAiFilePath(body.from);
1120
+ const to = normalizeAiFilePath(body.to);
1121
+ if (!from || !to) return respond(c, fail(err.badInput("Use absolute conversation file paths.")));
1122
+ if (aiProjectFilePathFromMount(to) !== null) return respond(c, fail(err.badInput("The /project namespace is reserved.")));
1123
+ const renamed = await aiFileStore.rename({ conversationId: conversation.id, from, to });
1124
+ if (renamed === "not_found") return notFound(c);
1125
+ if (renamed === "conflict") return respond(c, fail(err.conflict(`File "${to}"`)));
1126
+ return respond(c, ok({ renamed: true }));
1127
+ })
1128
+ );
1129
+ })();
1130
+
1131
+ export type AiRoutes = typeof aiRoutes;