@k2b/cloud 0.7.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 (182) hide show
  1. package/package.json +5 -4
  2. package/scripts/README.md +10 -0
  3. package/scripts/build.ts +6 -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.ts +1 -1
  103. package/src/api/capabilities.ts +45 -13
  104. package/src/api/index.ts +5 -5
  105. package/src/api/mcp.ts +5 -12
  106. package/src/browser/CloudResourceSearch.tsx +1 -1
  107. package/src/browser/command-bridge.ts +1 -1
  108. package/src/browser/command-shortcuts.ts +1 -1
  109. package/src/browser/commands.ts +8 -7
  110. package/src/browser/testing.ts +2 -1
  111. package/src/capabilities/client.ts +1 -1
  112. package/src/capabilities/command-link.ts +1 -1
  113. package/src/capabilities/server.ts +2 -1
  114. package/src/cli/admin/ai-quotas.ts +5 -5
  115. package/src/cli/admin/app-credentials.ts +7 -9
  116. package/src/cli/admin/index.ts +3 -3
  117. package/src/cli/admin/instance.ts +2 -2
  118. package/src/cli/admin/linux.ts +4 -2
  119. package/src/cli/admin/sync.ts +1 -1
  120. package/src/cli/capabilities.ts +42 -29
  121. package/src/config/define-env.ts +97 -0
  122. package/src/config/env.ts +147 -54
  123. package/src/config/index.ts +3 -1
  124. package/src/contracts/capabilities.ts +8 -1
  125. package/src/contracts/index.ts +5 -7
  126. package/src/contracts/registry.ts +2 -0
  127. package/src/contracts/shared.ts +12 -3
  128. package/src/desktop/index.ts +1 -0
  129. package/src/desktop/solid.tsx +1 -1
  130. package/src/index.ts +8 -2
  131. package/src/server/index.ts +2 -2
  132. package/src/server/ratelimit.ts +1 -1
  133. package/src/server/services/access-revision.ts +3 -3
  134. package/src/server/services/access.ts +3 -1
  135. package/src/services/accounts/notification-sender.ts +12 -2
  136. package/src/services/announcements/index.ts +4 -4
  137. package/src/services/app-approval.ts +11 -10
  138. package/src/services/identity/key-config.ts +6 -4
  139. package/src/services/identity/runtime-config.ts +6 -5
  140. package/src/services/ipa/posix.ts +1 -1
  141. package/src/services/ipa/sync.ts +1 -1
  142. package/src/services/logging/index.ts +1 -1
  143. package/src/services/logging/trace.ts +1 -1
  144. package/src/services/mandates/index.ts +5 -2
  145. package/src/services/mandates/policy.ts +106 -6
  146. package/src/services/notifications/batches.ts +1 -0
  147. package/src/services/pdf/gotenberg.ts +50 -14
  148. package/src/services/pdf/index.ts +11 -11
  149. package/src/services/public-http.ts +1 -1
  150. package/src/services/rail-shortcuts.ts +1 -1
  151. package/src/services/rail-snapshot.ts +1 -1
  152. package/src/services/request-cache-redis.ts +2 -1
  153. package/src/services/session/user.ts +1 -1
  154. package/src/services/settings/app.ts +5 -1
  155. package/src/services/settings/core-settings.ts +6 -8
  156. package/src/services/settings/index.ts +3 -3
  157. package/src/services/settings/store.ts +3 -3
  158. package/src/services/webauthn.ts +1 -1
  159. package/src/shared/index.ts +3 -6
  160. package/src/shared/markdown/extensions/info-blocks.ts +1 -1
  161. package/src/ssr/AdminSidebar.tsx +2 -2
  162. package/src/ssr/AppLaunchpad.island.tsx +12 -2
  163. package/src/ssr/AppLaunchpadPanel.tsx +4 -4
  164. package/src/ssr/GlobalSearchDialog.tsx +6 -6
  165. package/src/ssr/GlobalSearchTrigger.island.tsx +2 -2
  166. package/src/ssr/Layout.tsx +2 -2
  167. package/src/ssr/LayoutHeader.tsx +1 -1
  168. package/src/ssr/LayoutHelp.tsx +4 -6
  169. package/src/ssr/LayoutHelpTrigger.island.tsx +2 -2
  170. package/src/ssr/MinimalLayout.tsx +1 -1
  171. package/src/ssr/MobileNavigation.tsx +2 -2
  172. package/src/ssr/ProfilePreferences.island.tsx +1 -1
  173. package/src/ssr/admin-navigation.ts +6 -2
  174. package/src/ssr/islands/index.ts +2 -2
  175. package/src/ssr/mobile-menu-history.ts +1 -1
  176. package/src/workflows/ai/index.ts +2 -2
  177. package/src/workflows/ai/runtime.ts +16 -91
  178. package/src/workflows/ai/store.ts +1 -1
  179. package/src/workflows/ai/types.ts +6 -82
  180. package/src/workflows/store/index.ts +2 -2
  181. package/src/server/services/freeipa/test-certificates.ts +0 -44
  182. package/src/services/session/test-fixture.ts +0 -18
@@ -1,6 +1,6 @@
1
- import { AiModelPricingSchema } from "../shared/ai-costs";
2
1
  import { z } from "zod";
3
2
  import { coreSettings } from "../services";
3
+ import { AiModelPricingSchema } from "../shared/ai-costs";
4
4
  import { getAiCredential, listAiCredentialProfileIds } from "./credentials";
5
5
  import { AI_FIRECRAWL_API_KEY_SETTING_KEY } from "./firecrawl-tools";
6
6
  import { createAiProvider } from "./provider";
