@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
package/src/ai/chat-tasks.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Message } from "@k2b/nessi";
|
|
2
2
|
import { type SQL, sql } from "bun";
|
|
3
|
+
import { getCapability } from "../_internal/registry";
|
|
3
4
|
import { logger } from "../services/logging";
|
|
4
5
|
import {
|
|
5
6
|
createMandate,
|
|
@@ -9,9 +10,14 @@ import {
|
|
|
9
10
|
pauseMandate,
|
|
10
11
|
resumeMandate,
|
|
11
12
|
revokeMandate,
|
|
13
|
+
type TaskGrant,
|
|
14
|
+
TaskGrantsSchema,
|
|
15
|
+
updateMandatePolicy,
|
|
16
|
+
validateMandateIssueAuthority,
|
|
12
17
|
} from "../services/mandates";
|
|
13
18
|
import { parsePgJsonValue } from "../services/postgres";
|
|
14
19
|
import { withAiShortIdForDb } from "./short-id";
|
|
20
|
+
import { aiConversations } from "./store";
|
|
15
21
|
import type { AiChatTurnRunConfig, AiStoredMessage, AiTurnStatus } from "./types";
|
|
16
22
|
|
|
17
23
|
export type AiChatTaskState = "active" | "paused" | "completed" | "needs_attention";
|
|
@@ -25,6 +31,7 @@ export type AiChatTask = {
|
|
|
25
31
|
sponsorUserId: string;
|
|
26
32
|
mandateId: string | null;
|
|
27
33
|
mandateRevision: number | null;
|
|
34
|
+
grants: TaskGrant[];
|
|
28
35
|
prompt: string;
|
|
29
36
|
schedule: AiChatTaskSchedule;
|
|
30
37
|
timezone: string;
|
|
@@ -45,6 +52,7 @@ export type AiChatTaskOccurrence = {
|
|
|
45
52
|
state: AiChatTaskOccurrenceState;
|
|
46
53
|
turnId: string | null;
|
|
47
54
|
error: string | null;
|
|
55
|
+
resultText: string | null;
|
|
48
56
|
createdAt: string;
|
|
49
57
|
startedAt: string | null;
|
|
50
58
|
completedAt: string | null;
|
|
@@ -59,6 +67,7 @@ type TaskRow = {
|
|
|
59
67
|
sponsor_user_id: string;
|
|
60
68
|
mandate_id: string | null;
|
|
61
69
|
mandate_revision: number | bigint | null;
|
|
70
|
+
mandate_policy?: unknown;
|
|
62
71
|
prompt: string;
|
|
63
72
|
schedule_kind: "once" | "cron";
|
|
64
73
|
run_at: Date | string | null;
|
|
@@ -91,6 +100,8 @@ type OccurrenceRow = {
|
|
|
91
100
|
state: AiChatTaskOccurrenceState;
|
|
92
101
|
turn_id: string | null;
|
|
93
102
|
error: string | null;
|
|
103
|
+
result_text?: string | null;
|
|
104
|
+
request_key?: string;
|
|
94
105
|
created_at: Date | string;
|
|
95
106
|
started_at: Date | string | null;
|
|
96
107
|
completed_at: Date | string | null;
|
|
@@ -99,21 +110,45 @@ type OccurrenceRow = {
|
|
|
99
110
|
const iso = (value: Date | string): string => new Date(value).toISOString();
|
|
100
111
|
const nullableIso = (value: Date | string | null): string | null => (value === null ? null : iso(value));
|
|
101
112
|
const taskIdempotencyKey = (key: string): string => `task.create:${key}`;
|
|
102
|
-
const
|
|
113
|
+
const chatTaskMandatePolicy = (grants: TaskGrant[]): MandatePolicyV1 => ({
|
|
103
114
|
version: 1,
|
|
104
|
-
apps: "
|
|
105
|
-
operations:
|
|
106
|
-
|
|
107
|
-
|
|
115
|
+
apps: [...new Set(grants.map((grant) => ("appId" in grant ? grant.appId : "assistant")))],
|
|
116
|
+
operations: [
|
|
117
|
+
...new Set(
|
|
118
|
+
grants.flatMap((grant) =>
|
|
119
|
+
!("appId" in grant)
|
|
120
|
+
? [`runtime.${grant.kind}`]
|
|
121
|
+
: grant.kind === "query"
|
|
122
|
+
? [`capability.query:${grant.capabilityId}`]
|
|
123
|
+
: [`capability.action.review:${grant.capabilityId}`, `capability.action.run:${grant.capabilityId}`],
|
|
124
|
+
),
|
|
125
|
+
),
|
|
126
|
+
],
|
|
127
|
+
actions: grants.length ? "preapproved" : "deny",
|
|
128
|
+
grants,
|
|
129
|
+
});
|
|
130
|
+
const validateTaskGrants = async (input: unknown): Promise<TaskGrant[]> => {
|
|
131
|
+
const grants = TaskGrantsSchema.parse(input);
|
|
132
|
+
for (const grant of grants) {
|
|
133
|
+
if (!("appId" in grant)) continue;
|
|
134
|
+
const entry = await getCapability(grant.appId);
|
|
135
|
+
const operation = (grant.kind === "query" ? entry?.manifest.queries : entry?.manifest.actions)?.find(
|
|
136
|
+
(item) => item.localId === grant.capabilityId,
|
|
137
|
+
);
|
|
138
|
+
if (!operation) throw new Error(`Capability unavailable: ${grant.appId}.${grant.capabilityId}`);
|
|
139
|
+
if (grant.kind === "action" && (!("approval" in operation) || (operation.approval !== "none" && operation.approval !== "rememberable")))
|
|
140
|
+
throw new Error(`Capability always requires interactive approval: ${grant.appId}.${grant.capabilityId}`);
|
|
141
|
+
const properties = operation.inputSchema.properties ?? {};
|
|
142
|
+
for (const key of Object.keys(grant.fixedInput))
|
|
143
|
+
if (!Object.hasOwn(properties, key)) throw new Error(`Unknown fixed input ${key} for ${grant.appId}.${grant.capabilityId}`);
|
|
144
|
+
}
|
|
145
|
+
return grants;
|
|
146
|
+
};
|
|
108
147
|
const mandateAuthority = (userId: string) => ({ kind: "interactive" as const, userId });
|
|
109
148
|
const mandateWorkloadAuthority = { kind: "workload" as const, ownerAppId: "core" };
|
|
110
149
|
const mandateError = (operation: string, result: { ok: false; error: { message: string } }): Error =>
|
|
111
150
|
new AiChatTaskAuthorityError(`Could not ${operation} scheduled task mandate: ${result.error.message}`);
|
|
112
|
-
const isChatTaskMandatePolicy = (policy: MandatePolicyV1): boolean =>
|
|
113
|
-
policy.version === CHAT_TASK_MANDATE_POLICY.version &&
|
|
114
|
-
policy.apps === CHAT_TASK_MANDATE_POLICY.apps &&
|
|
115
|
-
policy.operations === CHAT_TASK_MANDATE_POLICY.operations &&
|
|
116
|
-
policy.actions === CHAT_TASK_MANDATE_POLICY.actions;
|
|
151
|
+
const isChatTaskMandatePolicy = (policy: MandatePolicyV1): boolean => policy.grants !== undefined;
|
|
117
152
|
const mandateRevision = async (
|
|
118
153
|
task: { mandate_id: string; mandate_revision: number | bigint },
|
|
119
154
|
state: "active" | "paused",
|
|
@@ -187,6 +222,26 @@ const stopTaskAdmission = async (
|
|
|
187
222
|
revision = revision + 1, updated_at = now()
|
|
188
223
|
WHERE id = ${task.id}::uuid AND state = 'active'
|
|
189
224
|
`;
|
|
225
|
+
if (state === "needs_attention" && reason) {
|
|
226
|
+
const occurrences = await db<{ id: string }[]>`
|
|
227
|
+
UPDATE ai.chat_task_occurrences SET state = 'failed', error = ${reason}, completed_at = now()
|
|
228
|
+
WHERE task_id = ${task.id}::uuid AND state = 'queued' RETURNING id
|
|
229
|
+
`;
|
|
230
|
+
if (!occurrences.length) {
|
|
231
|
+
// Reconciliation can detect revoked authority before the next occurrence exists.
|
|
232
|
+
const failed = await withAiShortIdForDb(
|
|
233
|
+
db,
|
|
234
|
+
"ai_chat_task_occurrences_short_id_unique",
|
|
235
|
+
(attempt, shortId) => attempt<{ id: string }[]>`
|
|
236
|
+
INSERT INTO ai.chat_task_occurrences(short_id, task_id, scheduled_for, trigger, request_key, task_revision, state, error, completed_at)
|
|
237
|
+
VALUES (${shortId}, ${task.id}::uuid, now(), 'scheduled', ${`authority:${task.id}:${task.revision}`}, ${task.revision}, 'failed', ${reason}, now())
|
|
238
|
+
ON CONFLICT DO NOTHING RETURNING id
|
|
239
|
+
`,
|
|
240
|
+
);
|
|
241
|
+
occurrences.push(...failed);
|
|
242
|
+
}
|
|
243
|
+
for (const occurrence of occurrences) await deliverOccurrenceResult(db, task, occurrence.id, reason);
|
|
244
|
+
}
|
|
190
245
|
return null;
|
|
191
246
|
};
|
|
192
247
|
/** The caller holds the task lock; mandate and sponsor locks remain held until turn admission commits. */
|
|
@@ -233,14 +288,30 @@ const pauseTerminalTaskMandate = async (task: TaskRow, db: SQL): Promise<number
|
|
|
233
288
|
if (!paused.ok) throw mandateError("pause", paused);
|
|
234
289
|
return paused.data.revision;
|
|
235
290
|
};
|
|
236
|
-
const taskFingerprint = (input: {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
291
|
+
const taskFingerprint = (input: {
|
|
292
|
+
chatId: string;
|
|
293
|
+
prompt: string;
|
|
294
|
+
schedule: AiChatTaskSchedule;
|
|
295
|
+
timezone: string;
|
|
296
|
+
grants?: TaskGrant[];
|
|
297
|
+
}): string =>
|
|
298
|
+
new Bun.CryptoHasher("sha256")
|
|
299
|
+
.update(JSON.stringify([input.chatId, input.prompt.trim(), input.schedule, input.timezone, input.grants ?? []]))
|
|
300
|
+
.digest("hex");
|
|
301
|
+
const taskSelect = () => sql`
|
|
302
|
+
SELECT task.*, (SELECT policy FROM auth.mandates WHERE id = task.mandate_id) AS mandate_policy, conversation.short_id AS conversation_short_id, conversation.title AS conversation_title
|
|
240
303
|
FROM ai.chat_tasks task
|
|
241
304
|
JOIN ai.conversations conversation ON conversation.id = task.conversation_id
|
|
242
305
|
`;
|
|
243
306
|
|
|
307
|
+
const taskGrants = (row: TaskRow): TaskGrant[] => {
|
|
308
|
+
try {
|
|
309
|
+
const policy = MandatePolicyV1Schema.safeParse(parsePgJsonValue(row.mandate_policy));
|
|
310
|
+
return policy.success ? (policy.data.grants ?? []) : [];
|
|
311
|
+
} catch {
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
314
|
+
};
|
|
244
315
|
const toTask = (row: TaskRow): AiChatTask => ({
|
|
245
316
|
id: row.id,
|
|
246
317
|
shortId: row.short_id,
|
|
@@ -250,6 +321,7 @@ const toTask = (row: TaskRow): AiChatTask => ({
|
|
|
250
321
|
sponsorUserId: row.sponsor_user_id,
|
|
251
322
|
mandateId: row.mandate_id,
|
|
252
323
|
mandateRevision: row.mandate_revision === null ? null : Number(row.mandate_revision),
|
|
324
|
+
grants: taskGrants(row),
|
|
253
325
|
prompt: row.prompt,
|
|
254
326
|
schedule: row.schedule_kind === "once" ? { kind: "once", runAt: iso(row.run_at!) } : { kind: "cron", cron: row.cron! },
|
|
255
327
|
timezone: row.timezone,
|
|
@@ -269,6 +341,7 @@ const toOccurrence = (row: OccurrenceRow): AiChatTaskOccurrence => ({
|
|
|
269
341
|
state: row.state,
|
|
270
342
|
turnId: row.turn_id,
|
|
271
343
|
error: row.error,
|
|
344
|
+
resultText: row.result_text ?? null,
|
|
272
345
|
createdAt: iso(row.created_at),
|
|
273
346
|
startedAt: nullableIso(row.started_at),
|
|
274
347
|
completedAt: nullableIso(row.completed_at),
|
|
@@ -277,7 +350,7 @@ const toOccurrence = (row: OccurrenceRow): AiChatTaskOccurrence => ({
|
|
|
277
350
|
const prepareTaskById = (taskId: string): Promise<AiChatTask | null> =>
|
|
278
351
|
sql.begin(async (tx) => {
|
|
279
352
|
const [task] = await tx<TaskRow[]>`
|
|
280
|
-
${taskSelect}
|
|
353
|
+
${taskSelect()}
|
|
281
354
|
WHERE task.id = ${taskId}::uuid
|
|
282
355
|
FOR UPDATE OF task
|
|
283
356
|
`;
|
|
@@ -286,7 +359,7 @@ const prepareTaskById = (taskId: string): Promise<AiChatTask | null> =>
|
|
|
286
359
|
});
|
|
287
360
|
const loadOccurrenceTask = async (occurrenceId: string, db: SQL): Promise<TaskRow | null> => {
|
|
288
361
|
const [task] = await db<TaskRow[]>`
|
|
289
|
-
${taskSelect}
|
|
362
|
+
${taskSelect()}
|
|
290
363
|
WHERE EXISTS (
|
|
291
364
|
SELECT 1 FROM ai.chat_task_occurrences occurrence
|
|
292
365
|
WHERE occurrence.id = ${occurrenceId}::uuid AND occurrence.task_id = task.id
|
|
@@ -297,7 +370,7 @@ const loadOccurrenceTask = async (occurrenceId: string, db: SQL): Promise<TaskRo
|
|
|
297
370
|
};
|
|
298
371
|
const loadTurnTask = async (turnId: string, db: SQL): Promise<TaskRow | null> => {
|
|
299
372
|
const [task] = await db<TaskRow[]>`
|
|
300
|
-
${taskSelect}
|
|
373
|
+
${taskSelect()}
|
|
301
374
|
WHERE EXISTS (
|
|
302
375
|
SELECT 1 FROM ai.chat_task_occurrences occurrence
|
|
303
376
|
WHERE occurrence.turn_id = ${turnId}::uuid AND occurrence.task_id = task.id
|
|
@@ -307,7 +380,128 @@ const loadTurnTask = async (turnId: string, db: SQL): Promise<TaskRow | null> =>
|
|
|
307
380
|
return task ?? null;
|
|
308
381
|
};
|
|
309
382
|
|
|
383
|
+
/** Called in the occurrence terminal transition transaction, so delivery is exactly once. */
|
|
384
|
+
const deliverOccurrenceResult = async (db: SQL, task: TaskRow, occurrenceId: string, resultText: string): Promise<void> => {
|
|
385
|
+
const [occurrence] = await db<OccurrenceRow[]>`
|
|
386
|
+
UPDATE ai.chat_task_occurrences SET result_text = ${resultText}, delivered_at = now()
|
|
387
|
+
WHERE id = ${occurrenceId}::uuid AND delivered_at IS NULL RETURNING *
|
|
388
|
+
`;
|
|
389
|
+
if (!occurrence) return;
|
|
390
|
+
const [destination] = await db<{ id: string }[]>`
|
|
391
|
+
SELECT id FROM ai.conversations WHERE id = ${task.conversation_id}::uuid AND archived_at IS NULL FOR UPDATE
|
|
392
|
+
`;
|
|
393
|
+
if (!destination) return;
|
|
394
|
+
const message: Message = { role: "assistant", content: [{ type: "text", text: resultText }] };
|
|
395
|
+
const meta = {
|
|
396
|
+
scheduledTask: {
|
|
397
|
+
taskId: task.short_id,
|
|
398
|
+
occurrenceId: occurrence.short_id,
|
|
399
|
+
scheduledFor: iso(occurrence.scheduled_for),
|
|
400
|
+
trigger: occurrence.trigger,
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
await withAiShortIdForDb(
|
|
404
|
+
db,
|
|
405
|
+
"idx_ai_messages_conversation_short_id",
|
|
406
|
+
(attempt, shortId) => attempt`
|
|
407
|
+
INSERT INTO ai.messages(short_id, conversation_id, seq, kind, role, message, search_text, meta)
|
|
408
|
+
VALUES (${shortId}, ${task.conversation_id}::uuid,
|
|
409
|
+
(SELECT COALESCE(MAX(seq),0)+1 FROM ai.messages WHERE conversation_id = ${task.conversation_id}::uuid),
|
|
410
|
+
'message', 'assistant', ${JSON.stringify(message)}::text::jsonb, ${resultText}, ${JSON.stringify(meta)}::text::jsonb)
|
|
411
|
+
`,
|
|
412
|
+
);
|
|
413
|
+
await db`UPDATE ai.conversations SET done = NULL, last_used_at = now(), updated_at = now(), background_received_at = now() WHERE id = ${task.conversation_id}::uuid`;
|
|
414
|
+
};
|
|
415
|
+
|
|
310
416
|
export const aiChatTasks = {
|
|
417
|
+
listActivities: async (input: { userId: string; limit?: number; offset?: number }) => {
|
|
418
|
+
const limit = Math.min(Math.max(input.limit ?? 50, 1), 100);
|
|
419
|
+
const rows = await sql<(OccurrenceRow & { unread: boolean })[]>`
|
|
420
|
+
SELECT occurrence.*, (occurrence.delivered_at > COALESCE(conversation.last_viewed_at, '-infinity')) AS unread
|
|
421
|
+
FROM ai.chat_task_occurrences occurrence
|
|
422
|
+
JOIN ai.chat_tasks task ON task.id = occurrence.task_id
|
|
423
|
+
JOIN ai.conversations conversation ON conversation.id = task.conversation_id
|
|
424
|
+
WHERE task.sponsor_user_id = ${input.userId}::uuid AND conversation.created_by_user_id = ${input.userId}::uuid
|
|
425
|
+
AND conversation.archived_at IS NULL
|
|
426
|
+
ORDER BY occurrence.created_at DESC, occurrence.id DESC LIMIT ${limit + 1} OFFSET ${Math.max(input.offset ?? 0, 0)}
|
|
427
|
+
`;
|
|
428
|
+
const page = rows.slice(0, limit);
|
|
429
|
+
if (!page.length) return { items: [], hasMore: false };
|
|
430
|
+
const tasks = await sql<TaskRow[]>`${taskSelect()} WHERE task.id IN
|
|
431
|
+
(SELECT value::uuid FROM jsonb_array_elements_text(${JSON.stringify([...new Set(page.map((row) => row.task_id))])}::text::jsonb))`;
|
|
432
|
+
const byId = new Map(tasks.map((row) => [row.id, toTask(row)]));
|
|
433
|
+
return {
|
|
434
|
+
items: page.flatMap((row) => {
|
|
435
|
+
const task = byId.get(row.task_id);
|
|
436
|
+
return task
|
|
437
|
+
? [
|
|
438
|
+
{
|
|
439
|
+
task,
|
|
440
|
+
occurrence: toOccurrence(row),
|
|
441
|
+
chatTitle: task.chatTitle,
|
|
442
|
+
unread: Boolean(row.unread),
|
|
443
|
+
},
|
|
444
|
+
]
|
|
445
|
+
: [];
|
|
446
|
+
}),
|
|
447
|
+
hasMore: rows.length > limit,
|
|
448
|
+
};
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
getOccurrenceDetail: async (input: { userId: string; taskId: string; occurrenceId: string }) => {
|
|
452
|
+
const task = await aiChatTasks.get({ userId: input.userId, taskId: input.taskId });
|
|
453
|
+
if (!task) return null;
|
|
454
|
+
const [row] = await sql<
|
|
455
|
+
OccurrenceRow[]
|
|
456
|
+
>`SELECT * FROM ai.chat_task_occurrences WHERE task_id = ${task.id}::uuid AND short_id = ${input.occurrenceId}`;
|
|
457
|
+
if (!row) return null;
|
|
458
|
+
const messages = row.turn_id
|
|
459
|
+
? await aiConversations.listTurnMessages({ conversationId: task.conversationId, loopId: row.turn_id, includeCompacted: true })
|
|
460
|
+
: [];
|
|
461
|
+
return { task, occurrence: toOccurrence(row), messages };
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
validateGrants: validateTaskGrants,
|
|
465
|
+
authorizeRuntime: async (input: {
|
|
466
|
+
mandate: { id: string; revision: number };
|
|
467
|
+
kind: "http" | "database";
|
|
468
|
+
input: unknown;
|
|
469
|
+
}): Promise<void> => {
|
|
470
|
+
const result = await validateMandateIssueAuthority({
|
|
471
|
+
mandateId: input.mandate.id,
|
|
472
|
+
expectedRevision: input.mandate.revision,
|
|
473
|
+
ownerAppId: "core",
|
|
474
|
+
targetAppId: "assistant",
|
|
475
|
+
operation: `runtime.${input.kind}`,
|
|
476
|
+
input: input.input,
|
|
477
|
+
});
|
|
478
|
+
if (!result.ok)
|
|
479
|
+
throw new AiChatTaskAuthorityError(
|
|
480
|
+
`Background ${input.kind} access denied: ${result.error.message}. Update this task's grants in the normal chat.`,
|
|
481
|
+
);
|
|
482
|
+
},
|
|
483
|
+
authorizeCapability: async (input: {
|
|
484
|
+
mandate: { id: string; revision: number };
|
|
485
|
+
appId: string;
|
|
486
|
+
capabilityId: string;
|
|
487
|
+
kind: "query" | "action";
|
|
488
|
+
input: unknown;
|
|
489
|
+
approval?: "none" | "rememberable" | "always";
|
|
490
|
+
}): Promise<void> => {
|
|
491
|
+
const result = await validateMandateIssueAuthority({
|
|
492
|
+
mandateId: input.mandate.id,
|
|
493
|
+
expectedRevision: input.mandate.revision,
|
|
494
|
+
ownerAppId: "core",
|
|
495
|
+
targetAppId: input.appId,
|
|
496
|
+
operation: `capability.${input.kind === "query" ? "query" : "action.run"}:${input.capabilityId}`,
|
|
497
|
+
input: input.input,
|
|
498
|
+
capabilityApproval: input.approval,
|
|
499
|
+
});
|
|
500
|
+
if (!result.ok)
|
|
501
|
+
throw new AiChatTaskAuthorityError(
|
|
502
|
+
`Background task cannot call ${input.appId}.${input.capabilityId}: ${result.error.message}. Check the task grants and fixed inputs in the normal chat; this run cannot expand them.`,
|
|
503
|
+
);
|
|
504
|
+
},
|
|
311
505
|
list: async (input: {
|
|
312
506
|
userId: string;
|
|
313
507
|
chatId?: string;
|
|
@@ -316,7 +510,7 @@ export const aiChatTasks = {
|
|
|
316
510
|
offset?: number;
|
|
317
511
|
}): Promise<AiChatTask[]> => {
|
|
318
512
|
const rows = await sql<TaskRow[]>`
|
|
319
|
-
${taskSelect}
|
|
513
|
+
${taskSelect()}
|
|
320
514
|
WHERE task.sponsor_user_id = ${input.userId}::uuid
|
|
321
515
|
AND (${input.chatId ?? null}::text IS NULL OR conversation.short_id = ${input.chatId ?? null})
|
|
322
516
|
AND (${input.state ?? null}::text IS NULL OR task.state = ${input.state ?? null})
|
|
@@ -329,7 +523,7 @@ export const aiChatTasks = {
|
|
|
329
523
|
|
|
330
524
|
get: async (input: { userId: string; taskId: string }): Promise<AiChatTask | null> => {
|
|
331
525
|
const rows = await sql<TaskRow[]>`
|
|
332
|
-
${taskSelect}
|
|
526
|
+
${taskSelect()}
|
|
333
527
|
WHERE task.sponsor_user_id = ${input.userId}::uuid
|
|
334
528
|
AND task.short_id = ${input.taskId}
|
|
335
529
|
LIMIT 1
|
|
@@ -343,6 +537,7 @@ export const aiChatTasks = {
|
|
|
343
537
|
prompt: string;
|
|
344
538
|
schedule: AiChatTaskSchedule;
|
|
345
539
|
timezone: string;
|
|
540
|
+
grants?: TaskGrant[];
|
|
346
541
|
idempotencyKey?: string;
|
|
347
542
|
idempotencyFingerprint?: string;
|
|
348
543
|
}): Promise<AiChatTask | null> => {
|
|
@@ -350,7 +545,7 @@ export const aiChatTasks = {
|
|
|
350
545
|
const fingerprint = scopedKey ? (input.idempotencyFingerprint ?? taskFingerprint(input)) : null;
|
|
351
546
|
if (input.idempotencyKey) {
|
|
352
547
|
const existing = await sql<(TaskRow & { idempotency_fingerprint: string | null })[]>`
|
|
353
|
-
${taskSelect}
|
|
548
|
+
${taskSelect()}
|
|
354
549
|
WHERE task.idempotency_key = ${scopedKey}
|
|
355
550
|
AND task.sponsor_user_id = ${input.userId}::uuid
|
|
356
551
|
LIMIT 1
|
|
@@ -360,6 +555,7 @@ export const aiChatTasks = {
|
|
|
360
555
|
return toTask(existing[0]);
|
|
361
556
|
}
|
|
362
557
|
}
|
|
558
|
+
const grants = await validateTaskGrants(input.grants ?? []);
|
|
363
559
|
let rows: TaskRow[];
|
|
364
560
|
try {
|
|
365
561
|
rows = await sql.begin(async (tx) => {
|
|
@@ -380,7 +576,7 @@ export const aiChatTasks = {
|
|
|
380
576
|
ownerAppId: "core",
|
|
381
577
|
workloadType: "ai.chat-task",
|
|
382
578
|
workloadId: taskId,
|
|
383
|
-
policy:
|
|
579
|
+
policy: chatTaskMandatePolicy(grants),
|
|
384
580
|
},
|
|
385
581
|
{ db: tx },
|
|
386
582
|
);
|
|
@@ -405,6 +601,7 @@ export const aiChatTasks = {
|
|
|
405
601
|
);
|
|
406
602
|
const task = inserted[0];
|
|
407
603
|
if (!task) throw new AiChatTaskCreateRace();
|
|
604
|
+
task.mandate_policy = mandate.data.policy;
|
|
408
605
|
return inserted;
|
|
409
606
|
});
|
|
410
607
|
} catch (error) {
|
|
@@ -414,7 +611,7 @@ export const aiChatTasks = {
|
|
|
414
611
|
if (rows[0]) return toTask(rows[0]);
|
|
415
612
|
if (!input.idempotencyKey) return null;
|
|
416
613
|
const existing = await sql<(TaskRow & { idempotency_fingerprint: string | null })[]>`
|
|
417
|
-
${taskSelect}
|
|
614
|
+
${taskSelect()}
|
|
418
615
|
WHERE task.idempotency_key = ${scopedKey}
|
|
419
616
|
AND task.sponsor_user_id = ${input.userId}::uuid
|
|
420
617
|
LIMIT 1
|
|
@@ -430,7 +627,7 @@ export const aiChatTasks = {
|
|
|
430
627
|
idempotencyFingerprint: string;
|
|
431
628
|
}): Promise<AiChatTask | null> => {
|
|
432
629
|
const rows = await sql<(TaskRow & { idempotency_fingerprint: string | null })[]>`
|
|
433
|
-
${taskSelect}
|
|
630
|
+
${taskSelect()}
|
|
434
631
|
WHERE task.idempotency_key = ${taskIdempotencyKey(input.idempotencyKey)}
|
|
435
632
|
AND task.sponsor_user_id = ${input.userId}::uuid
|
|
436
633
|
LIMIT 1
|
|
@@ -444,12 +641,13 @@ export const aiChatTasks = {
|
|
|
444
641
|
userId: string;
|
|
445
642
|
taskId: string;
|
|
446
643
|
prompt?: string;
|
|
644
|
+
grants?: TaskGrant[];
|
|
447
645
|
schedule?: AiChatTaskSchedule;
|
|
448
646
|
timezone?: string;
|
|
449
647
|
}): Promise<AiChatTask | null> =>
|
|
450
648
|
sql.begin(async (tx) => {
|
|
451
649
|
const rows = await tx<TaskRow[]>`
|
|
452
|
-
${taskSelect}
|
|
650
|
+
${taskSelect()}
|
|
453
651
|
WHERE task.sponsor_user_id = ${input.userId}::uuid
|
|
454
652
|
AND task.short_id = ${input.taskId}
|
|
455
653
|
FOR UPDATE OF task
|
|
@@ -460,7 +658,7 @@ export const aiChatTasks = {
|
|
|
460
658
|
const wasActive = current.state === "active";
|
|
461
659
|
const authorityStopped = wasActive && !(await prepareTaskForExecution(current, tx));
|
|
462
660
|
if (authorityStopped) {
|
|
463
|
-
[current] = await tx<TaskRow[]>`${taskSelect} WHERE task.id = ${current.id}::uuid`;
|
|
661
|
+
[current] = await tx<TaskRow[]>`${taskSelect()} WHERE task.id = ${current.id}::uuid`;
|
|
464
662
|
if (!current) return null;
|
|
465
663
|
}
|
|
466
664
|
const nextTimezone = input.timezone ?? current.timezone;
|
|
@@ -470,10 +668,27 @@ export const aiChatTasks = {
|
|
|
470
668
|
nextTimezone !== current.timezone
|
|
471
669
|
: false;
|
|
472
670
|
const promptChanged = input.prompt !== undefined && input.prompt.trim() !== current.prompt;
|
|
473
|
-
|
|
671
|
+
const grants = input.grants === undefined ? undefined : await validateTaskGrants(input.grants);
|
|
672
|
+
let updatedMandateRevision = Number(current.mandate_revision);
|
|
673
|
+
if (grants !== undefined) {
|
|
674
|
+
const mandate = await loadTaskMandate(current, tx);
|
|
675
|
+
if (!mandate || !taskMandateMatches(current, mandate)) throw new AiChatTaskAuthorityError("Scheduled task mandate is unavailable");
|
|
676
|
+
const updated = await updateMandatePolicy(
|
|
677
|
+
{
|
|
678
|
+
mandateId: mandate.id,
|
|
679
|
+
expectedRevision: Number(mandate.revision),
|
|
680
|
+
authority: mandateAuthority(input.userId),
|
|
681
|
+
policy: chatTaskMandatePolicy(grants),
|
|
682
|
+
},
|
|
683
|
+
{ db: tx },
|
|
684
|
+
);
|
|
685
|
+
if (!updated.ok) throw mandateError("update", updated);
|
|
686
|
+
updatedMandateRevision = updated.data.revision;
|
|
687
|
+
}
|
|
688
|
+
if (!promptChanged && !scheduleChanged && grants === undefined) return toTask(current);
|
|
474
689
|
await tx`
|
|
475
690
|
UPDATE ai.chat_tasks task
|
|
476
|
-
SET prompt = ${input.prompt?.trim() ?? current.prompt},
|
|
691
|
+
SET mandate_revision = ${updatedMandateRevision}, prompt = ${input.prompt?.trim() ?? current.prompt},
|
|
477
692
|
schedule_kind = ${input.schedule?.kind ?? current.schedule_kind},
|
|
478
693
|
run_at = ${input.schedule ? (input.schedule.kind === "once" ? input.schedule.runAt : null) : current.run_at}::timestamptz,
|
|
479
694
|
cron = ${input.schedule ? (input.schedule.kind === "cron" ? input.schedule.cron : null) : current.cron},
|
|
@@ -499,7 +714,7 @@ export const aiChatTasks = {
|
|
|
499
714
|
WHERE task_id = ${current.id}::uuid AND state = 'queued'
|
|
500
715
|
`;
|
|
501
716
|
}
|
|
502
|
-
const updated = await tx<TaskRow[]>`${taskSelect} WHERE task.id = ${current.id}::uuid`;
|
|
717
|
+
const updated = await tx<TaskRow[]>`${taskSelect()} WHERE task.id = ${current.id}::uuid`;
|
|
503
718
|
const next = updated[0]!;
|
|
504
719
|
if (next.state === "active" && !wasActive) {
|
|
505
720
|
const revision = await mandateRevision(requireTaskMandate(next), "active", input.userId, tx);
|
|
@@ -514,7 +729,7 @@ export const aiChatTasks = {
|
|
|
514
729
|
setState: async (input: { userId: string; taskId: string; state: "active" | "paused" }): Promise<AiChatTask | null> => {
|
|
515
730
|
return sql.begin(async (tx) => {
|
|
516
731
|
const rows = await tx<TaskRow[]>`
|
|
517
|
-
${taskSelect}
|
|
732
|
+
${taskSelect()}
|
|
518
733
|
WHERE task.sponsor_user_id = ${input.userId}::uuid AND task.short_id = ${input.taskId}
|
|
519
734
|
FOR UPDATE OF task
|
|
520
735
|
`;
|
|
@@ -555,7 +770,7 @@ export const aiChatTasks = {
|
|
|
555
770
|
delete: async (input: { userId: string; taskId: string }): Promise<boolean> => {
|
|
556
771
|
return sql.begin(async (tx) => {
|
|
557
772
|
const rows = await tx<TaskRow[]>`
|
|
558
|
-
${taskSelect}
|
|
773
|
+
${taskSelect()}
|
|
559
774
|
WHERE task.sponsor_user_id = ${input.userId}::uuid AND task.short_id = ${input.taskId}
|
|
560
775
|
FOR UPDATE OF task
|
|
561
776
|
`;
|
|
@@ -599,7 +814,7 @@ export const aiChatTasks = {
|
|
|
599
814
|
|
|
600
815
|
listActiveCron: async (): Promise<AiChatTask[]> => {
|
|
601
816
|
const rows = await sql<TaskRow[]>`
|
|
602
|
-
${taskSelect}
|
|
817
|
+
${taskSelect()}
|
|
603
818
|
WHERE task.state = 'active' AND task.schedule_kind = 'cron'
|
|
604
819
|
`;
|
|
605
820
|
const tasks: AiChatTask[] = [];
|
|
@@ -624,8 +839,14 @@ export const aiChatTasks = {
|
|
|
624
839
|
expectedRevision?: number;
|
|
625
840
|
}): Promise<AiChatTaskOccurrence | null> => {
|
|
626
841
|
return sql.begin(async (tx) => {
|
|
627
|
-
const [current] = await tx<TaskRow[]>`${taskSelect} WHERE task.id = ${input.taskId}::uuid FOR UPDATE OF task`;
|
|
842
|
+
const [current] = await tx<TaskRow[]>`${taskSelect()} WHERE task.id = ${input.taskId}::uuid FOR UPDATE OF task`;
|
|
628
843
|
if (!current || !(await prepareTaskForExecution(current, tx))) return null;
|
|
844
|
+
// One occurrence per schedule; timer ticks during a run do not accumulate.
|
|
845
|
+
const [inFlight] = await tx<
|
|
846
|
+
OccurrenceRow[]
|
|
847
|
+
>`SELECT * FROM ai.chat_task_occurrences WHERE task_id = ${input.taskId}::uuid AND state IN ('queued','running') LIMIT 1`;
|
|
848
|
+
if (inFlight) return inFlight.request_key === input.requestKey ? toOccurrence(inFlight) : null;
|
|
849
|
+
|
|
629
850
|
const rows = await withAiShortIdForDb(
|
|
630
851
|
tx,
|
|
631
852
|
"ai_chat_task_occurrences_short_id_unique",
|
|
@@ -686,20 +907,20 @@ export const aiChatTasks = {
|
|
|
686
907
|
occurrence.state, occurrence.turn_id, occurrence.error, occurrence.created_at, occurrence.started_at, occurrence.completed_at,
|
|
687
908
|
task.id AS task_row_id, task.short_id AS task_short_id, task.conversation_id, conversation.short_id AS conversation_short_id,
|
|
688
909
|
conversation.title AS conversation_title, task.sponsor_user_id, task.mandate_id, task.mandate_revision,
|
|
910
|
+
(SELECT policy FROM auth.mandates WHERE id = task.mandate_id) AS mandate_policy,
|
|
689
911
|
task.prompt, task.schedule_kind, task.run_at, task.cron,
|
|
690
912
|
task.timezone, task.state AS task_state, task.revision, task.last_error, task.created_at AS task_created_at, task.updated_at
|
|
691
913
|
FROM (
|
|
692
914
|
SELECT candidate.*,
|
|
693
|
-
row_number() OVER (PARTITION BY task.
|
|
915
|
+
row_number() OVER (PARTITION BY task.id ORDER BY candidate.created_at, candidate.id) AS conversation_rank
|
|
694
916
|
FROM ai.chat_task_occurrences candidate
|
|
695
917
|
JOIN ai.chat_tasks task ON task.id = candidate.task_id
|
|
696
918
|
JOIN ai.conversations conversation ON conversation.id = task.conversation_id
|
|
697
919
|
WHERE candidate.state = 'queued' AND task.state = 'active'
|
|
698
920
|
AND task.mandate_id IS NOT NULL AND task.mandate_revision IS NOT NULL
|
|
699
921
|
AND NOT EXISTS (
|
|
700
|
-
SELECT 1 FROM ai.
|
|
701
|
-
WHERE
|
|
702
|
-
AND turn.status IN ('queued', 'running', 'waiting_for_action')
|
|
922
|
+
SELECT 1 FROM ai.chat_task_occurrences running
|
|
923
|
+
WHERE running.task_id = task.id AND running.state = 'running'
|
|
703
924
|
)
|
|
704
925
|
) occurrence
|
|
705
926
|
JOIN ai.chat_tasks task ON task.id = occurrence.task_id
|
|
@@ -728,7 +949,7 @@ export const aiChatTasks = {
|
|
|
728
949
|
const occurrence = occurrences[0];
|
|
729
950
|
if (!occurrence) return null;
|
|
730
951
|
const tasks = await tx<TaskRow[]>`
|
|
731
|
-
${taskSelect}
|
|
952
|
+
${taskSelect()}
|
|
732
953
|
WHERE task.id = ${occurrence.task_id}::uuid AND task.state = 'active'
|
|
733
954
|
LIMIT 1
|
|
734
955
|
FOR UPDATE OF task
|
|
@@ -759,6 +980,7 @@ export const aiChatTasks = {
|
|
|
759
980
|
RETURNING occurrence.task_id, occurrence.task_revision
|
|
760
981
|
`;
|
|
761
982
|
if (rows[0]) {
|
|
983
|
+
await deliverOccurrenceResult(tx, task, input.occurrenceId, input.error);
|
|
762
984
|
const revision = await pauseTerminalTaskMandate(task, tx);
|
|
763
985
|
await tx`
|
|
764
986
|
UPDATE ai.chat_tasks
|
|
@@ -821,15 +1043,26 @@ export const aiChatTasks = {
|
|
|
821
1043
|
if (occurrence.archived_at) return { delivered: false as const, reason: "not_found" as const };
|
|
822
1044
|
if (Number(occurrence.task_revision) !== input.expectedRevision) return { delivered: false as const, reason: "stale" as const };
|
|
823
1045
|
const active = await tx<{ id: string }[]>`
|
|
824
|
-
SELECT id FROM ai.
|
|
825
|
-
WHERE
|
|
826
|
-
AND status IN ('queued', 'running', 'waiting_for_action')
|
|
1046
|
+
SELECT id FROM ai.chat_task_occurrences
|
|
1047
|
+
WHERE task_id = ${task.id}::uuid AND state = 'running'
|
|
827
1048
|
LIMIT 1
|
|
828
1049
|
`;
|
|
829
1050
|
if (active[0]) return { delivered: false as const, reason: "busy" as const };
|
|
830
1051
|
const mandate = requireTaskMandate(task);
|
|
1052
|
+
const contextRows = await tx<{ seq: number; kind: "message" | "summary"; message: Message }[]>`
|
|
1053
|
+
SELECT message.seq, message.kind, message.message FROM ai.messages message
|
|
1054
|
+
WHERE message.conversation_id = ${occurrence.conversation_id}::uuid AND message.compacted_at IS NULL
|
|
1055
|
+
AND NOT EXISTS (SELECT 1 FROM ai.turns pending WHERE pending.id::text = message.loop_id
|
|
1056
|
+
AND pending.status IN ('queued','running','waiting_for_action'))
|
|
1057
|
+
ORDER BY message.seq
|
|
1058
|
+
`;
|
|
831
1059
|
const runConfig: AiChatTurnRunConfig = {
|
|
832
1060
|
...input.runConfig,
|
|
1061
|
+
background: {
|
|
1062
|
+
taskId: task.short_id,
|
|
1063
|
+
occurrenceId: occurrence.short_id,
|
|
1064
|
+
context: contextRows.map((row, index) => ({ seq: index + 1, kind: row.kind, message: row.message })),
|
|
1065
|
+
},
|
|
833
1066
|
mandate: { id: mandate.mandate_id, revision: Number(mandate.mandate_revision) },
|
|
834
1067
|
};
|
|
835
1068
|
const turn = await withAiShortIdForDb(
|
|
@@ -854,16 +1087,16 @@ export const aiChatTasks = {
|
|
|
854
1087
|
tx,
|
|
855
1088
|
"idx_ai_messages_conversation_short_id",
|
|
856
1089
|
(attempt, shortId) => attempt`
|
|
857
|
-
INSERT INTO ai.
|
|
1090
|
+
INSERT INTO ai.task_messages (short_id, conversation_id, seq, kind, role, message, search_text, loop_id, meta)
|
|
858
1091
|
VALUES (
|
|
859
1092
|
${shortId}, ${occurrence.conversation_id}::uuid,
|
|
860
|
-
|
|
1093
|
+
1,
|
|
861
1094
|
'message', 'user', (${JSON.stringify(input.userMessage)}::text)::jsonb, ${input.runConfig.input}, ${turnId}::uuid,
|
|
862
1095
|
(${JSON.stringify(messageMeta)}::text)::jsonb
|
|
863
1096
|
)
|
|
864
1097
|
`,
|
|
865
1098
|
);
|
|
866
|
-
|
|
1099
|
+
|
|
867
1100
|
await tx`
|
|
868
1101
|
UPDATE ai.chat_task_occurrences
|
|
869
1102
|
SET state = 'running', turn_id = ${turnId}::uuid, task_revision = ${occurrence.task_revision}::bigint, started_at = now()
|
|
@@ -908,6 +1141,27 @@ export const aiChatTasks = {
|
|
|
908
1141
|
`;
|
|
909
1142
|
const row = rows[0];
|
|
910
1143
|
if (!row) return null;
|
|
1144
|
+
const responses = await tx<{ message: Message }[]>`
|
|
1145
|
+
SELECT message FROM ai.task_messages WHERE loop_id = ${input.turnId} AND role = 'assistant' ORDER BY seq DESC
|
|
1146
|
+
`;
|
|
1147
|
+
const resultText =
|
|
1148
|
+
responses
|
|
1149
|
+
.map((response) => {
|
|
1150
|
+
const message = response.message;
|
|
1151
|
+
if (message.role !== "assistant" || message.content.some((part) => part.type === "tool_call")) return "";
|
|
1152
|
+
return message.content
|
|
1153
|
+
.filter((part) => typeof part === "string" || part.type === "text")
|
|
1154
|
+
.map((part) => (typeof part === "string" ? part : part.type === "text" ? part.text : ""))
|
|
1155
|
+
.join("\n")
|
|
1156
|
+
.trim();
|
|
1157
|
+
})
|
|
1158
|
+
.find(Boolean) ||
|
|
1159
|
+
input.error ||
|
|
1160
|
+
row.turn_error ||
|
|
1161
|
+
(input.status === "completed"
|
|
1162
|
+
? "Background task completed."
|
|
1163
|
+
: "Background task could not finish. Please review this run in the chat.");
|
|
1164
|
+
await deliverOccurrenceResult(tx, task, row.occurrence_id, resultText);
|
|
911
1165
|
const unchangedSinceStart = Number(row.task_revision) === Number(row.current_revision);
|
|
912
1166
|
const currentOnceSlot = row.run_at !== null && iso(row.run_at) === iso(row.scheduled_for);
|
|
913
1167
|
if (
|