@k2b/cloud 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +22 -11
- package/scripts/README.md +10 -0
- package/scripts/browser-performance.ts +14 -0
- package/scripts/build-pdf-renderer.ts +39 -0
- package/scripts/build.ts +20 -35
- package/scripts/preload.ts +16 -12
- package/scripts/sync-recovery-smoke.ts +22 -6
- package/src/_internal/app-presentation.ts +14 -2
- package/src/_internal/build-metadata.ts +8 -1
- package/src/_internal/capabilities.ts +131 -7
- package/src/_internal/capability-streams.ts +81 -0
- package/src/_internal/define-app.ts +80 -37
- package/src/_internal/heartbeat.ts +4 -0
- package/src/_internal/help-catalog.ts +27 -172
- package/src/_internal/help.ts +13 -76
- package/src/_internal/page-responses.ts +2 -0
- package/src/_internal/postgres-application-name.ts +30 -0
- package/src/_internal/process-identity.ts +15 -0
- package/src/_internal/process-sync.ts +1 -1
- package/src/_internal/readiness.ts +4 -1
- package/src/_internal/registry-validation.ts +29 -25
- package/src/_internal/registry.ts +2 -53
- package/src/_internal/runtime-context.ts +5 -16
- package/src/_internal/server-timing.ts +13 -0
- package/src/_internal/status-preserving-ssr.ts +18 -6
- package/src/_internal/web-vitals-asset.ts +10 -0
- package/src/access/PermissionEditor.tsx +18 -151
- package/src/access/PrincipalPicker.tsx +110 -0
- package/src/access/ResourceApiKeys.tsx +1 -1
- package/src/access/ui.ts +1 -0
- package/src/ai/admin.ts +14 -13
- package/src/ai/assistant-models.ts +12 -1
- package/src/ai/audio-format.ts +34 -0
- package/src/ai/audio-tool.ts +90 -0
- package/src/ai/browser-code-contracts.ts +201 -0
- package/src/ai/browser.ts +4 -0
- package/src/ai/capabilities.ts +55 -36
- package/src/ai/capability-execution.ts +47 -12
- package/src/ai/chat/blocks.tsx +229 -90
- package/src/ai/chat/builtin-tools.tsx +2 -4
- package/src/ai/chat/capability-result.ts +15 -0
- package/src/ai/chat/capability-table.tsx +2 -10
- package/src/ai/chat/composer-adapter.ts +83 -3
- package/src/ai/chat/message-actions.tsx +8 -7
- package/src/ai/chat/message-utils.ts +25 -1
- package/src/ai/chat/messages.ts +2 -0
- package/src/ai/chat/presentation.tsx +31 -47
- package/src/ai/chat/primitives.tsx +43 -1
- package/src/ai/chat/tool-disclosure.tsx +21 -2
- package/src/ai/chat/tool-groups.ts +58 -0
- package/src/ai/chat-quotas.ts +25 -0
- package/src/ai/chat-task-contracts.ts +14 -10
- package/src/ai/chat-tasks.ts +297 -43
- package/src/ai/client/controller.ts +165 -39
- package/src/ai/client/file-source.ts +12 -6
- package/src/ai/client/live-connection.ts +1 -0
- package/src/ai/client/projection.ts +29 -11
- package/src/ai/client/transport.ts +6 -0
- package/src/ai/code-capability-routes.ts +105 -0
- package/src/ai/code-capability-transport.ts +39 -0
- package/src/ai/code-execution.ts +53 -0
- package/src/ai/code-mode-skill.ts +118 -0
- package/src/ai/code-runtime-tools.ts +179 -0
- package/src/ai/code-source-contracts.ts +270 -0
- package/src/ai/code-source-tools.ts +188 -0
- package/src/ai/compaction.ts +27 -6
- package/src/ai/data-analysis-skill.ts +10 -0
- package/src/ai/default-tools.ts +99 -3
- package/src/ai/dictation-runtime.ts +171 -0
- package/src/ai/dictations.ts +188 -0
- package/src/ai/draft-content.ts +49 -0
- package/src/ai/enrich.ts +6 -0
- package/src/ai/executor.ts +189 -33
- package/src/ai/fetch-file-tool.ts +2 -2
- package/src/ai/file-content-version.ts +27 -0
- package/src/ai/file-context.ts +2 -1
- package/src/ai/file-media-type.ts +12 -0
- package/src/ai/file-reference-contracts.ts +15 -0
- package/src/ai/file-tools.ts +8 -1
- package/src/ai/files-store.ts +212 -71
- package/src/ai/fixtures/vision-encrypted.pdf +0 -0
- package/src/ai/grids-skill.ts +42 -10
- package/src/ai/http.ts +26 -8
- package/src/ai/index.ts +26 -3
- package/src/ai/inference-calls.ts +197 -0
- package/src/ai/live-events.ts +5 -1
- 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 +64 -32
- package/src/ai/memory-tool.ts +1 -5
- package/src/ai/memory-workflow-evidence.ts +9 -4
- package/src/ai/message-queue.ts +178 -0
- package/src/ai/migrate.ts +189 -11
- package/src/ai/model-pricing.ts +43 -0
- package/src/ai/pdf-render-worker.ts +75 -0
- package/src/ai/pdf-render.fixture.ts +28 -0
- package/src/ai/pdf-render.ts +77 -0
- package/src/ai/projects-routes.ts +18 -43
- package/src/ai/projects.ts +33 -14
- package/src/ai/protocol.ts +3 -0
- package/src/ai/provider.ts +1 -8
- package/src/ai/quota-provider.ts +192 -0
- package/src/ai/quota-report.ts +152 -0
- package/src/ai/quotas-migrate.ts +45 -0
- package/src/ai/quotas.ts +168 -0
- package/src/ai/routes.ts +241 -11
- package/src/ai/run-timeout.ts +12 -0
- package/src/ai/runtime-tools.ts +7 -5
- package/src/ai/runtime.ts +74 -17
- package/src/ai/settings.ts +36 -8
- package/src/ai/short-id.ts +4 -5
- package/src/ai/skill-catalog.ts +0 -10
- package/src/ai/skill-search.ts +58 -0
- package/src/ai/skill-seeds.ts +114 -20
- package/src/ai/skill-tool.ts +21 -5
- package/src/ai/skills-routes.ts +49 -4
- package/src/ai/skills.ts +358 -163
- package/src/ai/store.ts +416 -201
- package/src/ai/stream.ts +5 -0
- package/src/ai/structured-runs.ts +4 -3
- package/src/ai/structured.ts +12 -1
- package/src/ai/system-prompt.ts +14 -2
- package/src/ai/task-contracts.ts +84 -0
- package/src/ai/task-execution.ts +92 -0
- package/src/ai/timeline.ts +2 -5
- package/src/ai/todo-contracts.ts +32 -0
- package/src/ai/todo-tool.ts +19 -0
- package/src/ai/tool-audit.ts +30 -40
- package/src/ai/tools.ts +6 -0
- package/src/ai/transcription.ts +202 -0
- package/src/ai/turn-timing.ts +114 -0
- package/src/ai/types.ts +58 -10
- package/src/ai/ui.tsx +3 -0
- package/src/ai/usage.ts +118 -118
- package/src/ai/vision-tool.ts +60 -10
- package/src/api/admin-ai-projects.ts +2 -2
- package/src/api/admin-ai-quotas.ts +80 -0
- package/src/api/admin-ai-skills.ts +48 -5
- package/src/api/admin-ai-usage.ts +2 -2
- package/src/api/admin-core-settings.ts +25 -0
- package/src/api/admin-identity.ts +91 -51
- package/src/api/admin-linux-identities.ts +10 -4
- package/src/api/admin-rail.ts +58 -0
- package/src/api/announcements.ts +14 -0
- package/src/api/app-approval.ts +2 -2
- package/src/api/auth.ts +1 -1
- package/src/api/capabilities.ts +324 -43
- package/src/api/capability-streams.ts +262 -0
- package/src/api/help.ts +8 -44
- package/src/api/index.ts +8 -2
- package/src/api/mcp.ts +45 -34
- package/src/api/me-web-vitals.ts +35 -0
- package/src/api/me.ts +2 -0
- package/src/api/search/schemas.ts +26 -15
- package/src/api/search.bench.ts +1 -1
- package/src/api/search.ts +54 -17
- package/src/browser/CloudResourceSearch.tsx +640 -702
- package/src/browser/app-approval-vault.ts +19 -7
- package/src/browser/command-bridge.ts +109 -0
- package/src/browser/command-shortcuts.ts +76 -0
- package/src/browser/commands.ts +67 -0
- package/src/browser/navigation-search.ts +48 -0
- package/src/browser/notifications.ts +1 -1
- package/src/browser/resource-picker.tsx +5 -18
- package/src/browser/resource-search-dialog.ts +9 -0
- package/src/browser/resource-search-input.ts +46 -0
- package/src/browser/resource-search-messages.ts +76 -40
- package/src/browser/resource-search.ts +7 -0
- package/src/browser/search-bridge.ts +102 -0
- package/src/browser/search-commands.ts +59 -0
- package/src/browser/search.ts +13 -0
- package/src/browser/spotlight-position.ts +208 -0
- package/src/browser/testing.ts +6 -0
- package/src/browser/web-vitals.ts +25 -0
- package/src/capabilities/claims.ts +179 -0
- package/src/capabilities/client.ts +34 -0
- package/src/capabilities/command-link.ts +16 -0
- package/src/capabilities/executions.ts +298 -0
- package/src/capabilities/migrate.ts +81 -0
- package/src/capabilities/server.ts +116 -35
- package/src/capabilities/stream-body.ts +16 -0
- package/src/capabilities/streams.ts +28 -0
- package/src/cli/admin/ai-quotas.ts +161 -0
- package/src/cli/admin/ai-skills.ts +64 -0
- package/src/cli/admin/ai-usage.ts +53 -30
- package/src/cli/admin/app-credentials.ts +77 -0
- package/src/cli/admin/data.ts +17 -0
- package/src/cli/admin/index.ts +10 -1
- 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 -0
- package/src/cli/capability-streams.ts +25 -0
- package/src/cli/index.ts +3 -2
- package/src/config/define-env.ts +97 -0
- package/src/config/env.ts +147 -51
- package/src/config/index.ts +3 -1
- package/src/contracts/app.ts +23 -3
- package/src/contracts/capabilities.ts +75 -5
- package/src/contracts/capability-streams.ts +46 -0
- package/src/contracts/commands.ts +49 -0
- package/src/contracts/index.ts +5 -2
- package/src/contracts/rail-admin.ts +38 -0
- package/src/contracts/rail-preferences.ts +16 -0
- package/src/contracts/registry.ts +5 -27
- package/src/contracts/settings-types.ts +1 -1
- package/src/contracts/shared.ts +18 -5
- package/src/contracts/web-vitals.ts +27 -0
- package/src/desktop/index.ts +1 -0
- package/src/desktop/solid.tsx +1 -1
- package/src/index.ts +8 -6
- package/src/server/index.ts +3 -0
- package/src/server/middleware/auth.ts +3 -2
- package/src/server/middleware/route-template.ts +9 -3
- package/src/server/middleware/runtime.ts +2 -1
- package/src/server/middleware/settings.ts +17 -11
- package/src/server/ratelimit.ts +1 -1
- package/src/server/services/access-revision.ts +10 -0
- package/src/server/services/access.ts +3 -1
- package/src/server/services/index.ts +1 -0
- package/src/services/account-category-policy.ts +9 -0
- package/src/services/account-lifecycle/audit.ts +1 -1
- package/src/services/accounts/app.ts +12 -2
- package/src/services/accounts/email-write.ts +23 -0
- package/src/services/accounts/groups.ts +15 -4
- package/src/services/accounts/identities.ts +249 -0
- package/src/services/accounts/identity-reconciliation.ts +87 -0
- package/src/services/accounts/ipa-data.ts +2 -2
- package/src/services/accounts/local-groups.ts +2 -2
- package/src/services/accounts/notification-sender.ts +12 -2
- package/src/services/accounts/posix.ts +45 -29
- package/src/services/accounts/users.ts +2 -2
- package/src/services/announcements/index.ts +69 -8
- package/src/services/app-approval.ts +11 -10
- package/src/services/audit/index.ts +1 -1
- package/src/services/cache-fill.ts +35 -0
- package/src/services/help/index.ts +109 -0
- package/src/services/help/maintenance.ts +25 -0
- package/src/services/help/store.ts +180 -0
- package/src/services/help/types.ts +46 -0
- package/src/services/identity/invocation-actor.ts +2 -2
- package/src/services/identity/key-config.ts +8 -5
- package/src/services/identity/key-ring.ts +1 -1
- package/src/services/identity/runtime-config.ts +6 -5
- package/src/services/index.ts +15 -0
- package/src/services/ipa/posix.ts +1 -1
- package/src/services/ipa/sync.ts +1 -1
- package/src/services/ipa/users.ts +268 -221
- package/src/services/legal-consent.ts +1 -1
- package/src/services/logging/index.ts +15 -13
- package/src/services/logging/trace.ts +2 -1
- package/src/services/logging/web-vitals.ts +81 -0
- package/src/services/mandates/index.ts +21 -4
- package/src/services/mandates/policy.ts +165 -2
- package/src/services/notifications/batches.ts +125 -57
- package/src/services/notifications/browser.ts +12 -0
- package/src/services/notifications/index.ts +0 -2
- package/src/services/notifications/platform.ts +0 -4
- package/src/services/oauth-tokens.ts +3 -3
- package/src/services/pdf/gotenberg.ts +131 -72
- package/src/services/pdf/index.ts +11 -8
- package/src/services/providers/local/users.ts +25 -16
- package/src/services/public-http.ts +101 -0
- package/src/services/rail-shortcuts.ts +95 -0
- package/src/services/rail-snapshot.ts +53 -0
- package/src/services/request-cache-redis.ts +29 -0
- package/src/services/session/index.ts +15 -2
- package/src/services/session/user.ts +62 -37
- package/src/services/settings/app.ts +5 -1
- package/src/services/settings/core-settings.ts +26 -35
- package/src/services/settings/defaults.ts +5 -0
- package/src/services/settings/index.ts +3 -3
- package/src/services/settings/store.ts +58 -84
- package/src/services/webauthn.ts +1 -1
- package/src/shared/ai-costs.ts +42 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/ai-quotas.ts +136 -0
- package/src/shared/ai-usage.ts +6 -16
- package/src/shared/app-presentation.ts +21 -15
- package/src/shared/capability-messages.ts +14 -0
- package/src/shared/help.ts +6 -42
- package/src/shared/index.ts +3 -0
- package/src/shared/markdown/extensions/info-blocks.ts +1 -1
- package/src/ssr/AdminLayout.tsx +7 -5
- package/src/ssr/AdminSidebar.tsx +30 -16
- package/src/ssr/AppLaunchpad.island.tsx +33 -136
- package/src/ssr/AppLaunchpadPanel.tsx +181 -0
- package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
- package/src/ssr/GlobalSearchDialog.tsx +167 -20
- package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
- package/src/ssr/Layout.tsx +17 -6
- package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +46 -14
- package/src/ssr/LayoutHelp.tsx +22 -23
- package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
- package/src/ssr/LayoutRail.tsx +17 -9
- package/src/ssr/MinimalLayout.tsx +1 -1
- package/src/ssr/MobileNavigation.tsx +71 -0
- package/src/ssr/MobileProfileActions.tsx +49 -0
- package/src/ssr/ProfilePreferences.island.tsx +3 -36
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/RailEditor.tsx +17 -2
- package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
- package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
- package/src/ssr/admin-navigation.ts +11 -1
- package/src/ssr/help.ts +29 -0
- package/src/ssr/islands/SearchBar.tsx +67 -0
- package/src/ssr/islands/index.ts +3 -1
- package/src/ssr/layout-context.ts +2 -2
- package/src/ssr/layout-help-search.ts +15 -0
- package/src/ssr/mobile-menu-history.ts +70 -0
- package/src/ssr/platform-messages.ts +16 -8
- package/src/ssr/profile-actions.ts +42 -0
- package/src/ssr/rail-context.ts +8 -4
- package/src/ssr/rail-messages.ts +2 -0
- package/src/ssr/rail-navigation.ts +3 -3
- package/src/ssr/workspace-navigation.ts +75 -0
- package/src/styles/effects.css +6 -6
- package/src/styles/global.css +54 -0
- package/src/styles/input.css +43 -41
- package/src/styles/resource-search.css +505 -0
- package/src/styles/utilities-feedback.css +13 -0
- package/src/styles/utilities-layout.css +1 -5
- package/src/styles/utilities-navigation.css +3 -39
- package/src/types/ambient.d.ts +5 -0
- package/src/workflows/ai/index.ts +2 -2
- package/src/workflows/ai/runtime.ts +19 -90
- package/src/workflows/ai/store.ts +2 -0
- package/src/workflows/ai/types.ts +6 -82
- package/src/workflows/ai-actions.ts +16 -13
- package/src/workflows/definition.ts +10 -5
- package/src/workflows/store/actions.ts +44 -20
- package/src/workflows/store/index.ts +4 -2
- package/src/workflows/store/runs.ts +36 -0
- package/src/workflows/store/worker-pool.ts +55 -0
- package/src/workflows/store/worker-runtime.ts +90 -0
- package/src/ai/kit-skill.ts +0 -22
- package/src/ai/usage-migrate.ts +0 -81
- package/src/browser/notification-target.ts +0 -27
- package/src/contracts/notification-live.ts +0 -66
- package/src/server/services/freeipa/test-certificates.ts +0 -44
- package/src/services/notifications/live.ts +0 -50
- package/src/services/session/test-fixture.ts +0 -18
- package/src/ssr/BrowserNotifications.island.tsx +0 -135
- package/src/ssr/islands/SearchBar.island.tsx +0 -80
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { sql } from "bun";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { CloudResourceRefSchema } from "../contracts/capabilities";
|
|
5
5
|
import { coreSettings } from "../services";
|
|
6
6
|
import { logger } from "../services/logging";
|
|
7
7
|
import { normalizeLocale } from "../shared/locale";
|
|
8
8
|
import { parseAiAttachmentMarkers } from "./attachments";
|
|
9
|
+
import { AiBackgroundAdmissionError, AiBackgroundCostError } from "./inference-calls";
|
|
10
|
+
import { type AiBackgroundMemoryProposal, aiMemories } from "./memories";
|
|
9
11
|
import { type AiMemoryLearningChange, aiMemoryLearningRuns } from "./memory-learning-runs";
|
|
10
12
|
import {
|
|
13
|
+
type AiMemoryWorkflowEvidence,
|
|
14
|
+
type AiMemoryWorkflowPattern,
|
|
11
15
|
listAiPendingWorkflowPatterns,
|
|
12
16
|
listAiTurnWorkflowEvidence,
|
|
13
17
|
markAiWorkflowPatternReviewed,
|
|
14
|
-
type AiMemoryWorkflowEvidence,
|
|
15
|
-
type AiMemoryWorkflowPattern,
|
|
16
18
|
} from "./memory-workflow-evidence";
|
|
17
|
-
import { type AiBackgroundMemoryProposal, aiMemories } from "./memories";
|
|
18
19
|
import { aiConversations } from "./store";
|
|
19
20
|
import type { RunAiStructuredInput, RunAiStructuredResult } from "./structured";
|
|
20
21
|
import { resolveAiBackgroundModel, runAiStructured } from "./structured";
|
|
@@ -217,13 +218,7 @@ export const buildMemoryLearningTranscript = (messages: AiStoredMessage[]): stri
|
|
|
217
218
|
messages
|
|
218
219
|
.flatMap((stored) => {
|
|
219
220
|
const message = stored.message;
|
|
220
|
-
if (
|
|
221
|
-
stored.kind !== "message" ||
|
|
222
|
-
message.role !== "user" ||
|
|
223
|
-
stored.meta?.agentMessage ||
|
|
224
|
-
stored.meta?.scheduledTask
|
|
225
|
-
)
|
|
226
|
-
return [];
|
|
221
|
+
if (stored.kind !== "message" || message.role !== "user" || stored.meta?.agentMessage || stored.meta?.scheduledTask) return [];
|
|
227
222
|
return message.content.flatMap((part) => {
|
|
228
223
|
if (typeof part === "string") return part.trim() ? [part.trim()] : [];
|
|
229
224
|
if (part.type !== "text") return [];
|
|
@@ -305,7 +300,15 @@ const estimatedTokens = (systemPrompt: string, taskInput: string): number =>
|
|
|
305
300
|
|
|
306
301
|
const workflowEvidenceKey = (pattern: AiMemoryWorkflowPattern): string =>
|
|
307
302
|
createHash("sha256")
|
|
308
|
-
.update(
|
|
303
|
+
.update(
|
|
304
|
+
JSON.stringify([
|
|
305
|
+
pattern.userId,
|
|
306
|
+
pattern.capabilityId,
|
|
307
|
+
pattern.resourceRef.type,
|
|
308
|
+
pattern.resourceRef.id,
|
|
309
|
+
...[...pattern.turnIds].sort(),
|
|
310
|
+
]),
|
|
311
|
+
)
|
|
309
312
|
.digest("hex");
|
|
310
313
|
|
|
311
314
|
const toProposal = (change: z.infer<typeof AiMemoryLearningChangeSchema>): AiBackgroundMemoryProposal => ({
|
|
@@ -329,7 +332,8 @@ const applyChanges = async (
|
|
|
329
332
|
!evidence.actions.some(
|
|
330
333
|
(action) => action.resourceRef.type === change.resourceRef?.type && action.resourceRef.id === change.resourceRef.id,
|
|
331
334
|
)
|
|
332
|
-
)
|
|
335
|
+
)
|
|
336
|
+
continue;
|
|
333
337
|
const results = await aiMemories.applyBackgroundProposal({
|
|
334
338
|
userId: evidence.candidate.userId,
|
|
335
339
|
sourceConversationId: evidence.candidate.conversationId,
|
|
@@ -376,9 +380,7 @@ const workflowPatternInput = async (
|
|
|
376
380
|
return {
|
|
377
381
|
source: evidence[0] ?? null,
|
|
378
382
|
mutableMemoryIds: new Set(
|
|
379
|
-
selected.memories
|
|
380
|
-
.filter((memory) => memory.source === "background" && memory.priority === "normal")
|
|
381
|
-
.map((memory) => memory.shortId),
|
|
383
|
+
selected.memories.filter((memory) => memory.source === "background" && memory.priority === "normal").map((memory) => memory.shortId),
|
|
382
384
|
),
|
|
383
385
|
input: [
|
|
384
386
|
`CAPABILITY: ${pattern.capabilityId}`,
|
|
@@ -395,7 +397,8 @@ const workflowPatternInput = async (
|
|
|
395
397
|
const usageNumbers = (usage: RunAiStructuredResult<z.ZodType>["usage"], reserved: number) => {
|
|
396
398
|
const inputTokens = usage?.input;
|
|
397
399
|
const outputTokens = usage?.output;
|
|
398
|
-
const total =
|
|
400
|
+
const total =
|
|
401
|
+
usage?.total ?? (inputTokens !== undefined || outputTokens !== undefined ? (inputTokens ?? 0) + (outputTokens ?? 0) : undefined);
|
|
399
402
|
return { inputTokens, outputTokens, accountedTokens: total ?? reserved };
|
|
400
403
|
};
|
|
401
404
|
|
|
@@ -414,8 +417,7 @@ export const learnAiMemoriesFromPrivateChats = async (
|
|
|
414
417
|
}
|
|
415
418
|
|
|
416
419
|
const configuredBudget =
|
|
417
|
-
input.deps?.monthlyTokenBudget ??
|
|
418
|
-
Number(await coreSettings.get<number>(AI_MEMORY_LEARNING_MONTHLY_TOKEN_BUDGET_SETTING_KEY));
|
|
420
|
+
input.deps?.monthlyTokenBudget ?? Number(await coreSettings.get<number>(AI_MEMORY_LEARNING_MONTHLY_TOKEN_BUDGET_SETTING_KEY));
|
|
419
421
|
const monthlyTokenBudget =
|
|
420
422
|
Number.isFinite(configuredBudget) && configuredBudget > 0
|
|
421
423
|
? Math.floor(configuredBudget)
|
|
@@ -432,9 +434,7 @@ export const learnAiMemoriesFromPrivateChats = async (
|
|
|
432
434
|
let defaultLocale: Promise<string> | undefined;
|
|
433
435
|
const resolveLearningLocale = (locale?: string | null): Promise<string> => {
|
|
434
436
|
if (locale) return Promise.resolve(normalizeLocale(locale));
|
|
435
|
-
defaultLocale ??= (input.deps?.readDefaultLocale ?? (() => coreSettings.get<string>("app.locale")))().then(
|
|
436
|
-
normalizeLocale,
|
|
437
|
-
);
|
|
437
|
+
defaultLocale ??= (input.deps?.readDefaultLocale ?? (() => coreSettings.get<string>("app.locale")))().then(normalizeLocale);
|
|
438
438
|
return defaultLocale;
|
|
439
439
|
};
|
|
440
440
|
|
|
@@ -514,6 +514,18 @@ export const learnAiMemoriesFromPrivateChats = async (
|
|
|
514
514
|
if (status === "skipped") summary.skipped += 1;
|
|
515
515
|
processedCandidates += 1;
|
|
516
516
|
} catch (error) {
|
|
517
|
+
if (error instanceof AiBackgroundCostError || error instanceof AiBackgroundAdmissionError) {
|
|
518
|
+
summary.skipped += 1;
|
|
519
|
+
if (runId)
|
|
520
|
+
await aiMemoryLearningRuns.finish({
|
|
521
|
+
runId,
|
|
522
|
+
status: "skipped",
|
|
523
|
+
durationMs: Date.now() - startedAt,
|
|
524
|
+
changes: [],
|
|
525
|
+
accountedTokens: 0,
|
|
526
|
+
});
|
|
527
|
+
return summary;
|
|
528
|
+
}
|
|
517
529
|
summary.failed += 1;
|
|
518
530
|
processedCandidates += 1;
|
|
519
531
|
await markFailed(candidate).catch(() => undefined);
|
|
@@ -564,7 +576,11 @@ export const learnAiMemoriesFromPrivateChats = async (
|
|
|
564
576
|
if (!runId) continue;
|
|
565
577
|
const result = await structured({
|
|
566
578
|
task: "memory-learn-workflow",
|
|
567
|
-
attribution: {
|
|
579
|
+
attribution: {
|
|
580
|
+
userId: pattern.userId,
|
|
581
|
+
conversationId: context.source.candidate.conversationId,
|
|
582
|
+
turnId: context.source.candidate.turnId,
|
|
583
|
+
},
|
|
568
584
|
appId: "ai",
|
|
569
585
|
systemPrompt: workflowPrompt,
|
|
570
586
|
input: context.input,
|
|
@@ -578,15 +594,19 @@ export const learnAiMemoriesFromPrivateChats = async (
|
|
|
578
594
|
if (result.output.workflow && (await learningEnabled(pattern.userId))) {
|
|
579
595
|
const memoryIds = result.output.workflow.memoryIds;
|
|
580
596
|
const action = memoryIds.length > 1 ? "merge" : memoryIds.length === 1 ? "replace" : "add";
|
|
581
|
-
changes = await applyChanges(
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
597
|
+
changes = await applyChanges(
|
|
598
|
+
context.source,
|
|
599
|
+
[
|
|
600
|
+
{
|
|
601
|
+
action,
|
|
602
|
+
kind: "workflow",
|
|
603
|
+
content: result.output.workflow.content,
|
|
604
|
+
memoryIds,
|
|
605
|
+
resourceRef: pattern.resourceRef,
|
|
606
|
+
},
|
|
607
|
+
],
|
|
608
|
+
context.mutableMemoryIds,
|
|
609
|
+
);
|
|
590
610
|
}
|
|
591
611
|
await markAiWorkflowPatternReviewed(pattern);
|
|
592
612
|
await aiMemoryLearningRuns.finish({
|
|
@@ -600,6 +620,18 @@ export const learnAiMemoriesFromPrivateChats = async (
|
|
|
600
620
|
summary.updated += changes.filter((change) => change.action === "updated" || change.action === "merged").length;
|
|
601
621
|
if (changes.length === 0) summary.skipped += 1;
|
|
602
622
|
} catch (error) {
|
|
623
|
+
if (error instanceof AiBackgroundCostError || error instanceof AiBackgroundAdmissionError) {
|
|
624
|
+
summary.skipped += 1;
|
|
625
|
+
if (runId)
|
|
626
|
+
await aiMemoryLearningRuns.finish({
|
|
627
|
+
runId,
|
|
628
|
+
status: "skipped",
|
|
629
|
+
durationMs: Date.now() - startedAt,
|
|
630
|
+
changes: [],
|
|
631
|
+
accountedTokens: 0,
|
|
632
|
+
});
|
|
633
|
+
return summary;
|
|
634
|
+
}
|
|
603
635
|
summary.failed += 1;
|
|
604
636
|
if (runId) {
|
|
605
637
|
await aiMemoryLearningRuns
|
package/src/ai/memory-tool.ts
CHANGED
|
@@ -108,11 +108,7 @@ export const createCloudAiMemoryTool = (evidence = "") =>
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
const memory = await aiMemories.getByShortId(userId, input.id);
|
|
111
|
-
if (
|
|
112
|
-
memory &&
|
|
113
|
-
!memoryMutationSupportedByEvidence(evidence, input.id) &&
|
|
114
|
-
!memoryMutationSupportedByEvidence(evidence, memory.content)
|
|
115
|
-
) {
|
|
111
|
+
if (memory && !memoryMutationSupportedByEvidence(evidence, input.id) && !memoryMutationSupportedByEvidence(evidence, memory.content)) {
|
|
116
112
|
return { ok: false, message: "A memory can only be forgotten when the user identifies it in this turn." };
|
|
117
113
|
}
|
|
118
114
|
if (!memory || !(await aiMemories.deleteByShortId(userId, input.id))) return { ok: false, message: "Memory not found." };
|
|
@@ -37,10 +37,15 @@ export const recordAiMemoryWorkflowEvidence = async (input: {
|
|
|
37
37
|
input.resources.flatMap((resource) => {
|
|
38
38
|
const parsed = CloudResourceRefSchema.safeParse(resource.ref);
|
|
39
39
|
if (!parsed.success) return [];
|
|
40
|
-
return [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
return [
|
|
41
|
+
[
|
|
42
|
+
`${parsed.data.type}\0${parsed.data.id}`,
|
|
43
|
+
{
|
|
44
|
+
ref: parsed.data,
|
|
45
|
+
...(resource.title ? { title: resource.title.trim().slice(0, MAX_RESOURCE_TITLE_CHARS) } : {}),
|
|
46
|
+
},
|
|
47
|
+
] as const,
|
|
48
|
+
];
|
|
44
49
|
}),
|
|
45
50
|
).values(),
|
|
46
51
|
].slice(0, MAX_RECEIPT_RESOURCES);
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { sql } from "bun";
|
|
2
|
+
import { logger } from "../services/logging";
|
|
3
|
+
import { aiChatAccessSubject, isAssistantChatTurn } from "./assistant-models";
|
|
4
|
+
import { editAiDraftText } from "./draft-content";
|
|
5
|
+
import { canonicalizeAiConversationAttachments } from "./file-context";
|
|
6
|
+
import { AI_FILES_MAX_CONVERSATION_BYTES_DEFAULT } from "./files-store";
|
|
7
|
+
import { aiInputToUserMessage, aiTurnInputToContent } from "./http";
|
|
8
|
+
import { AiQuotaError, aiQuotas } from "./quotas";
|
|
9
|
+
import { aiResourceMarker } from "./resource-markers";
|
|
10
|
+
import { aiConversations } from "./store";
|
|
11
|
+
import type { AiConversationService, AiDraftContentPart } from "./types";
|
|
12
|
+
|
|
13
|
+
const log = logger("ai:message-queue");
|
|
14
|
+
export const AI_MESSAGE_QUEUE_LIMIT = 32;
|
|
15
|
+
export class AiMessageQueueConflict extends Error {}
|
|
16
|
+
|
|
17
|
+
type Submission = Parameters<AiConversationService["submitChatTurn"]>[0];
|
|
18
|
+
|
|
19
|
+
export const migrateAiMessageQueue = async () => {
|
|
20
|
+
await sql`CREATE TABLE IF NOT EXISTS ai.queued_messages (
|
|
21
|
+
id uuid PRIMARY KEY, conversation_id uuid NOT NULL REFERENCES ai.conversations(id) ON DELETE CASCADE,
|
|
22
|
+
position bigint GENERATED ALWAYS AS IDENTITY, submission jsonb NOT NULL, content jsonb NOT NULL,
|
|
23
|
+
status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','failed','submitted','cancelled')),
|
|
24
|
+
error text, created_at timestamptz NOT NULL DEFAULT now()
|
|
25
|
+
)`.simple();
|
|
26
|
+
await sql`ALTER TABLE ai.queued_messages ADD COLUMN IF NOT EXISTS quota_checked_at TIMESTAMPTZ`.simple();
|
|
27
|
+
await sql`CREATE INDEX IF NOT EXISTS ai_queued_messages_pending ON ai.queued_messages(conversation_id, position) WHERE status IN ('pending','failed')`.simple();
|
|
28
|
+
await sql`CREATE TABLE IF NOT EXISTS ai.queued_message_files (
|
|
29
|
+
message_id uuid NOT NULL REFERENCES ai.queued_messages(id) ON DELETE CASCADE,
|
|
30
|
+
path text NOT NULL, bytes bytea NOT NULL, media_type text NOT NULL, size bigint NOT NULL,
|
|
31
|
+
origin text NOT NULL, dictation_recorded_at timestamptz, updated_at timestamptz NOT NULL, version bigint NOT NULL,
|
|
32
|
+
PRIMARY KEY(message_id, path)
|
|
33
|
+
)`.simple();
|
|
34
|
+
await sql`DROP TRIGGER IF EXISTS ai_live_queued_messages_changed ON ai.queued_messages`.simple();
|
|
35
|
+
await sql`CREATE TRIGGER ai_live_queued_messages_changed AFTER INSERT OR DELETE ON ai.queued_messages
|
|
36
|
+
FOR EACH ROW EXECUTE FUNCTION ai.live_conversation_child_changed('conversation-detail,conversation-list')`.simple();
|
|
37
|
+
await sql`DROP TRIGGER IF EXISTS ai_live_queued_messages_updated ON ai.queued_messages`.simple();
|
|
38
|
+
await sql`CREATE TRIGGER ai_live_queued_messages_updated AFTER UPDATE ON ai.queued_messages
|
|
39
|
+
FOR EACH ROW WHEN (OLD.status IS DISTINCT FROM NEW.status OR OLD.error IS DISTINCT FROM NEW.error OR OLD.content IS DISTINCT FROM NEW.content)
|
|
40
|
+
EXECUTE FUNCTION ai.live_conversation_child_changed('conversation-detail,conversation-list')`.simple();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const listQueuedMessages = async (conversationId: string) => {
|
|
44
|
+
const rows = await sql<{ id: string; content: AiDraftContentPart[]; error: string | null }[]>`
|
|
45
|
+
SELECT id, content, error FROM ai.queued_messages
|
|
46
|
+
WHERE conversation_id = ${conversationId}::uuid AND status IN ('pending','failed') ORDER BY position`;
|
|
47
|
+
return rows.map((row) => ({ id: row.id, content: row.content, failed: Boolean(row.error) }));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const queuedMessageAccepted = async (conversationId: string, id: string) => {
|
|
51
|
+
const [row] = await sql`SELECT id FROM ai.queued_messages WHERE id=${id}::uuid AND conversation_id=${conversationId}::uuid`;
|
|
52
|
+
return Boolean(row);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const enqueueChatMessage = async (id: string, input: Submission, content: AiDraftContentPart[]) =>
|
|
56
|
+
sql.begin(async (tx) => {
|
|
57
|
+
const [conversation] = await tx<{ draft_revision: number; project_id: string | null }[]>`
|
|
58
|
+
SELECT draft_revision, project_id FROM ai.conversations WHERE id = ${input.conversationId}::uuid AND archived_at IS NULL FOR UPDATE`;
|
|
59
|
+
if (!conversation) throw new AiMessageQueueConflict("Conversation not found.");
|
|
60
|
+
const [existing] =
|
|
61
|
+
await tx`SELECT id FROM ai.queued_messages WHERE id = ${id}::uuid AND conversation_id = ${input.conversationId}::uuid`;
|
|
62
|
+
if (existing) return;
|
|
63
|
+
if (Number(conversation.draft_revision) !== input.expectedDraftRevision)
|
|
64
|
+
throw new AiMessageQueueConflict("Conversation draft changed before the message was queued.");
|
|
65
|
+
if (conversation.project_id !== input.expectedProjectId)
|
|
66
|
+
throw new AiMessageQueueConflict("Conversation Project changed before the message was queued.");
|
|
67
|
+
const [count] = await tx<
|
|
68
|
+
{ count: number }[]
|
|
69
|
+
>`SELECT count(*)::int AS count FROM ai.queued_messages WHERE conversation_id = ${input.conversationId}::uuid AND status IN ('pending','failed')`;
|
|
70
|
+
if (count!.count >= AI_MESSAGE_QUEUE_LIMIT) throw new AiMessageQueueConflict("The message queue is full (32 messages).");
|
|
71
|
+
const [size] = await tx<
|
|
72
|
+
{ size: number }[]
|
|
73
|
+
>`SELECT COALESCE(sum(f.size),0)::float8 AS size FROM ai.queued_message_files f JOIN ai.queued_messages q ON q.id = f.message_id WHERE q.conversation_id = ${input.conversationId}::uuid`;
|
|
74
|
+
if (
|
|
75
|
+
Number(size!.size) + (input.runConfig.files?.attached ?? []).reduce((sum, file) => sum + file.size, 0) >
|
|
76
|
+
AI_FILES_MAX_CONVERSATION_BYTES_DEFAULT
|
|
77
|
+
)
|
|
78
|
+
throw new AiMessageQueueConflict("Queued attachments exceed the conversation file budget.");
|
|
79
|
+
const { expectedDraftRevision, ...submission } = input;
|
|
80
|
+
await tx`INSERT INTO ai.queued_messages(id, conversation_id, submission, content) VALUES (${id}::uuid, ${input.conversationId}::uuid, (${JSON.stringify(submission)}::text)::jsonb, (${JSON.stringify(content)}::text)::jsonb)`;
|
|
81
|
+
for (const file of input.runConfig.files?.attached ?? []) {
|
|
82
|
+
const copied =
|
|
83
|
+
await tx`INSERT INTO ai.queued_message_files(message_id,path,bytes,media_type,size,origin,dictation_recorded_at,updated_at,version)
|
|
84
|
+
SELECT ${id}::uuid,path,bytes,media_type,size,origin,dictation_recorded_at,updated_at,version FROM ai.files
|
|
85
|
+
WHERE conversation_id = ${input.conversationId}::uuid AND path = ${file.path} AND version = ${file.version ?? -1} RETURNING path`;
|
|
86
|
+
if (!copied.length) throw new AiMessageQueueConflict("An attached file changed before the message was queued.");
|
|
87
|
+
}
|
|
88
|
+
await tx`UPDATE ai.conversations SET draft_content = '[]'::jsonb, draft_revision = draft_revision + 1, draft_updated_at = now(), last_used_at = now(), done = CASE WHEN done IS TRUE THEN NULL ELSE done END WHERE id = ${input.conversationId}::uuid`;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/** Cancellation and retry share the conversation lock with promotion. */
|
|
92
|
+
export const updateQueuedMessage = async (conversationId: string, id: string, action: "cancel" | "retry") =>
|
|
93
|
+
sql.begin(async (tx) => {
|
|
94
|
+
await tx`SELECT id FROM ai.conversations WHERE id = ${conversationId}::uuid FOR UPDATE`;
|
|
95
|
+
const changed = await tx`UPDATE ai.queued_messages SET status = ${action === "cancel" ? "cancelled" : "pending"}, error = NULL
|
|
96
|
+
WHERE id = ${id}::uuid AND conversation_id = ${conversationId}::uuid AND status IN ('pending','failed') RETURNING id`;
|
|
97
|
+
if (action === "cancel" && changed.length) {
|
|
98
|
+
await tx`DELETE FROM ai.queued_message_files WHERE message_id = ${id}::uuid`;
|
|
99
|
+
await tx`UPDATE ai.queued_messages SET submission = '{}'::jsonb, content = '[]'::jsonb WHERE id = ${id}::uuid`;
|
|
100
|
+
}
|
|
101
|
+
return Boolean(changed.length);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/** Edit only queued text; immutable attachments and the independent composer draft stay intact. */
|
|
105
|
+
export const editQueuedMessage = async (conversationId: string, id: string, text: string) =>
|
|
106
|
+
sql.begin(async (tx) => {
|
|
107
|
+
await tx`SELECT id FROM ai.conversations WHERE id = ${conversationId}::uuid FOR UPDATE`;
|
|
108
|
+
const [row] = await tx<{ submission: Submission; content: AiDraftContentPart[] }[]>`SELECT submission, content FROM ai.queued_messages
|
|
109
|
+
WHERE id = ${id}::uuid AND conversation_id = ${conversationId}::uuid AND status IN ('pending','failed') FOR UPDATE`;
|
|
110
|
+
if (!row) return false;
|
|
111
|
+
const content = editAiDraftText(row.content, text);
|
|
112
|
+
const parts = content.map((part) =>
|
|
113
|
+
part.type === "text"
|
|
114
|
+
? part
|
|
115
|
+
: part.type === "project-file"
|
|
116
|
+
? { type: "text" as const, text: `Project file: ${part.path}` }
|
|
117
|
+
: part.type === "file"
|
|
118
|
+
? { type: "attachment" as const, path: part.path, mediaType: part.mediaType, size: part.size }
|
|
119
|
+
: { type: "text" as const, text: aiResourceMarker({ ref: part.ref, title: part.title, icon: part.icon, href: part.href }) },
|
|
120
|
+
);
|
|
121
|
+
const { message } = aiInputToUserMessage(aiTurnInputToContent({ content: parts }));
|
|
122
|
+
const userMessage = canonicalizeAiConversationAttachments(message, row.submission.runConfig.files);
|
|
123
|
+
if (userMessage.role !== "user") throw new Error("Queued message must be a user message.");
|
|
124
|
+
const submission = {
|
|
125
|
+
...row.submission,
|
|
126
|
+
userMessage,
|
|
127
|
+
runConfig: {
|
|
128
|
+
...row.submission.runConfig,
|
|
129
|
+
input: userMessage.content,
|
|
130
|
+
selectedSkillIds: [
|
|
131
|
+
...new Set(content.flatMap((part) => (part.type === "resource" && part.ref.type === "core.ai.skill" ? [part.ref.id] : []))),
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
await tx`UPDATE ai.queued_messages SET submission = (${JSON.stringify(submission)}::text)::jsonb,
|
|
136
|
+
content = (${JSON.stringify(content)}::text)::jsonb WHERE id = ${id}::uuid`;
|
|
137
|
+
return true;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/** Existing turn creation owns execution and recovery; the DB lock owns FIFO and deduplication. */
|
|
141
|
+
export const drainQueuedMessages = async (
|
|
142
|
+
enqueue: (job: { conversationId: string; turnId: string }) => Promise<unknown>,
|
|
143
|
+
conversationId?: string,
|
|
144
|
+
) => {
|
|
145
|
+
const rows = await sql<{ id: string; conversation_id: string; submission: Submission }[]>`
|
|
146
|
+
SELECT q.id, q.conversation_id, q.submission FROM ai.queued_messages q
|
|
147
|
+
JOIN ai.conversations c ON c.id = q.conversation_id
|
|
148
|
+
WHERE (q.status = 'pending' OR (q.status = 'failed' AND q.error IN ('quota_exhausted','quota_usage_unknown'))) AND c.archived_at IS NULL AND c.created_by_user_id IS NOT NULL
|
|
149
|
+
AND (${conversationId ?? null}::uuid IS NULL OR q.conversation_id = ${conversationId ?? null}::uuid)
|
|
150
|
+
AND NOT EXISTS (SELECT 1 FROM ai.queued_messages earlier WHERE earlier.conversation_id = q.conversation_id AND earlier.position < q.position AND earlier.status IN ('pending','failed'))
|
|
151
|
+
AND NOT EXISTS (SELECT 1 FROM ai.turns t WHERE t.conversation_id = q.conversation_id AND NOT COALESCE(t.run_config ? 'background', false) AND t.status IN ('queued','running','waiting_for_action'))
|
|
152
|
+
ORDER BY q.quota_checked_at NULLS FIRST, q.position LIMIT ${AI_MESSAGE_QUEUE_LIMIT}`;
|
|
153
|
+
for (const row of rows) {
|
|
154
|
+
try {
|
|
155
|
+
const quotaSubject = aiChatAccessSubject(row.submission.runConfig.actor);
|
|
156
|
+
if (isAssistantChatTurn(row.submission.runConfig) && quotaSubject)
|
|
157
|
+
await aiQuotas.assertAllowed(quotaSubject, row.submission.modelProfileId);
|
|
158
|
+
await sql`UPDATE ai.queued_messages SET status='pending', error=NULL WHERE id=${row.id}::uuid AND status='failed' AND error IN ('quota_exhausted','quota_usage_unknown')`;
|
|
159
|
+
const result = await aiConversations.submitChatTurn({ ...row.submission, queuedMessageId: row.id });
|
|
160
|
+
await enqueue({ conversationId: row.conversation_id, turnId: result.turn.id });
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error instanceof AiQuotaError) {
|
|
163
|
+
await sql`UPDATE ai.queued_messages SET status='failed', error=${error.code}, quota_checked_at=clock_timestamp() WHERE id=${row.id}::uuid AND status IN ('pending','failed')`;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
// Another dispatcher may have consumed it or a new foreground turn may have won.
|
|
167
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
168
|
+
if (reason === "Queued message is no longer ready." || reason.includes("idx_ai_turns_one_active_per_conversation")) continue;
|
|
169
|
+
await sql`UPDATE ai.queued_messages SET status = 'failed', error = 'dispatch_failed' WHERE id = ${row.id}::uuid AND status = 'pending'`;
|
|
170
|
+
log.error("Queued message dispatch failed", {
|
|
171
|
+
conversationId: row.conversation_id,
|
|
172
|
+
messageId: row.id,
|
|
173
|
+
code: "queue_dispatch_failed",
|
|
174
|
+
errorType: error instanceof Error ? error.name : "unknown",
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|