@@ -14,13 +14,12 @@ export const withAiShortId = async <T>(
14
14
  try {
15
15
  return await insert(allocate());
16
16
  } catch (error) {
17
- const uniqueViolation = typeof error === "object" && error !== null && (("code" in error && error.code === "23505") || ("errno" in error && error.errno === "23505"));
18
- const collision =
17
+ const uniqueViolation =
19
18
  typeof error === "object" &&
20
19
  error !== null &&
21
- uniqueViolation &&
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
  }
@@ -11,14 +11,15 @@ export async function withAiSkillSearch<T>(query: string, run: (bm25: boolean) =
11
11
  SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_textsearch')
12
12
  AND EXISTS (SELECT 1 FROM pg_class c JOIN pg_am am ON am.oid = c.relam
13
13
  WHERE c.oid = to_regclass(${BM25_INDEX}) AND am.amname = 'bm25') AS available
14
- `.then(rows => rows[0]?.available ?? false);
14
+ `.then((rows) => rows[0]?.available ?? false);
15
15
  const bm25 = await bm25Available;
16
16
  try {
17
17
  return await run(bm25);
18
18
  } catch (error) {
19
- const capabilityLost = typeof error === "object" && error !== null &&
20
- ["code", "errno"].some(key => key in error &&
21
- ["0A000", "42704", "42883", "55000"].includes(String(Reflect.get(error, key))));
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))));
22
23
  if (!bm25 || !capabilityLost) throw error;
23
24
  bm25Available = Promise.resolve(false);
24
25
  return run(false);
