@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/runtime.ts CHANGED
@@ -6,12 +6,16 @@ import { lazySync } from "../_internal/process-sync";
6
6
  import type { RequestActor } from "../server";
7
7
  import { logger } from "../services/logging";
8
8
  import { superviseRuntimeTask } from "../services/runtime-lifecycle";
9
-
9
+ import { coreSettings } from "../services/settings/api";
10
10
  import { type AiToolApprovalContext, aiTurnAllowsRememberedApprovals, rememberAiToolApproval } from "./approvals";
11
11
  import { aiChatAccessSubject, selectAssistantAiModelId } from "./assistant-models";
12
+ import { startAiDictationRuntime } from "./dictation-runtime";
12
13
  import { AiTurnExecutor } from "./executor";
13
14
  import { canonicalizeAiConversationAttachments, snapshotAiConversationFiles } from "./file-context";
14
15
  import { startAiInvalidationRuntime, stopAiInvalidationRuntime } from "./live-outbox";
16
+ import { drainQueuedMessages } from "./message-queue";
17
+ import { aiQuotas } from "./quotas";
18
+ import { parseAiResourceMarker } from "./resource-markers";
15
19
  import { isAiVisionModelConfigured } from "./settings";
16
20
  import { aiConversations } from "./store";
17
21
  import { publishAiTurnAbort, publishAiWireEvent } from "./stream";
@@ -44,7 +48,7 @@ const AI_TURN_LEASE_MS = 45_000;
44
48
  const AI_TURN_HEARTBEAT_MS = 3_000;
45
49
  const AI_TURN_WORKER_CONCURRENCY = 8;
46
50
  const AI_TURN_MAX_ATTEMPTS = 5;
47
- const AI_TURN_RUN_BUDGET_MS = 10 * 60_000;
51
+
48
52
  const AI_SWEEP_INTERVAL_MS = 15_000;
49
53
 
50
54
  // ---------------------------------------------------------------------------
