@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.
- package/package.json +5 -4
- package/scripts/README.md +10 -0
- package/scripts/build.ts +6 -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.ts +1 -1
- 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/index.ts +2 -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/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 +2 -2
- 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/MinimalLayout.tsx +1 -1
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/ProfilePreferences.island.tsx +1 -1
- package/src/ssr/admin-navigation.ts +6 -2
- package/src/ssr/islands/index.ts +2 -2
- package/src/ssr/mobile-menu-history.ts +1 -1
- 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/model-pricing.ts
CHANGED
|
@@ -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 {
|
|
7
|
-
import {
|
|
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
|
-
|
|
2
|
+
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import {
|
|
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
|
-
|
|
153
|
-
)
|
|
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
|
});
|
package/src/ai/projects.ts
CHANGED
|
@@ -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
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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)
|
|
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<
|
|
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
|
}
|
package/src/ai/quota-provider.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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. */
|
package/src/ai/quota-report.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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(
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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,
|
|
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,
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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(
|
|
1156
|
-
|
|
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",
|
|
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")));
|
package/src/ai/run-timeout.ts
CHANGED
|
@@ -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) {
|
|
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")
|
package/src/ai/runtime-tools.ts
CHANGED
|
@@ -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: [
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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(() =>
|
|
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) =>
|
|
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
|
]);
|