@@ -29,19 +30,29 @@ export function aiSkillSearchSql(query: string, bm25: boolean) {
29
30
  const terms = [...new Set(query.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [])];
30
31
  // Match every term in either field. Short prefixes stay exact; longer terms
31
32
  // tolerate typos without matching unrelated short words throughout the catalog.
32
- const matches = !query ? sql`TRUE` : !terms.length ? sql`FALSE` : sql`
33
+ const matches = !query
34
+ ? sql`TRUE`
35
+ : !terms.length
36
+ ? sql`FALSE`
37
+ : sql`
33
38
  NOT EXISTS (SELECT 1 FROM unnest(${toPgTextArray(terms)}::text[]) AS term
34
39
  WHERE strpos(lower(skill.name), term) = 0 AND strpos(lower(skill.description), term) = 0
35
40
  AND (length(term) < 4 OR greatest(word_similarity(term, skill.name),
36
41
  word_similarity(term, skill.description)) < 0.35))
37
42
  `;
38
- const rank = !query ? sql`0::real` : sql`
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`
39
48
  CASE WHEN lower(skill.name) = ${query.toLowerCase()} THEN 100 ELSE 0 END
40
49
  + CASE WHEN strpos(lower(skill.name), ${query.toLowerCase()}) > 0 THEN 10 ELSE 0 END
41
- + 2 * word_similarity(${query}, skill.name) + word_similarity(${query}, skill.description)
50
+ + 2 * strict_word_similarity(${query}, skill.name) + strict_word_similarity(${query}, skill.description)
42
51
  `;
43
- const textRank = !query ? sql`0::real` : bm25
44
- ? sql`-(skill.search_text <@> to_bm25query(${query}, ${BM25_INDEX}))`
45
- : sql`ts_rank_cd(to_tsvector('simple', skill.search_text), plainto_tsquery('simple', ${query}))`;
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}))`;
46
57
  return { matches, rank, textRank };
47
58
  }
@@ -1,5 +1,5 @@
1
- import { ASSISTANT_DATA_ANALYSIS_SKILL } from "./data-analysis-skill";
2
1
  import { ASSISTANT_CODE_MODE_SKILL } from "./code-mode-skill";
2
+ import { ASSISTANT_DATA_ANALYSIS_SKILL } from "./data-analysis-skill";
3
3
  import { CLOUD_GRIDS_INSTRUCTIONS, CLOUD_GRIDS_QUERY_REFERENCE } from "./grids-skill";
4
4
  import type { AiSkillTemplate } from "./skills";
5
5
 
@@ -296,9 +296,17 @@ refer to an existing schedule, list tasks before creating a duplicate.
296
296
  notebook constraint exists on an action that accepts only a note ID.
297
297
  4. Explain the scope through the normal create/update review. Grants authorize
298
298
  this task's capability use, not new access to resources. Always-approval
299
- capabilities cannot be granted. Interactive browser and Code Mode are not
300
- available in background runs; explain an unsupported requirement rather than
301
- promising it will work unattended.
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.
302
310
  5. Create the task. Confirm its actual saved timing, timezone and destination only
303
311
  after success. Queued or manually started does not mean completed.
304
312
 
@@ -430,7 +438,7 @@ const BUILTIN_CLOUD_AI_SKILLS: AiSkillTemplate[] = [
430
438
  ASSISTANT_CODE_MODE_SKILL,
431
439
  ASSISTANT_DATA_ANALYSIS_SKILL,
432
440
  {
433
- version: 4,
441
+ version: 5,
434
442
  key: "grids:cloud-grids",
435
443
  name: "cloud-grids",
436
444
  description:
@@ -447,10 +455,11 @@ const BUILTIN_CLOUD_AI_SKILLS: AiSkillTemplate[] = [
447
455
  instructions: CLOUD_ASSISTANT_INSTRUCTIONS,
448
456
  },
449
457
  {
450
- version: 1,
458
+ version: 2,
451
459
  key: "assistant:scheduled-tasks",
452
460
  name: "scheduled-tasks",
453
- description: "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.",
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.",
454
463
  instructions: SCHEDULED_TASKS_INSTRUCTIONS,
455
464
  },
456
465
  {
@@ -13,7 +13,12 @@ export async function loadSelectedAiSkills(ids: readonly string[], turnId: strin
13
13
  if (!subject || !allowed) throw new Error("The selected Skill requires a model and tool scope that allow Skills.");
14
14
  const skill = await aiSkills.loadForTurn(turnId, { id }, subject);
15
15
  if (!skill) throw new Error(`Selected Skill ${id} is unavailable or access was revoked.`);
16
- loaded.push({ name: skill.name, revision: skill.revision, instructions: skill.instructions, files: skill.files.map(file => file.path) });
16
+ loaded.push({
17
+ name: skill.name,
18
+ revision: skill.revision,
19
+ instructions: skill.instructions,
20
+ files: skill.files.map((file) => file.path),
21
+ });
17
22
  }
18
23
  return loaded;
19
24
  }
@@ -1,8 +1,8 @@
1
- import { aiProjects } from "./projects";
2
1
  import { Hono, type MiddlewareHandler } from "hono";
3
2
  import { z } from "zod";
4
3
  import { AuthenticatedPrincipalSchema } from "../contracts/shared";
5
4
  import { type AuthContext, auth, err, fail, ok, rateLimit, respond, v } from "../server";
5
+ import { aiProjects } from "./projects";
6
6
  import {
7
7
  AI_SKILL_DESCRIPTION_MAX_CHARS,
8
8
  AI_SKILL_EXTRA_FRONTMATTER_MAX_CHARS,
@@ -64,27 +64,44 @@ const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
64
64
  const skills = q === undefined ? await aiSkills.list(subject) : (await aiSkills.search(subject, q)).skills;
65
65
  return respond(c, ok({ skills: skills.map(publicSummary) }));
66
66
  })
67
- .get("/project-links/:projectId", v("query", z.object({ q: z.string().max(200).default(""), page: z.coerce.number().int().min(1).max(100000).default(1), available: z.enum(["true","false"]).default("false") })), async c => {
68
- const subject = c.get("accessSubject") ?? null, query = c.req.valid("query");
69
- const project = await aiProjects.getByShortId(c.req.param("projectId")!, subject, query.available === "true" ? "admin" : "read");
70
- const result = project ? await aiSkills.projectSkills(project.id, subject, { query:query.q, page:query.page, available:query.available === "true" }) : null;
71
- return result ? respond(c, ok({ ...result, items:result.items.map(publicSummary) })) : respond(c, fail(err.notFound("Project")));
72
- })
73
- .get("/:skillId/projects", async c => {
67
+ .get(
68
+ "/project-links/:projectId",
69
+ v(
70
+ "query",
71
+ z.object({
72
+ q: z.string().max(200).default(""),
73
+ page: z.coerce.number().int().min(1).max(100000).default(1),
74
+ available: z.enum(["true", "false"]).default("false"),
75
+ }),
76
+ ),
77
+ async (c) => {
78
+ const subject = c.get("accessSubject") ?? null,
79
+ query = c.req.valid("query");
80
+ const project = await aiProjects.getByShortId(c.req.param("projectId")!, subject, query.available === "true" ? "admin" : "read");
81
+ const result = project
82
+ ? await aiSkills.projectSkills(project.id, subject, { query: query.q, page: query.page, available: query.available === "true" })
83
+ : null;
84
+ return result ? respond(c, ok({ ...result, items: result.items.map(publicSummary) })) : respond(c, fail(err.notFound("Project")));
85
+ },
86
+ )
87
+ .get("/:skillId/projects", async (c) => {
74
88
  const subject = c.get("accessSubject") ?? null;
75
89
  const skill = await aiSkills.getByShortId(c.req.param("skillId")!, subject, "admin");
76
90
  const projects = skill ? await aiSkills.linkedProjects(skill.id, subject) : null;
77
- return projects ? respond(c, ok({ projects:projects.map(project => ({ id:project.shortId, name:project.name })) })) : respond(c, fail(err.notFound("Skill")));
91
+ return projects
92
+ ? respond(c, ok({ projects: projects.map((project) => ({ id: project.shortId, name: project.name })) }))
93
+ : respond(c, fail(err.notFound("Skill")));
78
94
  })
79
- .put("/:skillId/projects/:projectId", v("json", z.object({ linked:z.boolean() }).strict()), async c => {
95
+ .put("/:skillId/projects/:projectId", v("json", z.object({ linked: z.boolean() }).strict()), async (c) => {
80
96
  const subject = c.get("accessSubject") ?? null;
81
- const [skill,project] = await Promise.all([
82
- aiSkills.getByShortId(c.req.param("skillId")!,subject,"admin"),
83
- aiProjects.getByShortId(c.req.param("projectId")!,subject,"admin"),
97
+ const [skill, project] = await Promise.all([
98
+ aiSkills.getByShortId(c.req.param("skillId")!, subject, "admin"),
99
+ aiProjects.getByShortId(c.req.param("projectId")!, subject, "admin"),
84
100
  ]);
85
101
  const linked = c.req.valid("json").linked;
86
- return skill && project && await aiSkills.linkProject(skill.id,project.id,linked,subject)
87
- ? respond(c,ok({linked})) : respond(c,fail(err.notFound("Skill or Project")));
102
+ return skill && project && (await aiSkills.linkProject(skill.id, project.id, linked, subject))
103
+ ? respond(c, ok({ linked }))
104
+ : respond(c, fail(err.notFound("Skill or Project")));
88
105
  })
89
106
  .get("/templates/:name", dependencies.authenticate ?? auth.requireRole("authenticated"), (c) => {
90
107
  if (!c.get("accessSubject")) return respond(c, fail(err.forbidden("Skill templates require an authenticated access subject.")));
package/src/ai/skills.ts CHANGED
@@ -1,8 +1,6 @@
1
- import { aiProjects } from "./projects";
2
- import { AuthenticatedPrincipalSchema } from "../contracts/shared";
3
1
  import { createHash } from "node:crypto";
4
- import { accessRevision } from "../server/services/access-revision";
5
2
  import { type SQL, type SQLQuery, sql } from "bun";
3
+ import { AuthenticatedPrincipalSchema } from "../contracts/shared";
6
4
  import type { AccessSubject } from "../server";
7
5
  import {
8
6
  buildAccessPrincipalCondition,
@@ -12,9 +10,10 @@ import {
12
10
  type PermissionLevel,
13
11
  type Principal,
14
12
  } from "../server/services/access";
13
+ import { accessRevision } from "../server/services/access-revision";
15
14
  import { toPgUuidArray } from "../services/postgres";
16
- import { aiSkillSearchSql, withAiSkillSearch } from "./skill-search";
17
15
  import { mountAiSkillFilePath } from "./file-mount";
16
+ import { aiProjects } from "./projects";
18
17
  import { AI_SHORT_ID_PATTERN, withAiShortIdForDb } from "./short-id";
19
18
  import {
20
19
  type AiSkillExtraFrontmatter,
@@ -26,6 +25,7 @@ import {
26
25
  validateAiSkillName,
27
26
  validateAiSkillReferences,
28
27
  } from "./skill-format";
28
+ import { aiSkillSearchSql, withAiSkillSearch } from "./skill-search";
29
29
  import { getBuiltinAiSkillTemplates } from "./skill-seeds";
30
30
  import type { AiSkillFileToolContent, AiSkillFileToolStat } from "./types";
31
31
 
@@ -187,15 +187,17 @@ const principalForSubject = (subject: AccessSubject): Principal =>
187
187
  : { type: "service_account", serviceAccountId: subject.serviceAccountId };
188
188
 
189
189
  const accessMatch = (subject: AccessSubject | null): SQLQuery =>
190
- subject === null ? sql`FALSE` : buildAccessPrincipalCondition({
191
- subject,
192
- columns: {
193
- userId: sql`access.user_id`,
194
- groupId: sql`access.group_id`,
195
- serviceAccountId: sql`access.service_account_id`,
196
- authenticatedOnly: sql`access.authenticated_only`,
197
- },
198
- });
190
+ subject === null
191
+ ? sql`FALSE`
192
+ : buildAccessPrincipalCondition({
193
+ subject,
194
+ columns: {
195
+ userId: sql`access.user_id`,
196
+ groupId: sql`access.group_id`,
197
+ serviceAccountId: sql`access.service_account_id`,
198
+ authenticatedOnly: sql`access.authenticated_only`,
199
+ },
200
+ });
199
201
 
200
202
  const getRow = async (skillId: string, db: SQL = sql): Promise<SkillRow | null> =>
201
203
  (await db<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid`)[0] ?? null;