@@ -65,7 +69,14 @@ const aiTurnQueue = lazySync((sync) => {
65
69
 
66
70
  // No idempotency key: the DB claim is the only gate, so re-enqueues (recovery,
67
71
  // continuation, stale-sweep) are always allowed and never silently swallowed.
68
- const enqueueAiTurn = (job: AiTurnJob): Promise<unknown> => aiTurnQueue().send({ data: job, orderingKey: job.conversationId });
72
+ const enqueueAiTurn = (job: AiTurnJob): Promise<unknown> => aiTurnQueue().send({ data: job, orderingKey: job.turnId });
73
+
74
+ /** Wake the durable queue promptly; the periodic sweep retries a failed wake-up. */
75
+ export const wakeAiMessageQueue = (conversationId: string): void => {
76
+ void drainQueuedMessages(enqueueAiTurn, conversationId).catch(() =>
77
+ log.warn("AI message queue wake-up failed", { code: "queue_wakeup_failed", conversationId }),
78
+ );
79
+ };
69
80
 
70
81
  /** Enqueue a turn created atomically by another durable AI workflow. */
71
82
  export const enqueueExistingAiTurn = (input: AiTurnJob): Promise<unknown> => enqueueAiTurn(input);
@@ -103,7 +114,7 @@ export type SubmitAiChatTurnInput = {
103
114
  retrySourceTurnId?: string;
104
115
  };
105
116
 
106
- export const submitAiChatTurn = async (input: SubmitAiChatTurnInput): Promise<{ turn: AiTurn; message: AiStoredMessage }> => {
117
+ export const prepareAiChatTurn = async (input: SubmitAiChatTurnInput) => {
107
118
  if (input.clientToolIds?.length && input.toolSource?.kind !== "default") {
108
119
  throw new Error("Optional client tools require the default tool source.");
109
120
  }
@@ -128,8 +139,18 @@ export const submitAiChatTurn = async (input: SubmitAiChatTurnInput): Promise<{
128
139
  modelPolicy: input.modelPolicy,
129
140
  requestedModelId,
130
141
  });
142
+ const quotaSubject = aiChatAccessSubject(input.actor);
143
+ if (input.assistantChat && quotaSubject) await aiQuotas.assertAllowed(quotaSubject, resolved.profile.id);
131
144
  const runConfig: AiChatTurnRunConfig = {
132
145
  kind: "chat",
146
+ selectedSkillIds: [
147
+ ...new Set(
148
+ (userMessage.role === "user" ? userMessage.content : []).flatMap((part) => {
149
+ const marker = parseAiResourceMarker(typeof part === "string" ? part : part.type === "text" ? part.text : "");
150
+ return marker?.ref.type === "core.ai.skill" ? [marker.ref.id] : [];
151
+ }),
152
+ ),
153
+ ],
133
154
  ...(input.assistantChat ? { assistantChat: true } : {}),
134
155
  input: canonicalInput,
135
156
  chatId: input.chatId,
@@ -146,7 +167,7 @@ export const submitAiChatTurn = async (input: SubmitAiChatTurnInput): Promise<{
146
167
  toolApprovalContext: input.toolApprovalContext,
147
168
  };
148
169
 
149
- const submitted = await aiConversations.submitChatTurn({
170
+ return {
150
171
  conversationId: input.conversationId,
151
172
  modelProfileId: resolved.profile.id,
152
173
  runConfig,
@@ -156,8 +177,11 @@ export const submitAiChatTurn = async (input: SubmitAiChatTurnInput): Promise<{
156
177
  expectedProjectId: input.expectedProjectId,
157
178
  resources: input.resources,
158
179
  retrySourceTurnId: input.retrySourceTurnId,
159
- });
180
+ };
181
+ };
160
182
 
183
+ export const submitAiChatTurn = async (input: SubmitAiChatTurnInput): Promise<{ turn: AiTurn; message: AiStoredMessage }> => {
184
+ const submitted = await aiConversations.submitChatTurn(await prepareAiChatTurn(input));
161
185
  await enqueueAiTurn({ conversationId: input.conversationId, turnId: submitted.turn.id });
162
186
  return submitted;
163
187
  };
@@ -242,7 +266,8 @@ export const abortAiTurn = async (input: { conversationId: string; turnId: strin
242
266
 
243
267
  if (request.ownerless) {
244
268
  const finalized = await aiConversations.completeTurn({ ...input, status: "aborted", error: null });
245
- if (finalized === "completed") {
269
+ const config = await aiConversations.getTurnRunConfig(input);
270
+ if (finalized === "completed" && !(config?.kind === "chat" && config.background)) {
246
271
  const attempt = active?.turn.id === input.turnId ? active.turn.attempt : 1;
247
272
  const seq = (active?.turn.id === input.turnId ? active.liveSeq : 0) + 1;
248
273
  await publishAiWireEvent({
@@ -343,9 +368,7 @@ export const submitAiTurnAction = async (input: {
343
368
  return { ok: false, status: 409, message: "AI action was already resolved with a different response." };
344
369
  }
345
370
  }
346
- await aiToolAudit
347
- .noteToolCompleted({ turnId: input.turnId, callId: input.callId, result: input.action.result, isError: false })
348
- .catch(() => undefined);
371
+ await aiToolAudit.noteToolCompleted({ turnId: input.turnId, callId: input.callId, isError: false }).catch(() => undefined);
349
372
  }
350
373
 
351
374
  await enqueueAiTurn({ conversationId: input.conversationId, turnId: input.turnId });
@@ -362,6 +385,8 @@ const runClaimedTurn = async (
362
385
  leaseOwner: string,
363
386
  onTurnFinalized?: (event: AiTurnFinalizedEvent) => Promise<void>,
364
387
  ): Promise<void> => {
388
+ const timeoutMinutes = await coreSettings.get<number>("ai.turn_timeout_minutes");
389
+ const runBudgetMs = Math.max(0, Number(timeoutMinutes ?? 30)) * 60_000;
365
390
  const claim =
366
391
  (await aiConversations.claimTurn({
367
392
  ...job,
@@ -369,7 +394,7 @@ const runClaimedTurn = async (
369
394
  leaseMs: AI_TURN_LEASE_MS,
370
395
  from: "queue",
371
396
  maxAttempts: AI_TURN_MAX_ATTEMPTS,
372
- runBudgetMs: AI_TURN_RUN_BUDGET_MS,
397
+ runBudgetMs,
373
398
  })) ??
374
399
  (await aiConversations.claimTurn({
375
400
  ...job,
@@ -377,7 +402,7 @@ const runClaimedTurn = async (
377
402
  leaseMs: AI_TURN_LEASE_MS,
378
403
  from: "waiting",
379
404
  maxAttempts: AI_TURN_MAX_ATTEMPTS,
380
- runBudgetMs: AI_TURN_RUN_BUDGET_MS,
405
+ runBudgetMs,
381
406
  }));
382
407
 
383
408
  if (!claim) return; // Already owned, done, cancelled, or attempt-capped.
@@ -392,12 +417,29 @@ const runClaimedTurn = async (
392
417
  };
393
418
 
394
419
  const processMessage = async (
395
- message: QueueMessage<AiTurnJob>,
420
+ message: Pick<QueueMessage<AiTurnJob>, "messageId" | "data" | "heartbeat">,
396
421
  signal: AbortSignal,
397
422
  onTurnFinalized?: (event: AiTurnFinalizedEvent) => Promise<void>,
398
423
  ): Promise<void> => {
399
424
  const leaseOwner = `${AI_WORKER_ID}:${message.messageId}:${crypto.randomUUID()}`;
400
- const touch = setInterval(() => void message.heartbeat().catch(() => undefined), AI_TURN_HEARTBEAT_MS);
425
+ let heartbeatFailed = false;
426
+ const touch = setInterval(() => {
427
+ void message
428
+ .heartbeat()
429
+ .then(() => {
430
+ heartbeatFailed = false;
431
+ })
432
+ .catch(() => {
433
+ if (!heartbeatFailed && !signal.aborted) {
434
+ log.warn("AI queue heartbeat failed", {
435
+ code: "queue_heartbeat_failed",
436
+ conversationId: message.data.conversationId,
437
+ turnId: message.data.turnId,
438
+ });
439
+ }
440
+ heartbeatFailed = true;
441
+ });
442
+ }, AI_TURN_HEARTBEAT_MS);
401
443
  if (typeof touch === "object" && "unref" in touch) touch.unref();
402
444
  try {
403
445
  await runClaimedTurn(message.data, signal, leaseOwner, onTurnFinalized);
@@ -409,10 +451,13 @@ const processMessage = async (
409
451
  });
410
452
  } finally {
411
453
  clearInterval(touch);
454
+ if (!signal.aborted) wakeAiMessageQueue(message.data.conversationId);
412
455
  }
413
456
  };
414
457
 
415
458
  const publishSweepFinished = async (turn: AiTurnFinalizedAction & { error?: string }, status: "failed" | "aborted"): Promise<void> => {
459
+ const config = await aiConversations.getTurnRunConfig(turn);
460
+ if (config?.kind === "chat" && config.background) return;
416
461
  await publishAiWireEvent({
417
462
  v: 1,
418
463
  conversationId: turn.conversationId,
@@ -422,13 +467,23 @@ const publishSweepFinished = async (turn: AiTurnFinalizedAction & { error?: stri
422
467
  type: "turn_finished",
423
468
  status,
424
469
  error: status === "failed" ? (turn.error ?? "AI turn failed.") : null,
425
- }).catch(() => undefined);
470
+ }).catch(() =>
471
+ log.warn("AI completion notification failed", {
472
+ code: "completion_publish_failed",
473
+ conversationId: turn.conversationId,
474
+ turnId: turn.turnId,
475
+ }),
476
+ );
426
477
  };
427
478
 
428
479
  export const sweepAiRuntime = async (onTurnFinalized?: (event: AiTurnFinalizedEvent) => Promise<void>): Promise<void> => {
480
+ await aiQuotas.prune().catch(() => log.warn("Quota retention failed", { code: "quota_retention_failed" }));
481
+ await drainQueuedMessages(enqueueAiTurn);
429
482
  const sweep = await aiConversations.sweepTurns({ maxAttempts: AI_TURN_MAX_ATTEMPTS });
430
483
  await Promise.all([
431
- ...sweep.requeued.map((job) => enqueueAiTurn(job).catch(() => undefined)),
484
+ ...sweep.requeued.map((job) =>
485
+ enqueueAiTurn(job).catch(() => log.warn("AI recovery enqueue failed", { code: "recovery_enqueue_failed", ...job })),
486
+ ),
432
487
  ...sweep.failed.map((turn) => publishSweepFinished(turn, "failed")),
433
488
  ...sweep.aborted.map((turn) => publishSweepFinished(turn, "aborted")),
434
489
  ]);
@@ -508,11 +563,13 @@ export const startAiRuntime = (
508
563
  const sweepTimer = setInterval(runSweep, AI_SWEEP_INTERVAL_MS);
509
564
  if (typeof sweepTimer === "object" && "unref" in sweepTimer) sweepTimer.unref();
510
565
 
566
+ const stopDictations = startAiDictationRuntime();
511
567
  const state: AiRuntimeState = {
512
568
  listeners,
513
569
  stop: () => {
514
570
  if (running !== state) return;
515
571
  controller.abort();
572
+ stopDictations();
516
573
  clearInterval(sweepTimer);
517
574
  listeners.clear();
518
575
  running = null;
@@ -541,4 +598,4 @@ const releaseAiRuntime = (listener: ((event: AiTurnFinalizedEvent) => Promise<vo
541
598
  };
542
599
  };
543
600
 
544
- export const __aiRuntimeTest = { actionMatchesResolvedEvent };
601
+ export const __aiRuntimeTest = { actionMatchesResolvedEvent, processMessage, publishSweepFinished };
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { coreSettings } from "../services";
3
+ import { AiModelPricingSchema } from "../shared/ai-costs";
3
4
  import { getAiCredential, listAiCredentialProfileIds } from "./credentials";
4
5
  import { AI_FIRECRAWL_API_KEY_SETTING_KEY } from "./firecrawl-tools";
5
6
  import { createAiProvider } from "./provider";
@@ -94,10 +95,22 @@ const ModelProfileSchema = z
94
95
  maxOutputTokens: z.number().int().positive().optional(),
95
96
  maxLoadedTools: z.number().int().optional(),
96
97
  maxToolRounds: z.number().int().optional(),
97
- creditsPerInputToken: z.number().nonnegative().optional(),
98
- creditsPerOutputToken: z.number().nonnegative().optional(),
98
+ pricing: AiModelPricingSchema.optional(),
99
99
  })
100
100
  .superRefine((profile, ctx) => {
101
+ if (profile.capabilities?.includes("transcription")) {
102
+ if (profile.pricing) ctx.addIssue({ code: "custom", path: ["pricing"], message: "Audio pricing is not supported." });
103
+ if (profile.capabilities.some((capability) => capability !== "transcription")) {
104
+ ctx.addIssue({ code: "custom", path: ["capabilities"], message: "Audio transcription cannot be combined with chat capabilities." });
105
+ }
106
+ if (profile.provider !== "openai" && profile.provider !== "openai-compatible") {
107
+ ctx.addIssue({
108
+ code: "custom",
109
+ path: ["provider"],
110
+ message: "Audio transcription requires OpenAI or an OpenAI-compatible endpoint.",
111
+ });
112
+ }
113
+ }
101
114
  if (profile.provider === "openai-compatible" && !profile.baseURL) {
102
115
  ctx.addIssue({ code: "custom", path: ["baseURL"], message: "OpenAI-compatible profiles require a base URL." });
103
116
  }
@@ -177,6 +190,7 @@ export const validateAiSettingsConfiguration = (input: {
177
190
  defaultModelId: string;
178
191
  backgroundModelId: string;
179
192
  visionModelId?: string;
193
+ audioModelId?: string;
180
194
  workflowModelId: string;
181
195
  profiles: readonly AiModelProfile[];
182
196
  credentialProfileIds: readonly string[];
@@ -186,13 +200,14 @@ export const validateAiSettingsConfiguration = (input: {
186
200
  const errors: Record<string, string> = {};
187
201
  const enabledProfiles = input.profiles.filter((profile) => profile.enabled);
188
202
  const defaultProfile = input.profiles.find((profile) => profile.id === input.defaultModelId);
189
- if (!input.defaultModelId || !defaultProfile?.enabled) {
203
+ if (!input.defaultModelId || !defaultProfile?.enabled || defaultProfile.capabilities.includes("transcription")) {
190
204
  errors["ai.default_model_id"] = "Choose an enabled model profile.";
191
205
  }
192
206
 
193
207
  if (input.backgroundModelId) {
194
208
  const backgroundProfile = input.profiles.find((profile) => profile.id === input.backgroundModelId);
195
- if (!backgroundProfile?.enabled) errors["ai.background_model_id"] = "Choose an enabled model profile or use the platform default.";
209
+ if (!backgroundProfile?.enabled || backgroundProfile.capabilities.includes("transcription"))
210
+ errors["ai.background_model_id"] = "Choose an enabled chat model profile or use the platform default.";
196
211
  }
197
212
 
198
213
  if (input.visionModelId) {
@@ -204,7 +219,15 @@ export const validateAiSettingsConfiguration = (input: {
204
219
 
205
220
  if (input.workflowModelId) {
206
221
  const workflowProfile = input.profiles.find((profile) => profile.id === input.workflowModelId);
207
- if (!workflowProfile?.enabled) errors["ai.workflow_model_id"] = "Choose an enabled model profile or use the background model.";
222
+ if (!workflowProfile?.enabled || workflowProfile.capabilities.includes("transcription"))
223
+ errors["ai.workflow_model_id"] = "Choose an enabled chat model profile or use the background model.";
224
+ }
225
+
226
+ if (input.audioModelId) {
227
+ const audioProfile = input.profiles.find((profile) => profile.id === input.audioModelId);
228
+ if (!audioProfile?.enabled || !audioProfile.capabilities.includes("transcription")) {
229
+ errors["ai.audio_model_id"] = "Choose an enabled audio transcription profile or disable audio transcription.";
230
+ }
208
231
  }
209
232
 
210
233
  const configured = new Set(input.credentialProfileIds);
@@ -301,7 +324,7 @@ export const resolveAiSettingsStateFromRaw = async (input: {
301
324
  };
302
325
  }
303
326
 
304
- if (!defaultProfile.enabled) {
327
+ if (!defaultProfile.enabled || defaultProfile.capabilities.includes("transcription")) {
305
328
  return {
306
329
  ok: false,
307
330
  ...baseState,
@@ -309,7 +332,7 @@ export const resolveAiSettingsStateFromRaw = async (input: {
309
332
  profiles: parsed.profiles,
310
333
  error: {
311
334
  code: "default_model_disabled",
312
- message: `Default AI model "${input.defaultModelId}" is disabled.`,
335
+ message: `Default AI model "${input.defaultModelId}" must be an enabled text model.`,
313
336
  fields: { "ai.default_model_id": "Choose an enabled model profile id." },
314
337
  },
315
338
  };
@@ -398,6 +421,7 @@ const hasAll = <T extends string>(values: readonly T[], required: readonly T[] |
398
421
 
399
422
  const matchesPolicy = (profile: AiModelProfile, policy: AiModelPolicy): boolean => {
400
423
  if (!profile.enabled) return false;
424
+ if (profile.capabilities.includes("transcription") !== Boolean(policy.requiredCapabilities?.includes("transcription"))) return false;
401
425
  if ("allowedModelIds" in policy && policy.allowedModelIds && !policy.allowedModelIds.includes(profile.id)) return false;
402
426
  if (policy.allowedDataBoundaries && !policy.allowedDataBoundaries.includes(profile.dataBoundary)) return false;
403
427
  return hasAll(profile.capabilities, policy.requiredCapabilities);
@@ -521,7 +545,11 @@ export const toPublicAiSettingsState = async (allowedDataBoundaries?: AiDataBoun
521
545
  firecrawlConfigured: state.firecrawlConfigured,
522
546
  models:
523
547
  state.ok && state.enabled
524
- ? state.profiles.filter((profile) => profile.enabled && isUsableProfile(profile, configured)).map(profileToPublic)
548
+ ? state.profiles
549
+ .filter(
550
+ (profile) => matchesPolicy(profile, { kind: "selectable", allowedDataBoundaries }) && isUsableProfile(profile, configured),
551
+ )
552
+ .map(profileToPublic)
525
553
  : [],
526
554
  };
527
555
  };
@@ -14,13 +14,12 @@ export const withAiShortId = async <T>(
14
14
  try {
15
15
  return await insert(allocate());
16
16
  } catch (error) {
17
- const collision =
17
+ const uniqueViolation =
18
18
  typeof error === "object" &&
19
19
  error !== null &&
20
- "code" in error &&
21
- error.code === "23505" &&
22
- "constraint" in error &&
23
- error.constraint === constraint;
20
+ (("code" in error && error.code === "23505") || ("errno" in error && error.errno === "23505"));
21
+ const collision =
22
+ typeof error === "object" && error !== null && uniqueViolation && "constraint" in error && error.constraint === constraint;
24
23
  if (!collision) throw error;
25
24
  }
26
25
  }
@@ -60,14 +60,4 @@ export const selectAiSkillCatalog = (
60
60
  return { skills: selected, omitted: normalized.length - selected.length };
61
61
  };
62
62
 
63
- export const searchAiSkillCatalog = (
64
- skills: readonly AiSkillCatalogEntry[],
65
- query: string,
66
- limit: number,
67
- ): { skills: AiSkillCatalogEntry[]; more: boolean } => {
68
- const normalized = skills.map((skill) => ({ name: skill.name, description: skill.description }));
69
- const matches = ranked(normalized, query).filter((skill) => relevance(skill, query) > 0);
70
- return { skills: matches.slice(0, limit), more: matches.length > limit };
71
- };
72
-
73
63
  export const aiSkillCatalogChars = (skills: readonly AiSkillCatalogEntry[]): number => skills.map(catalogLine).join("\n").length;
@@ -0,0 +1,58 @@
1
+ import { sql } from "bun";
2
+ import { toPgTextArray } from "../services/postgres";
3
+
4
+ const BM25_INDEX = "ai.skills_search_bm25_idx";
5
+ let bm25Available: Promise<boolean> | undefined;
6
+
7
+ /** BM25 only ranks candidates; native matching and access checks always apply. */
8
+ export async function withAiSkillSearch<T>(query: string, run: (bm25: boolean) => Promise<T>): Promise<T> {
9
+ if (!query) return run(false);
10
+ bm25Available ??= sql<{ available: boolean }[]>`
11
+ SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_textsearch')
12
+ AND EXISTS (SELECT 1 FROM pg_class c JOIN pg_am am ON am.oid = c.relam
13
+ WHERE c.oid = to_regclass(${BM25_INDEX}) AND am.amname = 'bm25') AS available
14
+ `.then((rows) => rows[0]?.available ?? false);
15
+ const bm25 = await bm25Available;
16
+ try {
17
+ return await run(bm25);
18
+ } catch (error) {
19
+ const capabilityLost =
20
+ typeof error === "object" &&
21
+ error !== null &&
22
+ ["code", "errno"].some((key) => key in error && ["0A000", "42704", "42883", "55000"].includes(String(Reflect.get(error, key))));
23
+ if (!bm25 || !capabilityLost) throw error;
24
+ bm25Available = Promise.resolve(false);
25
+ return run(false);
26
+ }
27
+ }
28
+
29
+ export function aiSkillSearchSql(query: string, bm25: boolean) {
30
+ const terms = [...new Set(query.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [])];
31
+ // Match every term in either field. Short prefixes stay exact; longer terms
32
+ // tolerate typos without matching unrelated short words throughout the catalog.
33
+ const matches = !query
34
+ ? sql`TRUE`
35
+ : !terms.length
36
+ ? sql`FALSE`
37
+ : sql`
38
+ NOT EXISTS (SELECT 1 FROM unnest(${toPgTextArray(terms)}::text[]) AS term
39
+ WHERE strpos(lower(skill.name), term) = 0 AND strpos(lower(skill.description), term) = 0
40
+ AND (length(term) < 4 OR greatest(word_similarity(term, skill.name),
41
+ word_similarity(term, skill.description)) < 0.35))
42
+ `;
43
+ // Rank typos against whole words: a partial-word extent such as "invi…" in a
44
+ // description must not outscore the typo'd name "invoices" it was meant for.
45
+ const rank = !query
46
+ ? sql`0::real`
47
+ : sql`
48
+ CASE WHEN lower(skill.name) = ${query.toLowerCase()} THEN 100 ELSE 0 END
49
+ + CASE WHEN strpos(lower(skill.name), ${query.toLowerCase()}) > 0 THEN 10 ELSE 0 END
50
+ + 2 * strict_word_similarity(${query}, skill.name) + strict_word_similarity(${query}, skill.description)
51
+ `;
52
+ const textRank = !query
53
+ ? sql`0::real`
54
+ : bm25
55
+ ? sql`-(skill.search_text <@> to_bm25query(${query}, ${BM25_INDEX}))`
56
+ : sql`ts_rank_cd(to_tsvector('simple', skill.search_text), plainto_tsquery('simple', ${query}))`;
57
+ return { matches, rank, textRank };
58
+ }
@@ -1,6 +1,7 @@
1
- import { CLOUD_KIT_INSTRUCTIONS } from "./kit-skill";
1
+ import { ASSISTANT_CODE_MODE_SKILL } from "./code-mode-skill";
2
+ import { ASSISTANT_DATA_ANALYSIS_SKILL } from "./data-analysis-skill";
2
3
  import { CLOUD_GRIDS_INSTRUCTIONS, CLOUD_GRIDS_QUERY_REFERENCE } from "./grids-skill";
3
- import { aiSkills } from "./skills";
4
+ import type { AiSkillTemplate } from "./skills";
4
5
 
5
6
  const SKILL_CREATOR_INSTRUCTIONS = `# Create and improve Skills
6
7
 
@@ -81,7 +82,15 @@ Pass the exact current revision to mutations and read again after a revision con
81
82
 
82
83
  Creating, changing, and deleting Skills are reviewed mutations. Prepare the concrete content the user requested, then use the Capability review instead of asking for an extra confirmation that duplicates it.
83
84
 
84
- Sharing, access changes, imports, and exports are not available through these capabilities. Say so plainly rather than inventing a tool or bypassing Cloud permissions.`;
85
+ ## Optional App-backed workflows
86
+
87
+ A Skill can reference a reusable Studio App and its published actions instead of duplicating code. Load \`assistant-code-mode\` only when building, changing, or discovering such an App is useful. Apps may be stateless scripts, agent-only services with shared data, dashboards, or a combination. Many Skills need no code and many Apps need no Skill.
88
+
89
+ When referring to an App, include its exact ID, action names, input/output meaning, and a concrete workflow example. Read the action schemas instead of guessing APIs. Skill access and App access are independent: warn when intended recipients can use only one; never grant access implicitly.
90
+
91
+ For requested sharing, discover \`core.entities.search\` to resolve a recipient, then \`core.ai.skill.access.read\` and \`core.ai.skill.access.change\`. Read current grants and pass their exact accessRevision as expectedAccessRevision. Supply either a returned principal and permission (read/write/admin), or an existing accessId and permission (null revokes). Every mutation receives fresh review and preserves the last administrator. After a conflict, re-read and prepare a new review. For App permissions, read the Code Mode access reference; App Use/Manage is separate from Skill Read/Edit/Manage.
92
+
93
+ Imports and exports are not available through these capabilities. Do not invent a tool or bypass Cloud permissions.`;
85
94
 
86
95
  const CLOUD_MAIL_INSTRUCTIONS = `# Work with Cloud Mail
87
96
 
@@ -196,7 +205,7 @@ Use these defaults unless the user asks otherwise or a more specific loaded Skil
196
205
  - Simple subtasks use \`spaces.task.checklist.list\`, \`spaces.task.checklist.create\`, \`spaces.task.checklist.update\` and \`spaces.task.checklist.delete\`. Keep each entry to a label and completion state; do not turn checklist entries into independent assigned tasks.
197
206
  - Use comments for discussion; read before replacing or deleting them. For tag replacement, preserve tags not included in the requested change.
198
207
  - Use \`spaces.item.reference.find\` to find existing items for a source resource. Link on creation through references rather than a redundant second mutation. For an existing item use \`spaces.item.reference.add\`; use \`spaces.item.link-candidate.search\` when a writable target is unknown.
199
- - Use \`spaces.event.create-once\` only in retryable durable workflows with their stable idempotency key. Normal interactive creation uses \`spaces.event.create\`. Never blindly retry an uncertain mutation.
208
+ - Use \`spaces.event.create\` for interactive creation. Never blindly retry an uncertain mutation.
200
209
 
201
210
  ## Calendar mail and cross-app work
202
211
 
@@ -237,7 +246,7 @@ Use these defaults unless the user asks otherwise or a more specific loaded Skil
237
246
 
238
247
  - Conversations: \`core.ai.chats.search\`, \`core.ai.chat.read\`, \`core.ai.chat.search\`, \`core.ai.chat.resources\`, and \`core.ai.chats.resources\`.
239
248
  - Messaging: \`core.ai.chat.message\`.
240
- - Scheduled work: \`core.ai.tasks.list\`, \`core.ai.task.read\`, \`core.ai.task.create\`, \`core.ai.task.update\`, \`core.ai.task.pause\`, \`core.ai.task.resume\`, \`core.ai.task.run\`, and \`core.ai.task.delete\`.
249
+
241
250
 
242
251
  Load only the needed capabilities and reuse returned typed IDs unchanged.
243
252
 
@@ -246,9 +255,83 @@ Load only the needed capabilities and reuse returned typed IDs unchanged.
246
255
  - The runtime Chat ID identifies the current conversation. Use \`core.ai.chat.search\` for earlier content in it. For another conversation, use \`core.ai.chats.search\`, then \`core.ai.chat.read\` or \`core.ai.chat.search\` with the returned ID.
247
256
  - Use \`core.ai.chat.resources\` for resources from one known conversation and \`core.ai.chats.resources\` to search across conversations. Read a returned resource through its owning app rather than guessing its contents.
248
257
  - Before \`core.ai.chat.message\`, identify the exact target and message. Report queued or delivered status accurately and do not claim the target completed the requested work.
249
- - For reminders and recurring work, use \`core.ai.task.create\` with the exact runtime timezone. Ask one focused question when the schedule or intended conversation is materially ambiguous.
250
- - Use \`core.ai.tasks.list\` and \`core.ai.task.read\` before changing a task. Preserve its ID and use the focused update, pause, resume, run, or delete Action.
251
- - A scheduled run continues its conversation with the Project and permissions available at execution time; do not promise access that may later be unavailable.`;
258
+ - For one-time reminders, recurring background work, or changing and repairing a scheduled task, load the \`scheduled-tasks\` Skill.`;
259
+
260
+ const SCHEDULED_TASKS_INSTRUCTIONS = `# Schedule reminders and background work
261
+
262
+ Use this Skill to create or manage a task that runs later, once or repeatedly.
263
+ Use the current conversation unless the user names another owned chat. A simple
264
+ reminder is a valid task: its result is a normal message in that chat.
265
+
266
+ ## Load the tools for the job
267
+
268
+ - Create: \`core.ai.task.create\`.
269
+ - Find and inspect: \`core.ai.tasks.list\`, then \`core.ai.task.read\`.
270
+ - Change instructions, schedule or grants: \`core.ai.task.update\`.
271
+ - Control an existing task: \`core.ai.task.pause\`, \`core.ai.task.resume\`,
272
+ \`core.ai.task.run\`, or \`core.ai.task.delete\`, as requested.
273
+ - Diagnose an occurrence: \`core.ai.task.run.read\`, using the task and occurrence
274
+ IDs returned by task.read.
275
+
276
+ Discover and load only the needed capabilities. Read their current input schemas;
277
+ reuse returned IDs. Read an existing task before changing it. If the request may
278
+ refer to an existing schedule, list tasks before creating a duplicate.
279
+
280
+ ## Prepare the task
281
+
282
+ 1. Resolve the user's timing with the exact runtime timezone (IANA). For a
283
+ one-time reminder or delayed job use kind once and localAt; for recurrence use
284
+ kind cron and cron. Resolve relative wording against the current runtime date
285
+ and time. Ask only if a missing time, timezone or target changes the outcome.
286
+ 2. Write a self-contained prompt with the intended result, verified resource IDs
287
+ and relevant context. Use normal language for results and problems, without a
288
+ fixed return schema. A reminder prompt says what to remind the user about;
289
+ it needs no app capabilities merely to post its result to this chat.
290
+ 3. For work across apps, load the relevant domain Skills and discover every
291
+ capability needed, including searches and reads before writes. Propose grants
292
+ for those capabilities. Choose fixedInput fields from their real schemas:
293
+ constrain a specific note when the task targets it; allow a whole notebook or
294
+ unrestricted inputs when that is the intended scope. Empty fixedInput allows
295
+ any inputs within current user access. Do not invent resource IDs or claim a
296
+ notebook constraint exists on an action that accepts only a note ID.
297
+ 4. Explain the scope through the normal create/update review. Grants authorize
298
+ this task's capability use, not new access to resources. Always-approval
299
+ capabilities cannot be granted. Code Mode can run unattended on the server:
300
+ load its Skill for calculations, chat-file processing, AI tasks and exports.
301
+ Code capability calls inherit this task's grants, including fixed inputs.
302
+ HTTP and RSQL use the same grants list: kind http with fixedInput fields
303
+ origin, url and/or method; kind database with resourceId, operation and/or
304
+ table. Empty fixedInput grants all supported targets/operations within current
305
+ access. Prefer a resourceId-only database grant when a task needs connect and
306
+ multiple operations; a fixed operation also requires a separate connect grant.
307
+ HTTP secrets must already be configured in the chat or app. Interactive dialogs,
308
+ secret entry and capability binary streams are unavailable in scheduled code.
309
+ Explain those requirements instead of promising unattended interaction.
310
+ 5. Create the task. Confirm its actual saved timing, timezone and destination only
311
+ after success. Queued or manually started does not mean completed.
312
+
313
+ Examples: “Remind me tomorrow at 09:00 to call Anna” is one once task with no
314
+ capability grants. “Every weekday summarize my mail, compare it with calendar X,
315
+ and update note Y” is a recurring task with discovered read and write grants,
316
+ using verified calendar and note IDs where the respective schemas allow them.
317
+
318
+ ## Changes, errors and missing rights
319
+
320
+ Read the task and relevant failed run before proposing a repair. Distinguish
321
+ missing task grants from lost resource access and ordinary execution errors.
322
+ Update the same task's prompt, future schedule or grants; do not recreate it.
323
+ Omitted grants remain unchanged; supplied grants replace the complete list.
324
+ Expanded authority needs the normal user review and cannot restore lost resource
325
+ access. Check writes already performed before retrying with task.run to avoid
326
+ duplicates; use task.resume when the user wants future scheduled runs restored.
327
+
328
+ Each run works independently from a snapshot of the chat history at its start.
329
+ Available memories, Skills, Project context and chat files remain accessible
330
+ under runtime rules; files are live, not frozen copies. The run returns its result
331
+ or problem to the parent chat, reopening it and updating activity. The user can
332
+ keep chatting meanwhile. Do not load this management Skill on every background
333
+ run unless managing a schedule is itself needed; use the domain Skills for the
334
+ actual work.`;
252
335
 
253
336
  const CLOUD_NOTEBOOKS_REFERENCE = `## Flexible data and automatic page lists
254
337
 
@@ -351,13 +434,11 @@ Mail attachment inputs require base64: encode the exact returned calendar string
351
434
 
352
435
  Do not fabricate organizer/attendee addresses, regenerate the returned calendar text, commit after a failed attachment, or retry an unknown mutation result without reconciliation.`;
353
436
 
354
- const BUILTIN_CLOUD_AI_SKILLS: Array<Parameters<typeof aiSkills.seedOnce>[0]> = [
355
- {
356
- key: "kit:cloud-kit", name: "cloud-kit",
357
- description: "Use for Cloud Kit: programming or improving browser mini apps, file converters, JavaScript tools, workbench UIs, CSV processing or exact money calculations in an existing Kit app. Read current Kit Help before choosing SDK methods.",
358
- instructions: CLOUD_KIT_INSTRUCTIONS,
359
- },
437
+ const BUILTIN_CLOUD_AI_SKILLS: AiSkillTemplate[] = [
438
+ ASSISTANT_CODE_MODE_SKILL,
439
+ ASSISTANT_DATA_ANALYSIS_SKILL,
360
440
  {
441
+ version: 5,
361
442
  key: "grids:cloud-grids",
362
443
  name: "cloud-grids",
363
444
  description:
@@ -366,6 +447,7 @@ const BUILTIN_CLOUD_AI_SKILLS: Array<Parameters<typeof aiSkills.seedOnce>[0]> =
366
447
  references: [{ path: "references/query-tasks.md", content: CLOUD_GRIDS_QUERY_REFERENCE }],
367
448
  },
368
449
  {
450
+ version: 2,
369
451
  key: "assistant:cloud-assistant",
370
452
  name: "cloud-assistant",
371
453
  description:
@@ -373,6 +455,15 @@ const BUILTIN_CLOUD_AI_SKILLS: Array<Parameters<typeof aiSkills.seedOnce>[0]> =
373
455
  instructions: CLOUD_ASSISTANT_INSTRUCTIONS,
374
456
  },
375
457
  {
458
+ version: 2,
459
+ key: "assistant:scheduled-tasks",
460
+ name: "scheduled-tasks",
461
+ description:
462
+ "Create and manage one-time reminders, delayed jobs and recurring scheduled background tasks in Cloud Assistant. Use for requests such as remind me tomorrow, check in an hour, every morning, or changing, pausing, resuming and repairing a scheduled task, including missing permissions or failed runs.",
463
+ instructions: SCHEDULED_TASKS_INSTRUCTIONS,
464
+ },
465
+ {
466
+ version: 2,
376
467
  key: "core:skill-creator",
377
468
  name: "skill-creator",
378
469
  description:
@@ -380,6 +471,7 @@ const BUILTIN_CLOUD_AI_SKILLS: Array<Parameters<typeof aiSkills.seedOnce>[0]> =
380
471
  instructions: SKILL_CREATOR_INSTRUCTIONS,
381
472
  },
382
473
  {
474
+ version: 1,
383
475
  key: "mail:cloud-mail",
384
476
  name: "cloud-mail",
385
477
  description:
@@ -387,6 +479,7 @@ const BUILTIN_CLOUD_AI_SKILLS: Array<Parameters<typeof aiSkills.seedOnce>[0]> =
387
479
  instructions: CLOUD_MAIL_INSTRUCTIONS,
388
480
  },
389
481
  {
482
+ version: 1,
390
483
  key: "notebooks:cloud-notebooks",
391
484
  name: "cloud-notebooks",
392
485
  description:
@@ -395,6 +488,7 @@ const BUILTIN_CLOUD_AI_SKILLS: Array<Parameters<typeof aiSkills.seedOnce>[0]> =
395
488
  references: [{ path: "references/structured-pages.md", content: CLOUD_NOTEBOOKS_REFERENCE }],
396
489
  },
397
490
  {
491
+ version: 1,
398
492
  key: "contacts:cloud-contacts",
399
493
  name: "cloud-contacts",
400
494
  description:
@@ -402,6 +496,7 @@ const BUILTIN_CLOUD_AI_SKILLS: Array<Parameters<typeof aiSkills.seedOnce>[0]> =
402
496
  instructions: CLOUD_CONTACTS_INSTRUCTIONS,
403
497
  },
404
498
  {
499
+ version: 1,
405
500
  key: "spaces:cloud-spaces",
406
501
  name: "cloud-spaces",
407
502
  description:
@@ -410,6 +505,7 @@ const BUILTIN_CLOUD_AI_SKILLS: Array<Parameters<typeof aiSkills.seedOnce>[0]> =
410
505
  references: [{ path: "references/calendar-mail.md", content: CLOUD_SPACES_CALENDAR_REFERENCE }],
411
506
  },
412
507
  {
508
+ version: 1,
413
509
  key: "weather:cloud-weather",
414
510
  name: "cloud-weather",
415
511
  description:
@@ -418,13 +514,11 @@ const BUILTIN_CLOUD_AI_SKILLS: Array<Parameters<typeof aiSkills.seedOnce>[0]> =
418
514
  },
419
515
  ];
420
516
 
517
+ export const getBuiltinAiSkillTemplates = (): readonly AiSkillTemplate[] => BUILTIN_CLOUD_AI_SKILLS;
518
+
421
519
  export const getBuiltinAiSkillTemplate = (name: string) => {
422
520
  const seed = BUILTIN_CLOUD_AI_SKILLS.find((entry) => entry.name === name);
423
521
  if (!seed) return undefined;
424
- const { key: _key, ...template } = seed;
425
- return template;
426
- };
427
-
428
- export const seedCloudAiSkills = async (): Promise<void> => {
429
- for (const seed of BUILTIN_CLOUD_AI_SKILLS) await aiSkills.seedOnce(seed);
522
+ const { key, ...template } = seed;
523
+ return { ...template, templateId: key };
430
524
  };