@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.
- package/package.json +5 -4
- package/scripts/README.md +10 -0
- package/scripts/build.ts +14 -1
- package/scripts/preload.ts +7 -0
- package/scripts/sync-recovery-smoke.ts +21 -6
- package/src/_internal/build-metadata.ts +8 -1
- package/src/_internal/capabilities.ts +35 -12
- package/src/_internal/define-app.ts +28 -11
- package/src/_internal/page-responses.ts +1 -1
- package/src/_internal/readiness.ts +4 -1
- package/src/_internal/runtime-context.ts +1 -1
- package/src/access/PermissionEditor.tsx +15 -9
- package/src/access/PrincipalPicker.tsx +9 -3
- package/src/access/ResourceApiKeys.tsx +1 -1
- package/src/access/ui.ts +1 -2
- package/src/ai/admin.ts +2 -2
- package/src/ai/assistant-models.ts +1 -1
- package/src/ai/audio-tool.ts +1 -1
- package/src/ai/browser-code-contracts.ts +174 -52
- package/src/ai/browser.ts +4 -2
- package/src/ai/capabilities.ts +3 -3
- package/src/ai/chat/blocks.tsx +127 -50
- package/src/ai/chat/builtin-tools.tsx +2 -4
- package/src/ai/chat/capability-table.tsx +1 -1
- package/src/ai/chat/composer-adapter.ts +48 -13
- package/src/ai/chat/message-utils.ts +18 -5
- package/src/ai/chat/presentation.tsx +24 -11
- package/src/ai/chat/tool-disclosure.tsx +6 -2
- package/src/ai/chat/tool-groups.ts +8 -2
- package/src/ai/chat-quotas.ts +2 -2
- package/src/ai/chat-task-contracts.ts +9 -13
- package/src/ai/chat-tasks.ts +39 -18
- package/src/ai/client/controller.ts +87 -38
- package/src/ai/client/projection.ts +13 -6
- package/src/ai/client/transport.ts +4 -2
- package/src/ai/code-capability-routes.ts +22 -27
- package/src/ai/code-capability-transport.ts +2 -1
- package/src/ai/code-execution.ts +53 -0
- package/src/ai/code-mode-skill.ts +9 -5
- package/src/ai/code-runtime-tools.ts +16 -3
- package/src/ai/code-source-contracts.ts +100 -32
- package/src/ai/code-source-tools.ts +49 -6
- package/src/ai/compaction.ts +3 -3
- package/src/ai/default-tools.ts +87 -14
- package/src/ai/dictation-runtime.ts +1 -1
- package/src/ai/draft-content.ts +27 -12
- package/src/ai/enrich.ts +1 -1
- package/src/ai/executor.ts +8 -12
- package/src/ai/fetch-file-tool.ts +2 -2
- package/src/ai/file-content-version.ts +6 -1
- package/src/ai/file-tools.ts +8 -2
- package/src/ai/files-store.ts +55 -17
- package/src/ai/grids-skill.ts +2 -0
- package/src/ai/http.ts +18 -7
- package/src/ai/index.ts +24 -25
- package/src/ai/inference-calls.ts +3 -4
- package/src/ai/live.ts +1 -1
- package/src/ai/markdown-pdf-tool.ts +2 -2
- package/src/ai/memories.ts +30 -27
- package/src/ai/memory-learning-runs.ts +1 -1
- package/src/ai/memory-learning.ts +5 -5
- package/src/ai/memory-tool.ts +1 -5
- package/src/ai/memory-workflow-evidence.ts +9 -4
- package/src/ai/message-queue.ts +23 -12
- package/src/ai/migrate.ts +6 -4
- package/src/ai/model-pricing.ts +4 -4
- package/src/ai/pdf-render-worker.ts +3 -2
- package/src/ai/projects-routes.ts +15 -40
- package/src/ai/projects.ts +18 -13
- package/src/ai/quota-provider.ts +6 -5
- package/src/ai/quota-report.ts +5 -5
- package/src/ai/quotas.ts +3 -3
- package/src/ai/routes.ts +60 -34
- package/src/ai/run-timeout.ts +3 -1
- package/src/ai/runtime-tools.ts +6 -7
- package/src/ai/runtime.ts +38 -20
- package/src/ai/settings.ts +1 -1
- package/src/ai/short-id.ts +4 -5
- package/src/ai/skill-search.ts +21 -10
- package/src/ai/skill-seeds.ts +16 -7
- package/src/ai/skill-tool.ts +6 -1
- package/src/ai/skills-routes.ts +32 -15
- package/src/ai/skills.ts +54 -28
- package/src/ai/store.ts +80 -47
- package/src/ai/structured.ts +6 -2
- package/src/ai/system-prompt.ts +7 -5
- package/src/ai/task-contracts.ts +84 -0
- package/src/ai/task-execution.ts +92 -0
- package/src/ai/todo-contracts.ts +24 -12
- package/src/ai/todo-tool.ts +16 -13
- package/src/ai/tools.ts +1 -1
- package/src/ai/transcription.ts +4 -4
- package/src/ai/turn-timing.ts +1 -1
- package/src/ai/ui.tsx +2 -1
- package/src/ai/usage.ts +1 -1
- package/src/ai/vision-tool.ts +38 -17
- package/src/api/admin-ai-quotas.ts +14 -8
- package/src/api/admin-ai-skills.ts +3 -3
- package/src/api/admin-linux-identities.ts +10 -4
- package/src/api/admin-rail.ts +2 -2
- package/src/api/app-approval.ts +2 -2
- package/src/api/auth/schemas.ts +7 -1
- package/src/api/auth.ts +4 -3
- package/src/api/capabilities.ts +45 -13
- package/src/api/index.ts +5 -5
- package/src/api/mcp.ts +5 -12
- package/src/browser/CloudResourceSearch.tsx +1 -1
- package/src/browser/command-bridge.ts +1 -1
- package/src/browser/command-shortcuts.ts +1 -1
- package/src/browser/commands.ts +8 -7
- package/src/browser/testing.ts +2 -1
- package/src/capabilities/client.ts +1 -1
- package/src/capabilities/command-link.ts +1 -1
- package/src/capabilities/server.ts +2 -1
- package/src/cli/admin/ai-quotas.ts +5 -5
- package/src/cli/admin/app-credentials.ts +7 -9
- package/src/cli/admin/index.ts +3 -3
- package/src/cli/admin/instance.ts +2 -2
- package/src/cli/admin/linux.ts +4 -2
- package/src/cli/admin/sync.ts +1 -1
- package/src/cli/capabilities.ts +42 -29
- package/src/config/define-env.ts +97 -0
- package/src/config/env.ts +147 -54
- package/src/config/index.ts +3 -1
- package/src/contracts/capabilities.ts +8 -1
- package/src/contracts/index.ts +5 -7
- package/src/contracts/registry.ts +2 -0
- package/src/contracts/shared.ts +12 -3
- package/src/desktop/index.ts +1 -0
- package/src/desktop/solid.tsx +1 -1
- package/src/index.ts +8 -2
- package/src/server/app-assets.ts +27 -0
- package/src/server/index.ts +3 -2
- package/src/server/ratelimit.ts +1 -1
- package/src/server/services/access-revision.ts +3 -3
- package/src/server/services/access.ts +3 -1
- package/src/services/accounts/notification-sender.ts +12 -2
- package/src/services/announcements/index.ts +4 -4
- package/src/services/app-approval.ts +11 -10
- package/src/services/auth-flows/magic-link.ts +13 -1
- package/src/services/identity/key-config.ts +6 -4
- package/src/services/identity/runtime-config.ts +6 -5
- package/src/services/ipa/posix.ts +1 -1
- package/src/services/ipa/sync.ts +1 -1
- package/src/services/logging/index.ts +1 -1
- package/src/services/logging/trace.ts +1 -1
- package/src/services/mandates/index.ts +5 -2
- package/src/services/mandates/policy.ts +106 -6
- package/src/services/notifications/batches.ts +1 -0
- package/src/services/pdf/gotenberg.ts +50 -14
- package/src/services/pdf/index.ts +11 -11
- package/src/services/public-http.ts +1 -1
- package/src/services/rail-shortcuts.ts +1 -1
- package/src/services/rail-snapshot.ts +1 -1
- package/src/services/request-cache-redis.ts +2 -1
- package/src/services/session/user.ts +1 -1
- package/src/services/settings/app.ts +5 -1
- package/src/services/settings/core-settings.ts +6 -8
- package/src/services/settings/index.ts +3 -3
- package/src/services/settings/store.ts +3 -3
- package/src/services/webauthn.ts +1 -1
- package/src/shared/index.ts +3 -6
- package/src/shared/markdown/extensions/info-blocks.ts +1 -1
- package/src/ssr/AdminSidebar.tsx +2 -2
- package/src/ssr/AppLaunchpad.island.tsx +12 -2
- package/src/ssr/AppLaunchpadPanel.tsx +4 -4
- package/src/ssr/GlobalSearchDialog.tsx +6 -6
- package/src/ssr/GlobalSearchTrigger.island.tsx +2 -2
- package/src/ssr/Layout.tsx +4 -3
- package/src/ssr/LayoutHeader.tsx +1 -1
- package/src/ssr/LayoutHelp.tsx +4 -6
- package/src/ssr/LayoutHelpTrigger.island.tsx +2 -2
- package/src/ssr/LayoutRail.tsx +3 -2
- package/src/ssr/MinimalLayout.tsx +1 -1
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/ProfilePreferences.island.tsx +1 -1
- package/src/ssr/RailApps.island.tsx +7 -7
- package/src/ssr/admin-navigation.ts +6 -2
- package/src/ssr/index.ts +1 -0
- package/src/ssr/islands/index.ts +2 -2
- package/src/ssr/mobile-menu-history.ts +1 -1
- package/src/ssr/rail-navigation.ts +3 -2
- package/src/ssr/request-path.ts +12 -0
- package/src/workflows/ai/index.ts +2 -2
- package/src/workflows/ai/runtime.ts +16 -91
- package/src/workflows/ai/store.ts +1 -1
- package/src/workflows/ai/types.ts +6 -82
- package/src/workflows/store/index.ts +2 -2
- package/src/server/services/freeipa/test-certificates.ts +0 -44
- package/src/services/session/test-fixture.ts +0 -18
package/src/ai/settings.ts
CHANGED
|
@@ -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";
|
package/src/ai/short-id.ts
CHANGED
|
@@ -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 =
|
|
18
|
-
const collision =
|
|
17
|
+
const uniqueViolation =
|
|
19
18
|
typeof error === "object" &&
|
|
20
19
|
error !== null &&
|
|
21
|
-
|
|
22
|
-
|
|
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
|
}
|
package/src/ai/skill-search.ts
CHANGED
|
@@ -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 =
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
-
|
|
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 *
|
|
50
|
+
+ 2 * strict_word_similarity(${query}, skill.name) + strict_word_similarity(${query}, skill.description)
|
|
42
51
|
`;
|
|
43
|
-
const textRank = !query
|
|
44
|
-
? sql
|
|
45
|
-
:
|
|
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
|
}
|
package/src/ai/skill-seeds.ts
CHANGED
|
@@ -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.
|
|
300
|
-
|
|
301
|
-
|
|
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:
|
|
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:
|
|
458
|
+
version: 2,
|
|
451
459
|
key: "assistant:scheduled-tasks",
|
|
452
460
|
name: "scheduled-tasks",
|
|
453
|
-
description:
|
|
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
|
{
|
package/src/ai/skill-tool.ts
CHANGED
|
@@ -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({
|
|
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
|
}
|
package/src/ai/skills-routes.ts
CHANGED
|
@@ -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(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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}))
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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<
|
|
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(
|
|
497
|
-
|
|
498
|
-
|
|
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<
|
|
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(
|
|
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, {
|
|
637
|
-
|
|
638
|
-
|
|
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)
|
|
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(
|
|
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 {
|
|
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
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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(
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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 ===
|
|
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
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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] =
|
|
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:
|
|
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) =>
|
|
2394
|
-
|
|
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
|
-
|
|
2398
|
-
|
|
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
|
-
|
|
2405
|
-
|
|
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<
|
|
2416
|
-
|
|
2417
|
-
|
|
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] =
|
|
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)
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
-
? (
|
|
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
|
-
`
|
|
3268
|
+
`
|
|
3269
|
+
).map(rowToMessage)
|
|
3237
3270
|
: await aiConversations.listContextMessages({ conversationId: input.conversationId });
|
|
3238
3271
|
return rows.map((row) => ({
|
|
3239
3272
|
seq: row.seq,
|