@@ -220,7 +222,9 @@ const effectivePermission = (skillId: SQLQuery, subject: AccessSubject | null) =
220
222
  `;
221
223
 
222
224
  const permissionFor = async (skillId: string, subject: AccessSubject | null, db: SQL = sql): Promise<AiSkillPermission | "none"> => {
223
- const [row] = await db<{ permission: AiSkillPermission | null }[]>`SELECT (${effectivePermission(sql`${skillId}::uuid`, subject)}) AS permission`;
225
+ const [row] = await db<
226
+ { permission: AiSkillPermission | null }[]
227
+ >`SELECT (${effectivePermission(sql`${skillId}::uuid`, subject)}) AS permission`;
224
228
  return row?.permission ?? "none";
225
229
  };
226
230
 
@@ -332,8 +336,7 @@ const createSkillAccess = async (
332
336
  };
333
337
 
334
338
  const checkAccessRevision = async (skillId: string, expected: string | undefined, db: SQL) => {
335
- if (expected !== undefined && accessRevision(await listSkillAccess(skillId, db)) !== expected)
336
- throw new AiSkillRevisionConflictError();
339
+ if (expected !== undefined && accessRevision(await listSkillAccess(skillId, db)) !== expected) throw new AiSkillRevisionConflictError();
337
340
  };
338
341
 
339
342
  const updateSkillAccess = async (skillId: string, accessId: string, permission: AiSkillPermission, db: SQL): Promise<boolean> => {
@@ -493,9 +496,14 @@ const skillSnapshotFiles = (skill: AiSkill): { path: string; content: string }[]
493
496
  ];
494
497
 
495
498
  type SkillListOptions = { query?: string; enabledOnly?: boolean; limit?: number };
496
- async function listSkillSummaries(subject: AccessSubject | null, options: SkillListOptions & {
497
- project?: { id: string; linked: boolean }; manageOnly?: boolean; offset?: number;
498
- } = {}): Promise<AiSkillSummary[]> {
499
+ async function listSkillSummaries(
500
+ subject: AccessSubject | null,
501
+ options: SkillListOptions & {
502
+ project?: { id: string; linked: boolean };
503
+ manageOnly?: boolean;
504
+ offset?: number;
505
+ } = {},
506
+ ): Promise<AiSkillSummary[]> {
499
507
  const userId = subject?.type === "user" ? subject.userId : null;
500
508
  const query = (options.query ?? "").trim().slice(0, 200);
501
509
  const limit = Math.max(1, Math.min(201, options.limit ?? 200));
@@ -535,7 +543,9 @@ async function listSkillSummaries(subject: AccessSubject | null, options: SkillL
535
543
  }
536
544
 
537
545
  async function linkedSkillProjects(skillId: string, subject: AccessSubject | null) {
538
- const links = await sql<{projectId:string}[]>`SELECT project_id AS "projectId" FROM ai.project_skills WHERE skill_id=${skillId}::uuid ORDER BY project_id`;
546
+ const links = await sql<
547
+ { projectId: string }[]
548
+ >`SELECT project_id AS "projectId" FROM ai.project_skills WHERE skill_id=${skillId}::uuid ORDER BY project_id`;
539
549
  const result: Array<{ projectId: string; shortId: string | null; name: string | null }> = [];
540
550
  for (const link of links) {
541
551
  const project = await aiProjects.get(link.projectId, subject);
@@ -630,20 +640,30 @@ export const aiSkills = {
630
640
  return listSkillSummaries(subject, options);
631
641
  },
632
642
 
633
- async projectSkills(projectId: string, subject: AccessSubject | null, options: { query?: string; page?: number; available?: boolean } = {}) {
643
+ async projectSkills(
644
+ projectId: string,
645
+ subject: AccessSubject | null,
646
+ options: { query?: string; page?: number; available?: boolean } = {},
647
+ ) {
634
648
  if (!(await aiProjects.get(projectId, subject, options.available ? "admin" : "read"))) return null;
635
649
  const page = Math.max(1, Math.min(100000, Math.trunc(options.page ?? 1)));
636
- const skills = await listSkillSummaries(subject, { query: options.query, limit: 31, offset: (page-1)*30,
637
- project: { id: projectId, linked: !options.available }, manageOnly: options.available });
638
- return { items: skills.slice(0,30), hasNext: skills.length>30, page };
650
+ const skills = await listSkillSummaries(subject, {
651
+ query: options.query,
652
+ limit: 31,
653
+ offset: (page - 1) * 30,
654
+ project: { id: projectId, linked: !options.available },
655
+ manageOnly: options.available,
656
+ });
657
+ return { items: skills.slice(0, 30), hasNext: skills.length > 30, page };
639
658
  },
640
659
 
641
660
  async linkProject(skillId: string, projectId: string, linked: boolean, subject: AccessSubject | null): Promise<boolean> {
642
661
  if (!(await aiProjects.get(projectId, subject, "admin"))) return false;
643
- return sql.begin(async tx => {
662
+ return sql.begin(async (tx) => {
644
663
  const [row] = await tx`SELECT id FROM ai.skills WHERE id=${skillId}::uuid FOR UPDATE`;
645
664
  if (!row || !hasPermission(await permissionFor(skillId, subject, tx), "admin")) return false;
646
- if (linked) await tx`INSERT INTO ai.project_skills(project_id,skill_id) VALUES(${projectId}::uuid,${skillId}::uuid) ON CONFLICT DO NOTHING`;
665
+ if (linked)
666
+ await tx`INSERT INTO ai.project_skills(project_id,skill_id) VALUES(${projectId}::uuid,${skillId}::uuid) ON CONFLICT DO NOTHING`;
647
667
  else await tx`DELETE FROM ai.project_skills WHERE project_id=${projectId}::uuid AND skill_id=${skillId}::uuid`;
648
668
  return true;
649
669
  });
@@ -722,7 +742,7 @@ export const aiSkills = {
722
742
  },
723
743
 
724
744
  async listAccess(skillId: string, subject: AccessSubject | null): Promise<AiSkillAccess[] | null> {
725
- return sql.begin(async tx => {
745
+ return sql.begin(async (tx) => {
726
746
  const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id=${skillId}::uuid FOR UPDATE`;
