@k2b/cloud 0.7.0 → 0.9.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 (190) hide show
  1. package/package.json +5 -4
  2. package/scripts/README.md +10 -0
  3. package/scripts/build.ts +14 -1
  4. package/scripts/preload.ts +7 -0
  5. package/scripts/sync-recovery-smoke.ts +21 -6
  6. package/src/_internal/build-metadata.ts +8 -1
  7. package/src/_internal/capabilities.ts +35 -12
  8. package/src/_internal/define-app.ts +28 -11
  9. package/src/_internal/page-responses.ts +1 -1
  10. package/src/_internal/readiness.ts +4 -1
  11. package/src/_internal/runtime-context.ts +1 -1
  12. package/src/access/PermissionEditor.tsx +15 -9
  13. package/src/access/PrincipalPicker.tsx +9 -3
  14. package/src/access/ResourceApiKeys.tsx +1 -1
  15. package/src/access/ui.ts +1 -2
  16. package/src/ai/admin.ts +2 -2
  17. package/src/ai/assistant-models.ts +1 -1
  18. package/src/ai/audio-tool.ts +1 -1
  19. package/src/ai/browser-code-contracts.ts +174 -52
  20. package/src/ai/browser.ts +4 -2
  21. package/src/ai/capabilities.ts +3 -3
  22. package/src/ai/chat/blocks.tsx +127 -50
  23. package/src/ai/chat/builtin-tools.tsx +2 -4
  24. package/src/ai/chat/capability-table.tsx +1 -1
  25. package/src/ai/chat/composer-adapter.ts +48 -13
  26. package/src/ai/chat/message-utils.ts +18 -5
  27. package/src/ai/chat/presentation.tsx +24 -11
  28. package/src/ai/chat/tool-disclosure.tsx +6 -2
  29. package/src/ai/chat/tool-groups.ts +8 -2
  30. package/src/ai/chat-quotas.ts +2 -2
  31. package/src/ai/chat-task-contracts.ts +9 -13
  32. package/src/ai/chat-tasks.ts +39 -18
  33. package/src/ai/client/controller.ts +87 -38
  34. package/src/ai/client/projection.ts +13 -6
  35. package/src/ai/client/transport.ts +4 -2
  36. package/src/ai/code-capability-routes.ts +22 -27
  37. package/src/ai/code-capability-transport.ts +2 -1
  38. package/src/ai/code-execution.ts +53 -0
  39. package/src/ai/code-mode-skill.ts +9 -5
  40. package/src/ai/code-runtime-tools.ts +16 -3
  41. package/src/ai/code-source-contracts.ts +100 -32
  42. package/src/ai/code-source-tools.ts +49 -6
  43. package/src/ai/compaction.ts +3 -3
  44. package/src/ai/default-tools.ts +87 -14
  45. package/src/ai/dictation-runtime.ts +1 -1
  46. package/src/ai/draft-content.ts +27 -12
  47. package/src/ai/enrich.ts +1 -1
  48. package/src/ai/executor.ts +8 -12
  49. package/src/ai/fetch-file-tool.ts +2 -2
  50. package/src/ai/file-content-version.ts +6 -1
  51. package/src/ai/file-tools.ts +8 -2
  52. package/src/ai/files-store.ts +55 -17
  53. package/src/ai/grids-skill.ts +2 -0
  54. package/src/ai/http.ts +18 -7
  55. package/src/ai/index.ts +24 -25
  56. package/src/ai/inference-calls.ts +3 -4
  57. package/src/ai/live.ts +1 -1
  58. package/src/ai/markdown-pdf-tool.ts +2 -2
  59. package/src/ai/memories.ts +30 -27
  60. package/src/ai/memory-learning-runs.ts +1 -1
  61. package/src/ai/memory-learning.ts +5 -5
  62. package/src/ai/memory-tool.ts +1 -5
  63. package/src/ai/memory-workflow-evidence.ts +9 -4
  64. package/src/ai/message-queue.ts +23 -12
  65. package/src/ai/migrate.ts +6 -4
  66. package/src/ai/model-pricing.ts +4 -4
  67. package/src/ai/pdf-render-worker.ts +3 -2
  68. package/src/ai/projects-routes.ts +15 -40
  69. package/src/ai/projects.ts +18 -13
  70. package/src/ai/quota-provider.ts +6 -5
  71. package/src/ai/quota-report.ts +5 -5
  72. package/src/ai/quotas.ts +3 -3
  73. package/src/ai/routes.ts +60 -34
  74. package/src/ai/run-timeout.ts +3 -1
  75. package/src/ai/runtime-tools.ts +6 -7
  76. package/src/ai/runtime.ts +38 -20
  77. package/src/ai/settings.ts +1 -1
  78. package/src/ai/short-id.ts +4 -5
  79. package/src/ai/skill-search.ts +21 -10
  80. package/src/ai/skill-seeds.ts +16 -7
  81. package/src/ai/skill-tool.ts +6 -1
  82. package/src/ai/skills-routes.ts +32 -15
  83. package/src/ai/skills.ts +54 -28
  84. package/src/ai/store.ts +80 -47
  85. package/src/ai/structured.ts +6 -2
  86. package/src/ai/system-prompt.ts +7 -5
  87. package/src/ai/task-contracts.ts +84 -0
  88. package/src/ai/task-execution.ts +92 -0
  89. package/src/ai/todo-contracts.ts +24 -12
  90. package/src/ai/todo-tool.ts +16 -13
  91. package/src/ai/tools.ts +1 -1
  92. package/src/ai/transcription.ts +4 -4
  93. package/src/ai/turn-timing.ts +1 -1
  94. package/src/ai/ui.tsx +2 -1
  95. package/src/ai/usage.ts +1 -1
  96. package/src/ai/vision-tool.ts +38 -17
  97. package/src/api/admin-ai-quotas.ts +14 -8
  98. package/src/api/admin-ai-skills.ts +3 -3
  99. package/src/api/admin-linux-identities.ts +10 -4
  100. package/src/api/admin-rail.ts +2 -2
  101. package/src/api/app-approval.ts +2 -2
  102. package/src/api/auth/schemas.ts +7 -1
  103. package/src/api/auth.ts +4 -3
  104. package/src/api/capabilities.ts +45 -13
  105. package/src/api/index.ts +5 -5
  106. package/src/api/mcp.ts +5 -12
  107. package/src/browser/CloudResourceSearch.tsx +1 -1
  108. package/src/browser/command-bridge.ts +1 -1
  109. package/src/browser/command-shortcuts.ts +1 -1
  110. package/src/browser/commands.ts +8 -7
  111. package/src/browser/testing.ts +2 -1
  112. package/src/capabilities/client.ts +1 -1
  113. package/src/capabilities/command-link.ts +1 -1
  114. package/src/capabilities/server.ts +2 -1
  115. package/src/cli/admin/ai-quotas.ts +5 -5
  116. package/src/cli/admin/app-credentials.ts +7 -9
  117. package/src/cli/admin/index.ts +3 -3
  118. package/src/cli/admin/instance.ts +2 -2
  119. package/src/cli/admin/linux.ts +4 -2
  120. package/src/cli/admin/sync.ts +1 -1
  121. package/src/cli/capabilities.ts +42 -29
  122. package/src/config/define-env.ts +97 -0
  123. package/src/config/env.ts +147 -54
  124. package/src/config/index.ts +3 -1
  125. package/src/contracts/capabilities.ts +8 -1
  126. package/src/contracts/index.ts +5 -7
  127. package/src/contracts/registry.ts +2 -0
  128. package/src/contracts/shared.ts +12 -3
  129. package/src/desktop/index.ts +1 -0
  130. package/src/desktop/solid.tsx +1 -1
  131. package/src/index.ts +8 -2
  132. package/src/server/app-assets.ts +27 -0
  133. package/src/server/index.ts +3 -2
  134. package/src/server/ratelimit.ts +1 -1
  135. package/src/server/services/access-revision.ts +3 -3
  136. package/src/server/services/access.ts +3 -1
  137. package/src/services/accounts/notification-sender.ts +12 -2
  138. package/src/services/announcements/index.ts +4 -4
  139. package/src/services/app-approval.ts +11 -10
  140. package/src/services/auth-flows/magic-link.ts +13 -1
  141. package/src/services/identity/key-config.ts +6 -4
  142. package/src/services/identity/runtime-config.ts +6 -5
  143. package/src/services/ipa/posix.ts +1 -1
  144. package/src/services/ipa/sync.ts +1 -1
  145. package/src/services/logging/index.ts +1 -1
  146. package/src/services/logging/trace.ts +1 -1
  147. package/src/services/mandates/index.ts +5 -2
  148. package/src/services/mandates/policy.ts +106 -6
  149. package/src/services/notifications/batches.ts +1 -0
  150. package/src/services/pdf/gotenberg.ts +50 -14
  151. package/src/services/pdf/index.ts +11 -11
  152. package/src/services/public-http.ts +1 -1
  153. package/src/services/rail-shortcuts.ts +1 -1
  154. package/src/services/rail-snapshot.ts +1 -1
  155. package/src/services/request-cache-redis.ts +2 -1
  156. package/src/services/session/user.ts +1 -1
  157. package/src/services/settings/app.ts +5 -1
  158. package/src/services/settings/core-settings.ts +6 -8
  159. package/src/services/settings/index.ts +3 -3
  160. package/src/services/settings/store.ts +3 -3
  161. package/src/services/webauthn.ts +1 -1
  162. package/src/shared/index.ts +3 -6
  163. package/src/shared/markdown/extensions/info-blocks.ts +1 -1
  164. package/src/ssr/AdminSidebar.tsx +2 -2
  165. package/src/ssr/AppLaunchpad.island.tsx +12 -2
  166. package/src/ssr/AppLaunchpadPanel.tsx +4 -4
  167. package/src/ssr/GlobalSearchDialog.tsx +6 -6
  168. package/src/ssr/GlobalSearchTrigger.island.tsx +2 -2
  169. package/src/ssr/Layout.tsx +4 -3
  170. package/src/ssr/LayoutHeader.tsx +1 -1
  171. package/src/ssr/LayoutHelp.tsx +4 -6
  172. package/src/ssr/LayoutHelpTrigger.island.tsx +2 -2
  173. package/src/ssr/LayoutRail.tsx +3 -2
  174. package/src/ssr/MinimalLayout.tsx +1 -1
  175. package/src/ssr/MobileNavigation.tsx +2 -2
  176. package/src/ssr/ProfilePreferences.island.tsx +1 -1
  177. package/src/ssr/RailApps.island.tsx +7 -7
  178. package/src/ssr/admin-navigation.ts +6 -2
  179. package/src/ssr/index.ts +1 -0
  180. package/src/ssr/islands/index.ts +2 -2
  181. package/src/ssr/mobile-menu-history.ts +1 -1
  182. package/src/ssr/rail-navigation.ts +3 -2
  183. package/src/ssr/request-path.ts +12 -0
  184. package/src/workflows/ai/index.ts +2 -2
  185. package/src/workflows/ai/runtime.ts +16 -91
  186. package/src/workflows/ai/store.ts +1 -1
  187. package/src/workflows/ai/types.ts +6 -82
  188. package/src/workflows/store/index.ts +2 -2
  189. package/src/server/services/freeipa/test-certificates.ts +0 -44
  190. package/src/services/session/test-fixture.ts +0 -18