727
747
  if (!row || !hasPermission(await permissionFor(row.id, subject, tx), "admin")) return null;
728
748
  return listSkillAccess(skillId, tx);
@@ -743,7 +763,13 @@ export const aiSkills = {
743
763
  });
744
764
  },
745
765
 
746
- async updateAccess(skillId: string, accessId: string, subject: AccessSubject | null, permission: AiSkillPermission, expectedAccessRevision?: string): Promise<boolean> {
766
+ async updateAccess(
767
+ skillId: string,
768
+ accessId: string,
769
+ subject: AccessSubject | null,
770
+ permission: AiSkillPermission,
771
+ expectedAccessRevision?: string,
772
+ ): Promise<boolean> {
747
773
  return sql.begin(async (tx) => {
748
774
  const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid FOR UPDATE`;
749
775
  if (!row || !hasPermission(await permissionFor(row.id, subject, tx), "admin")) return false;
package/src/ai/store.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { parseAiTodoPlan } from "./todo-contracts";
2
1
  import type { DoneReason, InboundEvent, LoopAggregate, Message, SessionStore, StoreEntry } from "@k2b/nessi";
3
2
  import type { Usage } from "@k2b/nessi/ai";
4
- import { sql, type SQL } from "bun";
3
+ import { type SQL, sql } from "bun";
5
4
  import { type CapabilityActionReview, CapabilityActionReviewSchema } from "../contracts/capabilities";
6
5
  import { logger } from "../services/logging";
7
6
  import { toPgTextArray } from "../services/postgres";
8
7
  import type { AiTurnBlock } from "./protocol";
9
8
  import { withAiShortId, withAiShortIdForDb } from "./short-id";
9
+ import { parseAiTodoPlan } from "./todo-contracts";
10
10
  import type {
11
11
  AiConversation,
12
12
  AiConversationDraft,
@@ -527,7 +527,7 @@ const visibleToTurn = (turnId?: string) => sql`(
527
527
  )`;
528
528
 
529
529
  const rowToConversation = (row: ConversationRow): AiConversation => ({
530
- ...(row.activity ? {activity:row.activity} : {}),
530
+ ...(row.activity ? { activity: row.activity } : {}),
531
531
  id: row.id,
532
532
  hasActiveSchedule: Boolean(row.has_active_schedule),
533
533
  shortId: row.short_id,
@@ -539,19 +539,24 @@ const rowToConversation = (row: ConversationRow): AiConversation => ({
539
539
  pinnedAt: row.pinned_at ? iso(row.pinned_at) : null,
540
540
  archivedAt: row.archived_at ? iso(row.archived_at) : null,
541
541
  done: row.done,
542
- isDone: row.pinned_at ? false : row.is_done ?? row.done ?? (
543
- new Date(row.last_used_at).getTime() <= Date.now() - 7 * 86400000
544
- && !row.has_active_schedule
545
- && !["queued", "running", "waiting_for_action"].includes(row.latest_turn_status ?? "")
546
- ),
542
+ isDone: row.pinned_at
543
+ ? false
544
+ : (row.is_done ??
545
+ row.done ??
546
+ (new Date(row.last_used_at).getTime() <= Date.now() - 7 * 86400000 &&
547
+ !row.has_active_schedule &&
548
+ !["queued", "running", "waiting_for_action"].includes(row.latest_turn_status ?? ""))),
547
549
  lastUsedAt: iso(row.last_used_at),
548
550
  runStatus: conversationRunStatus(row.latest_turn_status, row.latest_browser_pending),
549
551
  runError: row.latest_turn_status === "failed" ? row.latest_turn_error?.trim() || "Assistant response failed." : null,
550
552
  unreadCompletion:
551
- Boolean(row.background_received_at && (!row.last_viewed_at || new Date(row.background_received_at).getTime() > new Date(row.last_viewed_at).getTime())) ||
552
- row.latest_turn_status === "completed" &&
553
- Boolean(row.latest_turn_completed_at) &&
554
- (!row.last_viewed_at || new Date(row.latest_turn_completed_at!).getTime() > new Date(row.last_viewed_at).getTime()),
553
+ Boolean(
554
+ row.background_received_at &&
555
+ (!row.last_viewed_at || new Date(row.background_received_at).getTime() > new Date(row.last_viewed_at).getTime()),
556
+ ) ||
557
+ (row.latest_turn_status === "completed" &&
558
+ Boolean(row.latest_turn_completed_at) &&
559
+ (!row.last_viewed_at || new Date(row.latest_turn_completed_at!).getTime() > new Date(row.last_viewed_at).getTime())),
555
560
  projectId: row.project_id,
556
561
  draft: {
557
562
  content: parseJsonValue<AiConversationDraft["content"]>(row.draft_content ?? []),
@@ -859,12 +864,13 @@ const appendTurnOwnedMessage = async (input: {
859
864
  modelProfileId?: string | null;
860
865
  meta?: AiStoredMessage["meta"];
861
866
  }): Promise<AiStoredMessage | null> => {
862
- return sql.begin(async tx => {
863
- if (!input.background) await tx`SELECT id FROM ai.conversations WHERE id = ${input.conversationId}::uuid FOR UPDATE`;
864
- const { usage, providerModel, stopReason } = messageColumns(input.message);
865
- const rows = await withAiShortIdForDb(tx,
866
- "idx_ai_messages_conversation_short_id",
867
- (attempt, shortId) => attempt<MessageRow[]>`
867
+ return sql.begin(async (tx) => {
868
+ if (!input.background) await tx`SELECT id FROM ai.conversations WHERE id = ${input.conversationId}::uuid FOR UPDATE`;
869
+ const { usage, providerModel, stopReason } = messageColumns(input.message);
870
+ const rows = await withAiShortIdForDb(
871
+ tx,
872
+ "idx_ai_messages_conversation_short_id",
873
+ (attempt, shortId) => attempt<MessageRow[]>`
868
874
  INSERT INTO ${sql(input.background ? "ai.task_messages" : "ai.messages")} (
869
875
  short_id,
870
876
  conversation_id,
@@ -883,7 +889,7 @@ const appendTurnOwnedMessage = async (input: {
883
889
  ${shortId},
884
890
  ${input.conversationId},
885
891
  CASE
886
- WHEN NOT ${Boolean(input.background)} AND ${input.kind === 'summary'} AND ${input.seq ?? null}::int IS NOT NULL AND ${input.seq ?? null}::int > 0 THEN ${input.seq ?? null}::int
892
+ WHEN NOT ${Boolean(input.background)} AND ${input.kind === "summary"} AND ${input.seq ?? null}::int IS NOT NULL AND ${input.seq ?? null}::int > 0 THEN ${input.seq ?? null}::int
887
893
  ELSE (SELECT COALESCE(MAX(seq), 0) + 1 FROM ${sql(input.background ? "ai.task_messages" : "ai.messages")} WHERE conversation_id = ${input.conversationId} AND seq > 0 AND (NOT ${Boolean(input.background)} OR loop_id = ${input.turnId}))
888
894
  END,
889
895
  ${input.kind ?? "message"},
@@ -905,12 +911,12 @@ const appendTurnOwnedMessage = async (input: {
905
911
  )
906
912
  RETURNING *
907
913
  `,
908
- );
909
- if (rows[0]) {
910
- if (!input.background) await tx`UPDATE ai.conversations SET updated_at = now() WHERE id = ${input.conversationId}`;
911
- return rowToMessage(rows[0]);
912
- }
913
- return null;
914
+ );
915
+ if (rows[0]) {
916
+ if (!input.background) await tx`UPDATE ai.conversations SET updated_at = now() WHERE id = ${input.conversationId}`;
917
+ return rowToMessage(rows[0]);
918
+ }
919
+ return null;
914
920
  });
915
921
  };
916
922
 
@@ -1776,7 +1782,8 @@ export const aiConversations: AiConversationService = {
1776
1782
  SELECT id FROM ai.turns WHERE conversation_id = ${input.conversationId}::uuid
1777
1783
  AND NOT COALESCE(run_config ? 'background', false) AND status IN ('queued', 'running', 'waiting_for_action') LIMIT 1
1778
1784
  `;
1779
- const [pending] = await tx`SELECT id FROM ai.queued_messages WHERE conversation_id = ${input.conversationId}::uuid AND status IN ('pending','failed') LIMIT 1`;
1785
+ const [pending] =
1786
+ await tx`SELECT id FROM ai.queued_messages WHERE conversation_id = ${input.conversationId}::uuid AND status IN ('pending','failed') LIMIT 1`;
1780
1787
  if (active || pending) return { ok: false as const, reason: "active_turn" as const };
1781
1788
  }
1782
1789
  await tx`UPDATE ai.conversations
@@ -2378,7 +2385,16 @@ export const aiConversations: AiConversationService = {
2378
2385
  await insertMessageLocked(
2379
2386
  {
2380
2387
  conversationId: input.conversationId,
2381
- message: todoPlan && input.summary.role === "assistant" ? { ...input.summary, content: [...input.summary.content, { type: "text", text: `Working plan at this checkpoint:\n${JSON.stringify(todoPlan)}` }] } : input.summary,
2388
+ message:
2389
+ todoPlan && input.summary.role === "assistant"
2390
+ ? {
2391
+ ...input.summary,
2392
+ content: [
2393
+ ...input.summary.content,
2394
+ { type: "text", text: `Working plan at this checkpoint:\n${JSON.stringify(todoPlan)}` },
2395
+ ],
2396
+ }
2397
+ : input.summary,
2382
2398
  kind: "summary",
2383
2399
  seq: checkpointSeq,
2384
2400
  loopId: null,
@@ -2390,20 +2406,25 @@ export const aiConversations: AiConversationService = {
2390
2406
  });
2391
2407
  },
2392
2408
 
2393
- createCompactionTurn: async (input) => sql.begin(async (tx) => {
2394
- const [conversation] = await tx<{ id: string }[]>`
2409
+ createCompactionTurn: async (input) =>
2410
+ sql.begin(async (tx) => {
2411
+ const [conversation] = await tx<{ id: string }[]>`
2395
2412
  SELECT id FROM ai.conversations WHERE id = ${input.conversationId}::uuid AND archived_at IS NULL FOR UPDATE
2396
2413
  `;
2397
- if (!conversation) throw new Error("Conversation not found.");
2398
- const rows = await withAiShortIdForDb(tx, "idx_ai_turns_conversation_short_id", (attempt, shortId) => attempt<TurnRow[]>`
2414
+ if (!conversation) throw new Error("Conversation not found.");
2415
+ const rows = await withAiShortIdForDb(
2416
+ tx,
2417
+ "idx_ai_turns_conversation_short_id",
2418
+ (attempt, shortId) => attempt<TurnRow[]>`
2399
2419
  INSERT INTO ai.turns (short_id, conversation_id, model_profile_id, status, run_config)
2400
2420
  VALUES (${shortId}, ${input.conversationId}, ${input.modelProfileId}, 'queued',
2401
2421
  (${input.runConfig ? JSON.stringify(input.runConfig) : null}::text)::jsonb)
2402
2422
  RETURNING *
2403
- `);
2404
- await tx`UPDATE ai.conversations SET done = CASE WHEN done IS TRUE THEN NULL ELSE done END, last_used_at = now() WHERE id = ${input.conversationId}::uuid`;
2405
- return rowToTurn(rows[0]!);
2406
- }),
2423
+ `,
2424
+ );
2425
+ await tx`UPDATE ai.conversations SET done = CASE WHEN done IS TRUE THEN NULL ELSE done END, last_used_at = now() WHERE id = ${input.conversationId}::uuid`;
2426
+ return rowToTurn(rows[0]!);
2427
+ }),
2407
2428
 
2408
2429
  submitChatTurn: async (input) => {
2409
2430
  return await sql.begin(async (tx) => {
@@ -2412,12 +2433,17 @@ export const aiConversations: AiConversationService = {
2412
2433
  `;
2413
2434
  if (!conversation) throw new Error("Conversation not found.");
2414
2435
  if (input.queuedMessageId) {
2415
- const [head] = await tx<{id:string;status:string}[]>`SELECT id, status FROM ai.queued_messages WHERE conversation_id = ${input.conversationId}::uuid AND status IN ('pending','failed') ORDER BY position LIMIT 1`;
2416
- const [busy] = await tx`SELECT id FROM ai.turns WHERE conversation_id = ${input.conversationId}::uuid AND NOT COALESCE(run_config ? 'background', false) AND status IN ('queued','running','waiting_for_action') LIMIT 1`;
2417
- if (!head || head.id !== input.queuedMessageId || head.status !== 'pending' || busy) throw new Error("Queued message is no longer ready.");
2436
+ const [head] = await tx<
2437
+ { id: string; status: string }[]
2438
+ >`SELECT id, status FROM ai.queued_messages WHERE conversation_id = ${input.conversationId}::uuid AND status IN ('pending','failed') ORDER BY position LIMIT 1`;
2439
+ const [busy] =
2440
+ await tx`SELECT id FROM ai.turns WHERE conversation_id = ${input.conversationId}::uuid AND NOT COALESCE(run_config ? 'background', false) AND status IN ('queued','running','waiting_for_action') LIMIT 1`;
2441
+ if (!head || head.id !== input.queuedMessageId || head.status !== "pending" || busy)
2442
+ throw new Error("Queued message is no longer ready.");
2418
2443
  }
2419
2444
  if (!input.queuedMessageId) {
2420
- const [queued] = await tx`SELECT id FROM ai.queued_messages WHERE conversation_id = ${input.conversationId}::uuid AND status IN ('pending','failed') LIMIT 1`;
2445
+ const [queued] =
2446
+ await tx`SELECT id FROM ai.queued_messages WHERE conversation_id = ${input.conversationId}::uuid AND status IN ('pending','failed') LIMIT 1`;
2421
2447
  if (queued) throw new Error("Queued messages must be sent first.");
2422
2448
  }
2423
2449
  if (input.expectedDraftRevision !== undefined && Number(conversation.draft_revision) !== input.expectedDraftRevision) {
@@ -2456,16 +2482,19 @@ export const aiConversations: AiConversationService = {
2456
2482
  `,
2457
2483
  );
2458
2484
  // Queue acceptance already recorded the user's interaction time.
2459
- if (!input.queuedMessageId) await tx`UPDATE ai.conversations SET done = CASE WHEN done IS TRUE THEN NULL ELSE done END, last_used_at = now() WHERE id = ${input.conversationId}::uuid`;
2485
+ if (!input.queuedMessageId)
2486
+ await tx`UPDATE ai.conversations SET done = CASE WHEN done IS TRUE THEN NULL ELSE done END, last_used_at = now() WHERE id = ${input.conversationId}::uuid`;
2460
2487
  const turn = rowToTurn(turnRows[0]!);
2461
2488
  const attachedFiles = input.runConfig.files?.attached ?? [];
2462
2489
  for (const file of attachedFiles) {
2463
2490
  const copied = input.queuedMessageId
2464
- ? await tx<{path:string}[]>`INSERT INTO ai.turn_files(turn_id,path,bytes,media_type,size,origin,dictation_recorded_at,updated_at,version)
2491
+ ? await tx<
2492
+ { path: string }[]
2493
+ >`INSERT INTO ai.turn_files(turn_id,path,bytes,media_type,size,origin,dictation_recorded_at,updated_at,version)
2465
2494
  SELECT ${turn.id}::uuid,path,bytes,media_type,size,origin,dictation_recorded_at,updated_at,version FROM ai.queued_message_files
2466
2495
  WHERE message_id = ${input.queuedMessageId}::uuid AND path = ${file.path} RETURNING path`
2467
2496
  : input.retrySourceTurnId
2468
- ? await tx<{ path: string }[]>`
2497
+ ? await tx<{ path: string }[]>`
2469
2498
  INSERT INTO ai.turn_files (turn_id, path, bytes, media_type, size, origin, dictation_recorded_at, updated_at, version)
2470
2499
  SELECT ${turn.id}::uuid, path, bytes, media_type, size, origin, dictation_recorded_at, updated_at, version
2471
2500
  FROM ai.turn_files
@@ -2476,7 +2505,7 @@ export const aiConversations: AiConversationService = {
2476
2505
  AND version = ${file.version ?? -1}
2477
2506
  RETURNING path
2478
2507
  `
2479
- : await tx<{ path: string }[]>`
2508
+ : await tx<{ path: string }[]>`
2480
2509
  INSERT INTO ai.turn_files (turn_id, path, bytes, media_type, size, origin, dictation_recorded_at, updated_at, version)
2481
2510
  SELECT ${turn.id}::uuid, path, bytes, media_type, size, origin, dictation_recorded_at, updated_at, version
2482
2511
  FROM ai.files
@@ -2548,7 +2577,9 @@ export const aiConversations: AiConversationService = {
2548
2577
  },
2549
2578
 
2550
2579
  getLatestTurn: async (input) => {
2551
- const [row] = await sql<TurnRow[]>`SELECT * FROM ai.turns WHERE conversation_id = ${input.conversationId}::uuid AND NOT COALESCE(run_config ? 'background', false)
2580
+ const [row] = await sql<
2581
+ TurnRow[]
2582
+ >`SELECT * FROM ai.turns WHERE conversation_id = ${input.conversationId}::uuid AND NOT COALESCE(run_config ? 'background', false)
2552
2583
  ORDER BY created_at DESC, id DESC LIMIT 1`;
2553
2584
  return row ? rowToTurn(row) : null;
2554
2585
  },
@@ -3223,17 +3254,19 @@ export const aiConversations: AiConversationService = {
3223
3254
  if (input.background && input.turnId) {
3224
3255
  const own = await aiConversations.listTurnMessages({ conversationId: input.conversationId, loopId: input.turnId });
3225
3256
  const context = input.background.context;
3226
- return [...context, ...own.map(row => ({ seq: context.length + row.seq, kind: row.kind, message: row.message }))];
3257
+ return [...context, ...own.map((row) => ({ seq: context.length + row.seq, kind: row.kind, message: row.message }))];
3227
3258
  }
3228
3259
  // Results delivered after this turn started belong to the next turn's context.
3229
3260
  // Compare durable message sequences rather than wall-clock timestamps.
3230
3261
  const rows = input.turnId
3231
- ? (await sql<MessageRow[]>`
3262
+ ? (
3263
+ await sql<MessageRow[]>`
3232
3264
  SELECT * FROM ai.messages
3233
3265
  WHERE conversation_id = ${input.conversationId}::uuid AND compacted_at IS NULL
3234
3266
  AND ${visibleToTurn(input.turnId)}
3235
3267
  ORDER BY seq ASC
3236
- `).map(rowToMessage)
3268
+ `
3269
+ ).map(rowToMessage)
3237
3270
  : await aiConversations.listContextMessages({ conversationId: input.conversationId });
3238
3271
  return rows.map((row) => ({
3239
3272
  seq: row.seq,