@@ -1,11 +1,11 @@
1
1
  import { sql } from "bun";
2
2
  import { coreSettings } from "../services";
3
- import { decryptValue } from "../services/settings/crypto";
4
- import { invalidateSettingsCache, set as setSetting } from "../services/settings";
5
3
  import { audit } from "../services/audit";
6
- import { AiModelPricingSchema, type AiModelPricing } from "../shared/ai-costs";
7
- import { parseAiModelProfiles } from "./settings";
4
+ import { invalidateSettingsCache, set as setSetting } from "../services/settings";
5
+ import { decryptValue } from "../services/settings/crypto";
6
+ import { type AiModelPricing, AiModelPricingSchema } from "../shared/ai-costs";
8
7
  import { AiQuotaError } from "./quotas";
8
+ import { parseAiModelProfiles } from "./settings";
9
9
 
10
10
  /** Change prices only, preserving credentials and model access in their own stores. */
11
11
  export async function setAiModelPricing(id: string, pricing: AiModelPricing | null, expected: AiModelPricing | null, actorId: string) {
@@ -1,9 +1,10 @@
1
1
  // Standalone subprocess. No server imports or application credentials.
2
- import { dirname, join } from "node:path";
2
+
3
3
  import { createRequire } from "node:module";
4
- import { z } from "zod";
4
+ import { dirname, join } from "node:path";
5
5
  import { createCanvas } from "@napi-rs/canvas";
6
6
  import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
7
+ import { z } from "zod";
7
8
 
8
9
  const MAX_BYTES = 10 * 1024 * 1024;
9
10
  const inputSchema = z.object({
@@ -71,16 +71,12 @@ const buildAiProjectsRoutes = (dependencies: AiProjectsRouteDependencies = {}) =
71
71
  new Hono<AuthContext>()
72
72
  .use(dependencies.limit ?? rateLimit())
73
73
  .use("*", dependencies.authenticate ?? auth.requireRole("authenticated"))
74
- .get("/", async (c) =>
75
- respond(c, ok({ projects: (await aiProjects.list(c.get("accessSubject") ?? null)).map(publicProject) })),
76
- )
74
+ .get("/", async (c) => respond(c, ok({ projects: (await aiProjects.list(c.get("accessSubject") ?? null)).map(publicProject) })))
77
75
  .post("/", dependencies.authenticate ?? auth.requireRole("authenticated"), v("json", ProjectFieldsSchema), async (c) =>
78
76
  respond(
79
77
  c,
80
78
  ok({
81
- project: publicProject(
82
- await aiProjects.create({ subject: c.get("accessSubject"), ...c.req.valid("json") }),
83
- ),
79
+ project: publicProject(await aiProjects.create({ subject: c.get("accessSubject"), ...c.req.valid("json") })),
84
80
  }),
85
81
  201,
86
82
  ),
@@ -108,9 +104,7 @@ const buildAiProjectsRoutes = (dependencies: AiProjectsRouteDependencies = {}) =
108
104
  })
109
105
  .post("/:projectId/access", v("json", ProjectAccessSchema), async (c) => {
110
106
  const project = await aiProjects.getByShortId(c.req.param("projectId")!, c.get("accessSubject") ?? null, "admin");
111
- const access = project
112
- ? await aiProjects.grantAccess(project.id, c.get("accessSubject") ?? null, c.req.valid("json"))
113
- : null;
107
+ const access = project ? await aiProjects.grantAccess(project.id, c.get("accessSubject") ?? null, c.req.valid("json")) : null;
114
108
  return access ? respond(c, ok({ access }), 201) : notFound(c);
115
109
  })
116
110
  .patch("/:projectId/access/:accessId", v("json", ProjectAccessUpdateSchema), async (c) => {
@@ -133,8 +127,7 @@ const buildAiProjectsRoutes = (dependencies: AiProjectsRouteDependencies = {}) =
133
127
  .delete("/:projectId/access/:accessId", async (c) => {
134
128
  const project = await aiProjects.getByShortId(c.req.param("projectId")!, c.get("accessSubject") ?? null, "admin");
135
129
  try {
136
- return project &&
137
- (await aiProjects.revokeAccess(project.id, c.req.param("accessId")!, c.get("accessSubject") ?? null))
130
+ return project && (await aiProjects.revokeAccess(project.id, c.req.param("accessId")!, c.get("accessSubject") ?? null))
138
131
  ? respond(c, ok({ deleted: true }))
139
132
  : notFound(c, "Access entry");
140
133
  } catch (error) {
@@ -148,35 +141,27 @@ const buildAiProjectsRoutes = (dependencies: AiProjectsRouteDependencies = {}) =
148
141
  return respond(
149
142
  c,
150
143
  ok({
151
- knowledge: (
152
- await aiProjects.listKnowledge(project.id, c.get("accessSubject") ?? null, c.req.valid("query").q)
153
- ).map((item) => publicKnowledge(item, project.shortId)),
144
+ knowledge: (await aiProjects.listKnowledge(project.id, c.get("accessSubject") ?? null, c.req.valid("query").q)).map((item) =>
145
+ publicKnowledge(item, project.shortId),
146
+ ),
154
147
  }),
155
148
  );
156
149
  })
157
150
  .post("/:projectId/knowledge", v("json", KnowledgeSchema), async (c) => {
158
151
  const project = await aiProjects.getByShortId(c.req.param("projectId")!, c.get("accessSubject") ?? null, "write");
159
- const knowledge = project
160
- ? await aiProjects.createKnowledge(project.id, c.get("accessSubject") ?? null, c.req.valid("json"))
161
- : null;
152
+ const knowledge = project ? await aiProjects.createKnowledge(project.id, c.get("accessSubject") ?? null, c.req.valid("json")) : null;
162
153
  return knowledge ? respond(c, ok({ knowledge: publicKnowledge(knowledge, project!.shortId) }), 201) : notFound(c);
163
154
  })
164
155
  .patch("/:projectId/knowledge/:knowledgeId", v("json", UpdateKnowledgeSchema), async (c) => {
165
156
  const project = await aiProjects.getByShortId(c.req.param("projectId")!, c.get("accessSubject") ?? null, "write");
166
157
  const knowledge =
167
158
  project &&
168
- (await aiProjects.updateKnowledge(
169
- project.id,
170
- c.req.param("knowledgeId")!,
171
- c.get("accessSubject") ?? null,
172
- c.req.valid("json"),
173
- ));
159
+ (await aiProjects.updateKnowledge(project.id, c.req.param("knowledgeId")!, c.get("accessSubject") ?? null, c.req.valid("json")));
174
160
  return knowledge ? respond(c, ok({ knowledge: publicKnowledge(knowledge, project!.shortId) })) : notFound(c, "Knowledge entry");
175
161
  })
176
162
  .delete("/:projectId/knowledge/:knowledgeId", async (c) => {
177
163
  const project = await aiProjects.getByShortId(c.req.param("projectId")!, c.get("accessSubject") ?? null, "write");
178
- return project &&
179
- (await aiProjects.deleteKnowledge(project.id, c.req.param("knowledgeId")!, c.get("accessSubject") ?? null))
164
+ return project && (await aiProjects.deleteKnowledge(project.id, c.req.param("knowledgeId")!, c.get("accessSubject") ?? null))
180
165
  ? respond(c, ok({ deleted: true }))
181
166
  : notFound(c, "Knowledge entry");
182
167
  })
@@ -186,20 +171,14 @@ const buildAiProjectsRoutes = (dependencies: AiProjectsRouteDependencies = {}) =
186
171
  return respond(
187
172
  c,
188
173
  ok({
189
- files: (await aiProjects.listFiles(project.id, c.get("accessSubject") ?? null)).map((file) =>
190
- publicFile(file, project.shortId),
191
- ),
174
+ files: (await aiProjects.listFiles(project.id, c.get("accessSubject") ?? null)).map((file) => publicFile(file, project.shortId)),
192
175
  }),
193
176
  );
194
177
  })
195
178
  .post("/:projectId/files", v("json", ProjectFileSchema), async (c) => {
196
179
  const body = c.req.valid("json");
197
180
  try {
198
- const project = await aiProjects.getByShortId(
199
- c.req.param("projectId")!,
200
- c.get("accessSubject") ?? null,
201
- "write",
202
- );
181
+ const project = await aiProjects.getByShortId(c.req.param("projectId")!, c.get("accessSubject") ?? null, "write");
203
182
  const file = project
204
183
  ? await aiProjects.writeFile(project.id, c.get("accessSubject") ?? null, {
205
184
  path: body.path,
@@ -214,9 +193,7 @@ const buildAiProjectsRoutes = (dependencies: AiProjectsRouteDependencies = {}) =
214
193
  })
215
194
  .get("/:projectId/files/:fileId", async (c) => {
216
195
  const project = await aiProjects.getByShortId(c.req.param("projectId")!, c.get("accessSubject") ?? null);
217
- const file = project
218
- ? await aiProjects.readFile(project.id, c.req.param("fileId")!, c.get("accessSubject") ?? null)
219
- : null;
196
+ const file = project ? await aiProjects.readFile(project.id, c.req.param("fileId")!, c.get("accessSubject") ?? null) : null;
220
197
  if (!file) return notFound(c, "Project file");
221
198
  return respond(
222
199
  c,
@@ -229,8 +206,7 @@ const buildAiProjectsRoutes = (dependencies: AiProjectsRouteDependencies = {}) =
229
206
  })
230
207
  .delete("/:projectId/files/:fileId", async (c) => {
231
208
  const project = await aiProjects.getByShortId(c.req.param("projectId")!, c.get("accessSubject") ?? null, "write");
232
- return project &&
233
- (await aiProjects.deleteFile(project.id, c.req.param("fileId")!, c.get("accessSubject") ?? null))
209
+ return project && (await aiProjects.deleteFile(project.id, c.req.param("fileId")!, c.get("accessSubject") ?? null))
234
210
  ? respond(c, ok({ deleted: true }))
235
211
  : notFound(c, "Project file");
236
212
  })
@@ -265,8 +241,7 @@ const buildAiProjectsRoutes = (dependencies: AiProjectsRouteDependencies = {}) =
265
241
  })
266
242
  .delete("/:projectId/references/:referenceId", async (c) => {
267
243
  const project = await aiProjects.getByShortId(c.req.param("projectId")!, c.get("accessSubject") ?? null, "write");
268
- return project &&
269
- (await aiProjects.deleteReference(project.id, c.req.param("referenceId")!, c.get("accessSubject") ?? null))
244
+ return project && (await aiProjects.deleteReference(project.id, c.req.param("referenceId")!, c.get("accessSubject") ?? null))
270
245
  ? respond(c, ok({ deleted: true }))
271
246
  : notFound(c, "Project reference");
272
247
  });
@@ -1,7 +1,6 @@
1
- import { AuthenticatedPrincipalSchema } from "../contracts/shared";
2
- import { aiFileContentVersion, AiFileWriteError, AiFileVersionConflict } from "./file-content-version";
3
1
  import { type SQL, type SQLQuery, sql } from "bun";
4
2
  import type { CloudResourceRef } from "../contracts/capabilities";
3
+ import { AuthenticatedPrincipalSchema } from "../contracts/shared";
5
4
  import type { AccessSubject } from "../server";
6
5
  import {
7
6
  buildAccessPrincipalCondition,
@@ -12,6 +11,7 @@ import {
12
11
  type Principal,
13
12
  } from "../server/services/access";
14
13
  import { toPgUuidArray } from "../services/postgres";
14
+ import { AiFileVersionConflict, AiFileWriteError, aiFileContentVersion } from "./file-content-version";
15
15
  import { mountAiProjectFilePath } from "./file-mount";
16
16
  import { withAiShortIdForDb } from "./short-id";
17
17
  import type { AiProjectPromptSnapshot } from "./types";
@@ -170,15 +170,17 @@ const principalForSubject = (subject: AccessSubject): Principal =>
170
170
  : { type: "service_account", serviceAccountId: subject.serviceAccountId };
171
171
 
172
172
  const accessMatch = (subject: AccessSubject | null): SQLQuery =>
173
- subject === null ? sql`FALSE` : buildAccessPrincipalCondition({
174
- subject,
175
- columns: {
176
- userId: sql`access.user_id`,
177
- groupId: sql`access.group_id`,
178
- serviceAccountId: sql`access.service_account_id`,
179
- authenticatedOnly: sql`access.authenticated_only`,
180
- },
181
- });
173
+ subject === null
174
+ ? sql`FALSE`
175
+ : buildAccessPrincipalCondition({
176
+ subject,
177
+ columns: {
178
+ userId: sql`access.user_id`,
179
+ groupId: sql`access.group_id`,
180
+ serviceAccountId: sql`access.service_account_id`,
181
+ authenticatedOnly: sql`access.authenticated_only`,
182
+ },
183
+ });
182
184
 
183
185
  const permissionFor = async (row: ProjectRow, subject: AccessSubject | null, db: SQL = sql): Promise<AiProjectPermission | "none"> => {
184
186
  const match = accessMatch(subject);
@@ -805,14 +807,17 @@ export const aiProjects = {
805
807
  input: { path: string; mediaType: string; bytes: Uint8Array; expectedVersion?: string | null },
806
808
  ): Promise<AiProjectFile | null> {
807
809
  if (!(await requireProject(projectId, subject, "write"))) return null;
808
- if (input.bytes.byteLength > AI_PROJECT_FILE_MAX_BYTES) throw new AiFileWriteError("STORAGE_FULL", "Project file exceeds the size limit; nothing was written.");
810
+ if (input.bytes.byteLength > AI_PROJECT_FILE_MAX_BYTES)
811
+ throw new AiFileWriteError("STORAGE_FULL", "Project file exceeds the size limit; nothing was written.");
809
812
  const path = normalizeProjectPath(input.path);
810
813
  return sql.begin(async (tx) => {
811
814
  const [row] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id=${projectId}::uuid FOR UPDATE`;
812
815
  const project = row ? await toProject(row, subject, tx) : null;
813
816
  if (!project || !hasPermission(project.permission, "write")) return null;
814
817
  if (input.expectedVersion !== undefined) {
815
- const [existing] = await tx<(FileRow & { bytes: Uint8Array })[]>`SELECT id,short_id,project_id,path,media_type,size,updated_at,bytes FROM ai.project_files WHERE project_id=${projectId}::uuid AND path=${path}`;
818
+ const [existing] = await tx<
819
+ (FileRow & { bytes: Uint8Array })[]
820
+ >`SELECT id,short_id,project_id,path,media_type,size,updated_at,bytes FROM ai.project_files WHERE project_id=${projectId}::uuid AND path=${path}`;
816
821
  const version = existing ? aiFileContentVersion({ ...toFile(existing), bytes: existing.bytes }) : null;
817
822
  if (version !== input.expectedVersion) throw new AiFileVersionConflict();
818
823
  }
@@ -1,11 +1,12 @@
1
- import { isAssistantChatTurn } from "./assistant-models";
1
+ import { setTimeout as delay } from "node:timers/promises";
2
2
  import { estimateTokens, type Provider } from "@k2b/nessi";
3
- import type { AccessSubject } from "../server/services/access";
4
3
  import { sql } from "bun";
5
- import { setTimeout as delay } from "node:timers/promises";
6
- import { AiBackgroundAdmissionError, beginAiCall, finishAiCall, type AiCallContext } from "./inference-calls";
7
- import type { AiModelProfile } from "./types";
4
+ import type { AccessSubject } from "../server/services/access";
8
5
  import { logger } from "../services/logging";
6
+ import { isAssistantChatTurn } from "./assistant-models";
7
+ import { AiBackgroundAdmissionError, type AiCallContext, beginAiCall, finishAiCall } from "./inference-calls";
8
+ import type { AiModelProfile } from "./types";
9
+
9
10
  const log = logger("ai:quotas");
10
11
 
11
12
  /** One wrapper owns call accounting. complete() can have a separate compaction purpose. */
@@ -1,11 +1,11 @@
1
- import { hasBillableAiPricing } from "../shared/ai-costs";
2
- import { readAiSettingsState } from "./settings";
3
- import { toPgTextArray } from "../services/postgres";
4
1
  import { sql } from "bun";
5
- import { recursiveGroupIdsSubquery } from "../services/accounts/group-sql";
6
- import { AiQuotaReportQuerySchema, type AiQuotaReport, type AiQuotaReportQuery } from "../shared/ai-quotas";
7
2
  import { resolveDisplayNames } from "../server/services/access";
3
+ import { recursiveGroupIdsSubquery } from "../services/accounts/group-sql";
4
+ import { toPgTextArray } from "../services/postgres";
5
+ import { hasBillableAiPricing } from "../shared/ai-costs";
6
+ import { type AiQuotaReport, type AiQuotaReportQuery, AiQuotaReportQuerySchema } from "../shared/ai-quotas";
8
7
  import { aiQuotas } from "./quotas";
8
+ import { readAiSettingsState } from "./settings";
9
9
 
10
10
  /** Resolve display metadata only for administration, never on the inference path. */
11
11
  export async function quotaAdminConfig() {
package/src/ai/quotas.ts CHANGED
@@ -1,8 +1,8 @@
1
+ import { type SQL, sql } from "bun";
2
+ import { type AccessSubject, buildAccessPrincipalCondition } from "../server/services/access";
1
3
  import { hasBillableAiPricing } from "../shared/ai-costs";
4
+ import { type AiQuotaConfig, AiQuotaConfigSchema, type AiQuotaIdentity, type AiQuotaSnapshot } from "../shared/ai-quotas";
2
5
  import { readAiSettingsState } from "./settings";
3
- import { sql, type SQL } from "bun";
4
- import { buildAccessPrincipalCondition, type AccessSubject } from "../server/services/access";
5
- import { AiQuotaConfigSchema, type AiQuotaConfig, type AiQuotaSnapshot, type AiQuotaIdentity } from "../shared/ai-quotas";
6
6
 
7
7
  export class AiQuotaError extends Error {
8
8
  constructor(
package/src/ai/routes.ts CHANGED
@@ -1,13 +1,5 @@
1
- import { getAiChatQuotas } from "./chat-quotas";
2
- import { enqueueChatMessage, listQueuedMessages, updateQueuedMessage, editQueuedMessage, queuedMessageAccepted, AiMessageQueueConflict } from "./message-queue";
3
- import { prepareAiChatTurn, wakeAiMessageQueue } from "./runtime";
4
- import { bodyLimit } from "hono/body-limit";
5
- import { AI_AUDIO_MAX_BYTES } from "./audio-format";
6
- import { aiDictations, AiDictationConflict, AiDictationStartSchema, AiDictationListSchema } from "./dictations";
7
- import { enqueueAiDictation } from "./dictation-runtime";
8
- import { resolveAiAudioModel } from "./transcription";
9
- import { aiModelAccess } from "./model-access";
10
1
  import { type Context, Hono } from "hono";
2
+ import { bodyLimit } from "hono/body-limit";
11
3
  import { z } from "zod";
12
4
  import { listCapabilities } from "../_internal/registry";
13
5
  import {
@@ -24,12 +16,16 @@ import {
24
16
  respond,
25
17
  v,
26
18
  } from "../server";
27
- import { coreSettings } from "../services/settings/api";
28
19
  import { logger } from "../services/logging";
20
+ import { coreSettings } from "../services/settings/api";
29
21
  import type { AiToolApprovalContext } from "./approvals";
30
22
  import { assistantAiSettingsState, listAssistantAiModels, selectAssistantAiModelId } from "./assistant-models";
23
+ import { AI_AUDIO_MAX_BYTES } from "./audio-format";
31
24
  import { buildAiCapabilityCatalog } from "./capabilities";
25
+ import { getAiChatQuotas } from "./chat-quotas";
32
26
  import { createConfiguredDefaultCloudAiTools } from "./default-tools";
27
+ import { enqueueAiDictation } from "./dictation-runtime";
28
+ import { AiDictationConflict, AiDictationListSchema, AiDictationStartSchema, aiDictations } from "./dictations";
33
29
  import { aiProjectFilePathFromMount } from "./file-mount";
34
30
  import { AI_FILES_MAX_FILE_BYTES_DEFAULT, aiFileStore, decodeAiFileContent, guessAiMediaType, normalizeAiFilePath } from "./files-store";
35
31
  import {
@@ -51,6 +47,15 @@ import { aiMaintenanceJobs } from "./maintenance";
51
47
  import { AI_MEMORY_CONTENT_MAX_CHARS, aiMemories } from "./memories";
52
48
  import { aiMemoryLearningRuns } from "./memory-learning-runs";
53
49
  import { createCloudAiMemoryTool } from "./memory-tool";
50
+ import {
51
+ AiMessageQueueConflict,
52
+ editQueuedMessage,
53
+ enqueueChatMessage,
54
+ listQueuedMessages,
55
+ queuedMessageAccepted,
56
+ updateQueuedMessage,
57
+ } from "./message-queue";
58
+ import { aiModelAccess } from "./model-access";
54
59
  import { personalAiModelPolicy } from "./personal-agent";
55
60
  import { aiActorUser, aiPrefsUserId, aiUserPrefs } from "./prefs";
56
61
  import { aiProjects } from "./projects";
@@ -61,9 +66,11 @@ import {
61
66
  AiTurnActionSchema,
62
67
  abortAiTurn,
63
68
  listPendingAiTurnActions,
69
+ prepareAiChatTurn,
64
70
  submitAiChatTurn,
65
71
  submitAiCompaction,
66
72
  submitAiTurnAction,
73
+ wakeAiMessageQueue,
67
74
  } from "./runtime";
68
75
  import { readAiSettingsState, selectAiModelProfile } from "./settings";
69
76
  import { AI_SHORT_ID_PATTERN } from "./short-id";
@@ -74,6 +81,7 @@ import { aiConversations } from "./store";
74
81
  import { createAiConversationStreamResponse, loadAiStreamState } from "./stream";
75
82
  import { composeAiSystemPrompt } from "./system-prompt";
76
83
  import { aiToolPromptHints } from "./tools";
84
+ import { resolveAiAudioModel } from "./transcription";
77
85
  import type { AiConversation, AiModelPolicy, AiTurn } from "./types";
78
86
 
79
87
  /** Everything a resolved, authorized request needs to run against the shared runtime. */
@@ -574,7 +582,7 @@ export const aiRoutes = (() => {
574
582
  if (ctx instanceof Response) return ctx;
575
583
  const conversation = await loadConversation(c, ctx);
576
584
  if (!conversation) return notFound(c);
577
- const messageId = (c.req.param("messageId") ?? "");
585
+ const messageId = c.req.param("messageId") ?? "";
578
586
  if (!messageId) return respond(c, fail(err.badInput("Invalid message id.")));
579
587
  const input = c.req.valid("json");
580
588
  const feedback = await aiConversations.setMessageFeedback({
@@ -593,7 +601,7 @@ export const aiRoutes = (() => {
593
601
  if (ctx instanceof Response) return ctx;
594
602
  const conversation = await loadConversation(c, ctx);
595
603
  if (!conversation) return notFound(c);
596
- const messageId = (c.req.param("messageId") ?? "");
604
+ const messageId = c.req.param("messageId") ?? "";
597
605
  if (!messageId) return respond(c, fail(err.badInput("Invalid message id.")));
598
606
  return (await aiConversations.clearMessageFeedback({
599
607
  conversationId: conversation.id,
@@ -766,30 +774,36 @@ export const aiRoutes = (() => {
766
774
  const conversation = await loadConversation(c, ctx);
767
775
  return conversation ? respond(c, ok(await listQueuedMessages(conversation.id))) : notFound(c);
768
776
  })
769
- .patch("/conversations/:conversationId/queue/:messageId", v("json", z.object({text:z.string().trim().min(1).max(20000)}).strict()), async (c) => {
770
- const ctx = await resolveContext(c);
771
- if (ctx instanceof Response) return ctx;
772
- const conversation = await loadConversation(c, ctx);
773
- if (!conversation || !z.uuid().safeParse(c.req.param("messageId")).success) return notFound(c);
774
- return await editQueuedMessage(conversation.id,(c.req.param("messageId") ?? ""),c.req.valid("json").text) ? respond(c,ok({ok:true})) : notFound(c);
775
- })
777
+ .patch(
778
+ "/conversations/:conversationId/queue/:messageId",
779
+ v("json", z.object({ text: z.string().trim().min(1).max(20000) }).strict()),
780
+ async (c) => {
781
+ const ctx = await resolveContext(c);
782
+ if (ctx instanceof Response) return ctx;
783
+ const conversation = await loadConversation(c, ctx);
784
+ if (!conversation || !z.uuid().safeParse(c.req.param("messageId")).success) return notFound(c);
785
+ return (await editQueuedMessage(conversation.id, c.req.param("messageId") ?? "", c.req.valid("json").text))
786
+ ? respond(c, ok({ ok: true }))
787
+ : notFound(c);
788
+ },
789
+ )
776
790
  .delete("/conversations/:conversationId/queue/:messageId", async (c) => {
777
791
  const ctx = await resolveContext(c);
778
792
  if (ctx instanceof Response) return ctx;
779
793
  const conversation = await loadConversation(c, ctx);
780
794
  if (!conversation || !z.uuid().safeParse(c.req.param("messageId")).success) return notFound(c);
781
- if (!await updateQueuedMessage(conversation.id, (c.req.param("messageId") ?? ""), "cancel")) return notFound(c);
795
+ if (!(await updateQueuedMessage(conversation.id, c.req.param("messageId") ?? "", "cancel"))) return notFound(c);
782
796
  wakeAiMessageQueue(conversation.id);
783
- return respond(c, ok({ok:true}));
797
+ return respond(c, ok({ ok: true }));
784
798
  })
785
799
  .post("/conversations/:conversationId/queue/:messageId/retry", async (c) => {
786
800
  const ctx = await resolveContext(c);
787
801
  if (ctx instanceof Response) return ctx;
788
802
  const conversation = await loadConversation(c, ctx);
789
803
  if (!conversation || !z.uuid().safeParse(c.req.param("messageId")).success) return notFound(c);
790
- if (!await updateQueuedMessage(conversation.id, (c.req.param("messageId") ?? ""), "retry")) return notFound(c);
804
+ if (!(await updateQueuedMessage(conversation.id, c.req.param("messageId") ?? "", "retry"))) return notFound(c);
791
805
  wakeAiMessageQueue(conversation.id);
792
- return respond(c, ok({ok:true}));
806
+ return respond(c, ok({ ok: true }));
793
807
  })
794
808
  .put("/conversations/:conversationId/draft", v("json", AiSaveConversationDraftInputSchema), async (c) => {
795
809
  const ctx = await resolveContext(c);
@@ -820,7 +834,7 @@ export const aiRoutes = (() => {
820
834
  const conversation = await loadConversation(c, ctx);
821
835
  if (!conversation) return notFound(c);
822
836
  const body = c.req.valid("json");
823
- if (body.queueId && await queuedMessageAccepted(conversation.id, body.queueId)) return respond(c,ok({queued:true}));
837
+ if (body.queueId && (await queuedMessageAccepted(conversation.id, body.queueId))) return respond(c, ok({ queued: true }));
824
838
  if (conversation.draft.revision !== body.draftRevision) {
825
839
  return respond(c, fail(err.conflict("The conversation draft changed in another session.")));
826
840
  }
@@ -882,7 +896,11 @@ export const aiRoutes = (() => {
882
896
  201,
883
897
  );
884
898
  } catch (error) {
885
- if (error instanceof AiMessageQueueConflict || (error instanceof Error && error.message === "Queued messages must be sent first.")) return respond(c,fail(err.conflict(error.message)));
899
+ if (
900
+ error instanceof AiMessageQueueConflict ||
901
+ (error instanceof Error && error.message === "Queued messages must be sent first.")
902
+ )
903
+ return respond(c, fail(err.conflict(error.message)));
886
904
  return toAiErrorResponse(c, error);
887
905
  }
888
906
  })
@@ -912,7 +930,7 @@ export const aiRoutes = (() => {
912
930
  if (ctx instanceof Response) return ctx;
913
931
  const conversation = await loadConversation(c, ctx);
914
932
  if (!conversation) return notFound(c);
915
- const messageId = (c.req.param("messageId") ?? "");
933
+ const messageId = c.req.param("messageId") ?? "";
916
934
 
917
935
  const messages = await aiConversations.listMessages({ conversationId: conversation.id });
918
936
  const target = messages.find((m) => m.shortId === messageId);
@@ -985,7 +1003,7 @@ export const aiRoutes = (() => {
985
1003
  if (ctx instanceof Response) return ctx;
986
1004
  const conversation = await loadConversation(c, ctx);
987
1005
  if (!conversation) return notFound(c);
988
- const messageId = (c.req.param("messageId") ?? "");
1006
+ const messageId = c.req.param("messageId") ?? "";
989
1007
 
990
1008
  const messages = await aiConversations.listMessages({ conversationId: conversation.id });
991
1009
  const target = messages.find((m) => m.shortId === messageId);
@@ -1152,9 +1170,16 @@ export const aiRoutes = (() => {
1152
1170
  if (ctx instanceof Response) return ctx;
1153
1171
  const conversation = await loadConversation(c, ctx);
1154
1172
  if (!conversation) return notFound(c);
1155
- return respond(c, ok(await aiDictations.discardOperation({
1156
- conversationId: conversation.id, userId: ctx.ownerUserId, operationId: c.req.valid("json").operationId,
1157
- })));
1173
+ return respond(
1174
+ c,
1175
+ ok(
1176
+ await aiDictations.discardOperation({
1177
+ conversationId: conversation.id,
1178
+ userId: ctx.ownerUserId,
1179
+ operationId: c.req.valid("json").operationId,
1180
+ }),
1181
+ ),
1182
+ );
1158
1183
  })
1159
1184
  .get("/conversations/:conversationId/dictations", v("query", AiDictationListSchema), async (c) => {
1160
1185
  const ctx = await resolveContext(c);
@@ -1240,9 +1265,9 @@ export const aiRoutes = (() => {
1240
1265
  return respond(c, fail(err.badInput(`File exceeds the ${Math.floor(AI_FILES_MAX_FILE_BYTES_DEFAULT / (1024 * 1024))} MB limit`)));
1241
1266
  }
1242
1267
  const name = (file.name || "upload").replaceAll("/", "_").replaceAll("\\", "_").replaceAll("\0", "").slice(0, 160) || "upload";
1243
- const directory=form?.get("directory") ?? "/";
1244
- if(typeof directory!=="string")return respond(c,fail(err.badInput("Invalid directory")));
1245
- const path = normalizeAiFilePath(`${directory.replace(/\/$/,"")}/${name}`);
1268
+ const directory = form?.get("directory") ?? "/";
1269
+ if (typeof directory !== "string") return respond(c, fail(err.badInput("Invalid directory")));
1270
+ const path = normalizeAiFilePath(`${directory.replace(/\/$/, "")}/${name}`);
1246
1271
  if (!path) return respond(c, fail(err.badInput("Invalid file name")));
1247
1272
  if (aiProjectFilePathFromMount(path) !== null) return respond(c, fail(err.badInput("The /project namespace is reserved.")));
1248
1273
 
@@ -1256,7 +1281,8 @@ export const aiRoutes = (() => {
1256
1281
  return respond(c, ok({ file: stat }));
1257
1282
  } catch (error) {
1258
1283
  logger("ai:files").warn("Conversation upload failed", {
1259
- code: "file_upload_failed", conversationId: conversation.id,
1284
+ code: "file_upload_failed",
1285
+ conversationId: conversation.id,
1260
1286
  errorType: error instanceof Error ? error.name : "unknown",
1261
1287
  });
1262
1288
  return respond(c, fail(err.badInput(error instanceof Error ? error.message : "Upload failed")));
@@ -1,6 +1,8 @@
1
1
  /** Preserve why execution stopped instead of reporting an operator deadline as a user abort. */
2
2
  export class AiRunTimeout extends Error {
3
- constructor(readonly budgetMs: number | null) { super("AI_RUN_TIMEOUT"); }
3
+ constructor(readonly budgetMs: number | null) {
4
+ super("AI_RUN_TIMEOUT");
5
+ }
4
6
  messageFor(locale?: string): string {
5
7
  const minutes = this.budgetMs ? this.budgetMs / 60_000 : null;
6
8
  return locale?.startsWith("de")
@@ -1,3 +1,4 @@
1
+ export { CloudAiTranscribeAudioInputSchema, CloudAiTranscribeAudioOutputSchema, createCloudAiTranscribeAudioTool } from "./audio-tool";
1
2
  export {
2
3
  CLOUD_AI_DEFERRED_BUILTIN_TOOL_NAMES,
3
4
  createCloudAiCardTool,
@@ -8,6 +9,11 @@ export {
8
9
  createConfiguredDefaultCloudAiTools,
9
10
  createDefaultCloudAiTools,
10
11
  } from "./default-tools";
12
+ export {
13
+ CloudAiFetchFileInputSchema,
14
+ CloudAiFetchFileOutputSchema,
15
+ createCloudAiFetchFileTool,
16
+ } from "./fetch-file-tool";
11
17
  export {
12
18
  CloudAiCalculateInputSchema,
13
19
  CloudAiCalculateOutputSchema,
@@ -27,16 +33,9 @@ export {
27
33
  evaluateAiDate,
28
34
  evaluateAiMath,
29
35
  } from "./file-tools";
30
- export {
31
- CloudAiFetchFileInputSchema,
32
- CloudAiFetchFileOutputSchema,
33
- createCloudAiFetchFileTool,
34
- } from "./fetch-file-tool";
35
36
  export {
36
37
  CloudAiMarkdownToPdfInputSchema,
37
38
  CloudAiMarkdownToPdfOutputSchema,
38
39
  createCloudAiMarkdownToPdfTool,
39
40
  } from "./markdown-pdf-tool";
40
41
  export { createCloudAiViewImageTool } from "./vision-tool";
41
-
42
- export { createCloudAiTranscribeAudioTool, CloudAiTranscribeAudioInputSchema, CloudAiTranscribeAudioOutputSchema } from "./audio-tool";
package/src/ai/runtime.ts CHANGED
@@ -1,8 +1,3 @@
1
- import { parseAiResourceMarker } from "./resource-markers";
2
- import { aiQuotas } from "./quotas";
3
- import { drainQueuedMessages } from "./message-queue";
4
- import { coreSettings } from "../services/settings/api";
5
- import { startAiDictationRuntime } from "./dictation-runtime";
6
1
  import { isDeepStrictEqual } from "node:util";
7
2
  import type { InboundEvent, Input, Message } from "@k2b/nessi";
8
3
  import type { QueueMessage } from "@k2b/sync";
@@ -11,12 +6,16 @@ import { lazySync } from "../_internal/process-sync";
11
6
  import type { RequestActor } from "../server";
12
7
  import { logger } from "../services/logging";
13
8
  import { superviseRuntimeTask } from "../services/runtime-lifecycle";
14
-
9
+ import { coreSettings } from "../services/settings/api";
15
10
  import { type AiToolApprovalContext, aiTurnAllowsRememberedApprovals, rememberAiToolApproval } from "./approvals";
16
11
  import { aiChatAccessSubject, selectAssistantAiModelId } from "./assistant-models";
12
+ import { startAiDictationRuntime } from "./dictation-runtime";
17
13
  import { AiTurnExecutor } from "./executor";
18
14
  import { canonicalizeAiConversationAttachments, snapshotAiConversationFiles } from "./file-context";
19
15
  import { startAiInvalidationRuntime, stopAiInvalidationRuntime } from "./live-outbox";
16
+ import { drainQueuedMessages } from "./message-queue";
17
+ import { aiQuotas } from "./quotas";
18
+ import { parseAiResourceMarker } from "./resource-markers";
20
19
  import { isAiVisionModelConfigured } from "./settings";
21
20
  import { aiConversations } from "./store";
22
21
  import { publishAiTurnAbort, publishAiWireEvent } from "./stream";
@@ -144,10 +143,14 @@ export const prepareAiChatTurn = async (input: SubmitAiChatTurnInput) => {
144
143
  if (input.assistantChat && quotaSubject) await aiQuotas.assertAllowed(quotaSubject, resolved.profile.id);
145
144
  const runConfig: AiChatTurnRunConfig = {
146
145
  kind: "chat",
147
- selectedSkillIds: [...new Set((userMessage.role === "user" ? userMessage.content : []).flatMap(part => {
148
- const marker = parseAiResourceMarker(typeof part === "string" ? part : part.type === "text" ? part.text : "");
149
- return marker?.ref.type === "core.ai.skill" ? [marker.ref.id] : [];
150
- }))],
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
+ ],
151
154
  ...(input.assistantChat ? { assistantChat: true } : {}),
152
155
  input: canonicalInput,
153
156
  chatId: input.chatId,
@@ -421,14 +424,21 @@ const processMessage = async (
421
424
  const leaseOwner = `${AI_WORKER_ID}:${message.messageId}:${crypto.randomUUID()}`;
422
425
  let heartbeatFailed = false;
423
426
  const touch = setInterval(() => {
424
- void message.heartbeat().then(() => { heartbeatFailed = false; }).catch(() => {
425
- if (!heartbeatFailed && !signal.aborted) {
426
- log.warn("AI queue heartbeat failed", {
427
- code: "queue_heartbeat_failed", conversationId: message.data.conversationId, turnId: message.data.turnId,
428
- });
429
- }
430
- heartbeatFailed = true;
431
- });
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
+ });
432
442
  }, AI_TURN_HEARTBEAT_MS);
433
443
  if (typeof touch === "object" && "unref" in touch) touch.unref();
434
444
  try {
@@ -457,7 +467,13 @@ const publishSweepFinished = async (turn: AiTurnFinalizedAction & { error?: stri
457
467
  type: "turn_finished",
458
468
  status,
459
469
  error: status === "failed" ? (turn.error ?? "AI turn failed.") : null,
460
- }).catch(() => log.warn("AI completion notification failed", {code:"completion_publish_failed",conversationId:turn.conversationId,turnId:turn.turnId}));
470
+ }).catch(() =>
471
+ log.warn("AI completion notification failed", {
472
+ code: "completion_publish_failed",
473
+ conversationId: turn.conversationId,
474
+ turnId: turn.turnId,
475
+ }),
476
+ );
461
477
  };
462
478
 
463
479
  export const sweepAiRuntime = async (onTurnFinalized?: (event: AiTurnFinalizedEvent) => Promise<void>): Promise<void> => {
@@ -465,7 +481,9 @@ export const sweepAiRuntime = async (onTurnFinalized?: (event: AiTurnFinalizedEv
465
481
  await drainQueuedMessages(enqueueAiTurn);
466
482
  const sweep = await aiConversations.sweepTurns({ maxAttempts: AI_TURN_MAX_ATTEMPTS });
467
483
  await Promise.all([
468
- ...sweep.requeued.map((job) => enqueueAiTurn(job).catch(() => log.warn("AI recovery enqueue failed", {code:"recovery_enqueue_failed",...job}))),
484
+ ...sweep.requeued.map((job) =>
485
+ enqueueAiTurn(job).catch(() => log.warn("AI recovery enqueue failed", { code: "recovery_enqueue_failed", ...job })),
486
+ ),
469
487
  ...sweep.failed.map((turn) => publishSweepFinished(turn, "failed")),
470
488
  ...sweep.aborted.map((turn) => publishSweepFinished(turn, "aborted")),
471
489
  ]);