@k2b/cloud 0.22.0 → 0.24.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 +4 -3
- package/scripts/README.md +1 -1
- package/scripts/build-canvas-workers.ts +58 -0
- package/scripts/build.ts +4 -5
- package/src/_internal/canvas-worker.ts +24 -0
- package/src/_internal/capabilities.ts +230 -75
- package/src/_internal/define-app.ts +90 -7
- package/src/_internal/fixtures/filesv2-manifest-cloud-v0.29.0.json +887 -0
- package/src/_internal/page-responses.ts +20 -4
- package/src/_internal/registry-validation.ts +6 -0
- package/src/_internal/registry.ts +78 -3
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/static-assets.ts +18 -3
- package/src/access/ResourceApiKeys.tsx +30 -30
- package/src/ai/capability-execution.ts +5 -0
- package/src/ai/chat/blocks.tsx +2 -1
- package/src/ai/chat/messages.ts +6 -0
- package/src/ai/client/controller.ts +80 -21
- package/src/ai/client/transport.ts +47 -3
- package/src/ai/code-mode-skill.ts +2 -2
- package/src/ai/index.ts +0 -7
- package/src/ai/live-events.ts +1 -262
- package/src/ai/live.ts +31 -2
- package/src/ai/migrate.ts +35 -16
- package/src/ai/pdf-render.ts +2 -22
- package/src/ai/routes.ts +5 -4
- package/src/ai/runtime.ts +1 -6
- package/src/ai/solid.ts +0 -5
- package/src/ai/stream.ts +89 -65
- package/src/api/app-approval.ts +5 -1
- package/src/api/help.ts +2 -2
- package/src/api/index.ts +3 -0
- package/src/api/me.ts +19 -5
- package/src/api/pwa-phone.ts +168 -0
- package/src/api/pwa.ts +179 -0
- package/src/api/search/schemas.ts +34 -0
- package/src/api/search.ts +118 -50
- package/src/browser/CloudResourceSearch.browser-harness.tsx +11 -0
- package/src/browser/CloudResourceSearch.tsx +208 -80
- package/src/browser/app-session.ts +69 -0
- package/src/browser/live-connection.ts +211 -0
- package/src/browser/live-websocket.ts +31 -11
- package/src/browser/live.ts +2 -0
- package/src/browser/resource-search-messages.ts +15 -2
- package/src/browser/search-stream.ts +151 -0
- package/src/contracts/app.ts +10 -0
- package/src/contracts/capabilities.ts +89 -24
- package/src/contracts/capability-compatibility.ts +28 -14
- package/src/contracts/file-provider.ts +158 -0
- package/src/contracts/index.ts +2 -0
- package/src/contracts/pwa-paths.ts +6 -0
- package/src/contracts/pwa.ts +156 -0
- package/src/contracts/registry.ts +7 -0
- package/src/contracts/shared.ts +7 -1
- package/src/events/index.ts +2 -0
- package/src/events/live-engine.ts +830 -0
- package/src/events/live-protocol.ts +31 -0
- package/src/events/live.ts +298 -0
- package/src/server/actor.ts +7 -0
- package/src/server/api-client.ts +23 -2
- package/src/server/help.ts +3 -3
- package/src/server/index.ts +3 -1
- package/src/server/middleware/auth.ts +80 -8
- package/src/server/middleware/openapi.ts +2 -1
- package/src/server/services/access.ts +49 -0
- package/src/server/services/index.ts +1 -0
- package/src/services/app-approval.ts +6 -25
- package/src/services/audit/index.ts +2 -0
- package/src/services/branding/app-icon-source.ts +40 -0
- package/src/services/branding/app-icons.ts +112 -0
- package/src/services/branding/icon-render-worker.ts +69 -0
- package/src/services/branding/icon-svg.ts +22 -0
- package/src/services/identity/invocation-actor.ts +8 -5
- package/src/services/identity/invocation-authority.ts +1 -0
- package/src/services/identity/invocation-token.ts +6 -0
- package/src/services/index.ts +1 -0
- package/src/services/outbox.ts +114 -80
- package/src/services/pairing-secret.ts +16 -0
- package/src/services/pdf/markdown.ts +22 -2
- package/src/services/pwa-devices.ts +640 -0
- package/src/services/session/index.ts +212 -57
- package/src/services/session/recent.ts +42 -0
- package/src/services/session/user.ts +15 -5
- package/src/shared/index.ts +1 -0
- package/src/{browser → shared}/locale-preference.ts +1 -1
- package/src/shared/markdown/formula.ts +122 -26
- package/src/shared/markdown/index.ts +27 -23
- package/src/ssr/AppLaunchpad.island.tsx +8 -1
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/GlobalAnnouncements.island.tsx +1 -1
- package/src/ssr/Layout.tsx +10 -12
- package/src/ssr/LayoutHeader.tsx +14 -2
- package/src/ssr/LayoutHelp.tsx +2 -4
- package/src/ssr/LayoutRail.tsx +13 -4
- package/src/ssr/MinimalLayoutPreferences.island.tsx +1 -1
- package/src/ssr/MobileProfileActions.tsx +19 -13
- package/src/ssr/PageError.tsx +23 -1
- package/src/ssr/ProfilePreferences.island.tsx +9 -1
- package/src/ssr/PwaLayout.tsx +70 -0
- package/src/ssr/PwaRuntime.island.tsx +105 -0
- package/src/ssr/TimezoneCookie.island.tsx +7 -1
- package/src/ssr/app-navigation.ts +25 -10
- package/src/ssr/index.ts +8 -1
- package/src/ssr/layout-context.ts +1 -1
- package/src/ssr/preference-controller.ts +2 -2
- package/src/ssr/profile-actions.ts +3 -1
- package/src/ssr/profile-preferences-messages.ts +2 -0
- package/src/ssr/pwa-messages.ts +24 -0
- package/src/styles/global.css +6 -0
- package/src/styles/resource-search.css +19 -0
- package/src/styles/utilities-feedback.css +2 -2
- package/scripts/build-pdf-renderer.ts +0 -39
- package/src/ai/client/live-connection.ts +0 -141
- package/src/ai/live-messages.ts +0 -45
- package/src/ai/live-outbox.ts +0 -102
- package/src/ai/live-routes.ts +0 -412
- package/src/shared/markdown/extensions/info-blocks.ts +0 -108
|
@@ -4,7 +4,7 @@ import type { AiSkillTemplate } from "./skills";
|
|
|
4
4
|
|
|
5
5
|
export const ASSISTANT_CODE_MODE_SKILL = {
|
|
6
6
|
"key": "assistant:code-mode",
|
|
7
|
-
"version":
|
|
7
|
+
"version": 57,
|
|
8
8
|
"name": "assistant-code-mode",
|
|
9
9
|
"description": "Inspect and transform unfamiliar data, analyze files, compare results across Cloud apps, or build and improve interactive and agent-only Apps in Assistant Studio. Use for quick code experiments, data analysis, file generation, resource SQL queries and combining discovered Cloud capabilities. For plain arithmetic or date offsets, answer directly or use calculate.",
|
|
10
10
|
"instructions": "# Assistant code mode\n\nChoose the smallest useful result: one-off answer, exported file, or reusable\nStudio App. Apps may expose agent actions, a display-only dashboard, or both.\nPersistence is optional. One-off scripts stay in their chat and cannot be shared. Reuse an\nexisting Cloud feature when it fits. For a\nquick reading of an uploaded PDF or Office document, `read_file` can return\nMarkdown; use code for exact cells, calculations, original PDF text or positions.\n\n## Start from the contract\n\nLoad the needed `code_*` tools individually through `load_tools` and read their\ninput schemas. They are Assistant tools, not capabilities or functions inside\ncode. Discover other Cloud operations before using `capabilities.run`.\n\nRuntime namespaces are globals: no imports or package installation are needed.\nOnly relative imports of the resource's own source files are supported. There is\nno DOM or native network access. Before using a namespace, read its reference\nbelow for signatures, options and return values. Do not invent methods or infer\nan API from a familiar library. For discovered Cloud capabilities and external\nAPIs, obtain their actual contracts separately.\n\nInspect supplied data before joining, filtering or calculating: column names,\ntypes, units, date ranges and missing values. Ask only for decisions or inputs\nthat cannot be established from available evidence. For several real steps,\nkeep a short `todo_write` plan and update it as work changes; skip ceremony for a\nsmall experiment. A failed experiment should change the next hypothesis.\n\n## First file script\n\nPass exact current-chat manifest paths as `code_run.inputPaths`, and this entry\nas `code_run.code` for a small CSV:\n\n```js\nexport default async () => {\n const [input] = await files.list();\n if (!input) throw new Error(\"Select a CSV input.\");\n const rows = await sheet.fromCsv(await files.read(input.name));\n return { rows: rows.length, columns: Object.keys(rows[0] ?? {}), sample: rows.slice(0, 3) };\n};\n```\n\n`input.name` is the full path, such as `/sales.csv`; pass it unchanged to\n`files.read`, which returns a `File`. CSV rows are objects keyed by headers:\n`rows[0]` is already data. Do not drop it. For older Excel CSVs, use\n`sheet.fromCsv(file, {encoding:\"windows-1252\"})`. Inspect actual headings first.\nFor a tiny experiment without files, `export default () => ({answer:42})` suffices.\nEach run has fresh variables. No saved resource or UI is required.\n\n## Reference routing\n\nRead only the rows relevant to the task. Each link describes its own complete\nsupported surface; links within references add related workflows when needed.\n\n| Task / API | Read |\n| --- | --- |\n| Source entry, input/output files, pickers, CSV, IDs | [Runtime and files](/skills/assistant-code-mode/references/runtime.md) |\n| Inspect PDF pages, read PDF text/positions or XLSX/ODS cells, write ODS | [Documents](/skills/assistant-code-mode/references/documents.md) |\n| Generate a PDF, save one in Files, embed attachments, combine invoice HTML and XML | [PDF generation](/skills/assistant-code-mode/references/pdf.md) |\n| Exact amounts, taxes, allocation, localized money | [Money](/skills/assistant-code-mode/references/money.md) |\n| Export DATEV bookings or SEPA transfers | [DATEV and SEPA](/skills/assistant-code-mode/references/finance.md) |\n| Parse a CAMT bank report | [Bank reports](/skills/assistant-code-mode/references/camt.md) |\n| Calculate, create or read electronic invoices/XML/PDF attachments | [Electronic invoices](/skills/assistant-code-mode/references/einvoice.md) |\n| Controls, layouts and dialogs | [UI and dialogs](/skills/assistant-code-mode/references/ui.md), [Analytics UI](/skills/assistant-code-mode/references/analytics.md) |\n| Chart types, series and axes | [Charts](/skills/assistant-code-mode/references/charts.md) |\n| Long processing, progress, cancellation | [Background work](/skills/assistant-code-mode/references/work.md) |\n| Persist JSON or files locally/shared | [Storage](/skills/assistant-code-mode/references/storage.md) |\n| Copy files between stores; list and download Filesv2 beside Grids documents | [File transfers](/skills/assistant-code-mode/references/files.md) |\n| Resource SQL, schema, row CRUD, imports | [Database](/skills/assistant-code-mode/references/database.md) |\n| Generate text, classify data or extract structured fields | [AI calculations](/skills/assistant-code-mode/references/ai.md) |\n| Discovered Cloud queries/actions | [Capability calls](/skills/assistant-code-mode/references/capabilities.md) |\n| External HTTPS and personal secrets | [HTTP and secrets](/skills/assistant-code-mode/references/http.md) |\n| Call a published App action; declare handlers | [App actions](/skills/assistant-code-mode/references/app-actions.md) |\n| Reuse work across chats, create or edit an App | [Source workflow](/skills/assistant-code-mode/references/source-workflow.md) |\n| Publish, restore, copy | [Publishing](/skills/assistant-code-mode/references/publishing.md) |\n| Find recipients or change App/Skill sharing | [Access](/skills/assistant-code-mode/references/access.md) |\n| Inspect, export, clear server data, or delete an App | [Management](/skills/assistant-code-mode/references/management.md) |\n| Execute, inspect, interact, export, stop, diagnose errors | [Run and debug](/skills/assistant-code-mode/references/debugging.md) |\n| Unfamiliar inputs or cross-app investigation | [Investigation](/skills/assistant-code-mode/references/investigation.md) |\n| Complete app starters | [Examples](/skills/assistant-code-mode/references/examples.md) |\n\nFor a new app, read Source workflow and the closest complete example before\nwriting source, plus only the API references it uses. For analytical reports or\ndashboards, also load `assistant-data-analysis` for metrics and source validation.\n\n## Choose the delivery\n\nFor a one-off chart, calculator, or interactive analysis in this conversation,\nuse `code_run({code,inputPaths})`, test the controls, then\n`code_present({runId,title})`. Read [Chat visualizations](/skills/assistant-code-mode/references/chat.md).\nA successful run is visible to the agent only; present it before saying the\nuser can see it. No saved App or chat file is necessary.\n\nUse a Studio App when the user needs an independently accessible, reusable\napplication. Use `files.save`, `code_export`, and `present` when the requested\nresult is a file. These are separate delivery choices.\n\n## Verify and deliver\n\nRun the actual source (the saved revision for Apps) and test relevant controls with IDs returned by\n`code_run`/`code_interact`, including invalid inputs and picker fixtures. Creating,\ncompiling or saving source does not verify behavior. If `work.status` is\n`running`, wait with `code_inspect({runId,waitMs:30000})`; do not restart the job.\nInspect only when the returned snapshot needs more detail. Errors and\n`outputTruncated` are not successful complete results.\n\nFor a CSV, call `await files.save(sheet.toCsv(rows), \"result.csv\")` inside code;\nfor a spreadsheet, `await files.save(await sheet.toOds(sheets), \"result.ods\")`.\nThen call the **tool** `code_export` with the returned `runId` and captured file\nname, and `present` its returned chat path. `files.save` returns no path.\nReuse exported data via its path/version rather than retyping truncated output.\nReconcile row counts, exclusions and totals before reporting findings.\n\nOpen GUI apps with `code_open`. Saving or testing does\nnot replace a user's already-running app. Stop runs no longer needed that retain\nUI, jobs or output files. Never claim an unexecuted result is verified.\n\nAgent execution runs independently of the user's tab. Agent local storage is\ntemporary; shared storage, database writes and external actions are real, even\nin tests. Cancellation and source restore do not undo them. Apps select local\nfiles explicitly; they never gain implicit access to chat attachments. Use\n`code_secret` for credentials, never chat or app controls. Honor normal access\nand approval decisions; availability is not authorization for unrelated actions.",
|
|
@@ -64,7 +64,7 @@ export const ASSISTANT_CODE_MODE_SKILL = {
|
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
"path": "references/files.md",
|
|
67
|
-
"content": "# Explicit file references and transfers\n\nFiles belong to a chat, Project, or App shared store. A location is\n`{scope:\"chat\"|\"project\"|\"app\",id,path}`. A reference adds an opaque string\n`version`. The current chat ID is supplied as `Chat:` in the platform context;\nuse it for `scope:\"chat\"` rather than guessing an ID. Reuse returned locations and references exactly; access to a location\nnever grants access to its whole store or to another resource.\n\nLoad only `code_files`, `code_file_stat`, and `code_file_copy` as needed.\n\n1. `code_files({scope,id,after?:string,limit?:number})` returns `container`,\n `items:[{location,path,size,mediaType}]`, and `nextAfter`. Limit defaults to\n 100, maximum 1,000. Continue with `nextAfter` until null.\n2. `code_file_stat({file:location})` returns `{exists:false}` or\n `{exists:true,reference,size,mediaType}`. It does not print file bytes.\n3. `code_file_copy({source:reference,destination:location,expectedVersion})`\n copies bytes on the server. For a new destination use `expectedVersion:null`;\n replacing a file requires its exact current version from `code_file_stat`.\n The result contains the destination `reference,size,mediaType`.\n\nEvery copy receives fresh user review with the exact source, destination, and\noverwrite scope. App and Project files may be readable by other authorized\nusers; copying a private chat attachment there is an explicit disclosure.\nRejection does not copy anything. Source versions, destination versions, current\npermissions, and the destination's byte limits are checked again during execution.\nAfter a conflict, inspect current state and prepare a new review before retrying.\n\nApp file reads and writes require Use, not Manage. Project files require Read\nfor sources and Write for destinations. Chat files require ownership. Transfers\nwork without a running UI and do not mount other chat attachments implicitly.\n\nFor example, inspect an invoice attachment, copy its reference into the target\nApp's shared file store, then call the App's published `importFiles` action with\nthe destination key expected by its discovered input schema. Do not pass a chat\npath to an App and assume it can read it. Action handlers see only their explicit\ninput and authorized App data. Use [App actions](app-actions.md) for discovery.\n\nFor small UTF-8 files that should become App source, use\n`code_write({id,expectedRevision,files:[{path:\"data.json\",fromFile:reference}]})`.\nThis is a reviewed import with the same source references, not a chat-only\nspecial case. Source file/bundle limits still apply; keep larger or private\nruntime data in shared files or the database instead of embedding it in source.\n\nKnown rejections return `CONFLICT` for an occupied or changed destination and\n`STORAGE_FULL` for a destination byte limit. No destination bytes were written.\nChoose another path or reduce the file size, then prepare a new review.\n\n\n## List Filesv2 files beside Grids documents\n\nDiscover the installed contracts first. Call `filesv2.bases.list`, then\n`filesv2.entry.list` for a folder or `filesv2.entry.search-in-base` for names\nbelow a known path. Keep the filters unchanged and follow `data.next` as\n`after` until null, even after an empty page. Each `data.items` entry includes\nits own `{type:\"filesv2.entry\",id}` ref and file metadata. Keep that ref in the\nStudio list, alongside the `grids.document` refs from `grids.document.list`.\nUse both `type` and `id` as the identity; dispatch each type to its own\noperations. Grids uses its own `page` cursor, not Filesv2's `data.next`.\n\nFilesv2 refs
|
|
67
|
+
"content": "# Explicit file references and transfers\n\nFiles belong to a chat, Project, or App shared store. A location is\n`{scope:\"chat\"|\"project\"|\"app\",id,path}`. A reference adds an opaque string\n`version`. The current chat ID is supplied as `Chat:` in the platform context;\nuse it for `scope:\"chat\"` rather than guessing an ID. Reuse returned locations and references exactly; access to a location\nnever grants access to its whole store or to another resource.\n\nLoad only `code_files`, `code_file_stat`, and `code_file_copy` as needed.\n\n1. `code_files({scope,id,after?:string,limit?:number})` returns `container`,\n `items:[{location,path,size,mediaType}]`, and `nextAfter`. Limit defaults to\n 100, maximum 1,000. Continue with `nextAfter` until null.\n2. `code_file_stat({file:location})` returns `{exists:false}` or\n `{exists:true,reference,size,mediaType}`. It does not print file bytes.\n3. `code_file_copy({source:reference,destination:location,expectedVersion})`\n copies bytes on the server. For a new destination use `expectedVersion:null`;\n replacing a file requires its exact current version from `code_file_stat`.\n The result contains the destination `reference,size,mediaType`.\n\nEvery copy receives fresh user review with the exact source, destination, and\noverwrite scope. App and Project files may be readable by other authorized\nusers; copying a private chat attachment there is an explicit disclosure.\nRejection does not copy anything. Source versions, destination versions, current\npermissions, and the destination's byte limits are checked again during execution.\nAfter a conflict, inspect current state and prepare a new review before retrying.\n\nApp file reads and writes require Use, not Manage. Project files require Read\nfor sources and Write for destinations. Chat files require ownership. Transfers\nwork without a running UI and do not mount other chat attachments implicitly.\n\nFor example, inspect an invoice attachment, copy its reference into the target\nApp's shared file store, then call the App's published `importFiles` action with\nthe destination key expected by its discovered input schema. Do not pass a chat\npath to an App and assume it can read it. Action handlers see only their explicit\ninput and authorized App data. Use [App actions](app-actions.md) for discovery.\n\nFor small UTF-8 files that should become App source, use\n`code_write({id,expectedRevision,files:[{path:\"data.json\",fromFile:reference}]})`.\nThis is a reviewed import with the same source references, not a chat-only\nspecial case. Source file/bundle limits still apply; keep larger or private\nruntime data in shared files or the database instead of embedding it in source.\n\nKnown rejections return `CONFLICT` for an occupied or changed destination and\n`STORAGE_FULL` for a destination byte limit. No destination bytes were written.\nChoose another path or reduce the file size, then prepare a new review.\n\n\n## List Filesv2 files beside Grids documents\n\nDiscover the installed contracts first. Call `filesv2.bases.list`, then\n`filesv2.entry.list` for a folder or `filesv2.entry.search-in-base` for names\nbelow a known path. Keep the filters unchanged and follow `data.next` as\n`after` until null, even after an empty page. Each `data.items` entry includes\nits own `{type:\"filesv2.entry\",id}` ref and file metadata. Keep that ref in the\nStudio list, alongside the `grids.document` refs from `grids.document.list`.\nUse both `type` and `id` as the identity; dispatch each type to its own\noperations. Grids uses its own `page` cursor, not Filesv2's `data.next`.\n\nFilesv2 refs are opaque; they do not grant access or pin a content version.\nOn storage with stable file IDs (`n:…` refs), a ref keeps naming the same file\nacross rename and move; elsewhere it names a base and path, so moving or\nrenaming changes it. Store refs exactly as returned and never build one from a\npath. Different refs can name the same file, because older path refs stay\nvalid. A 404 means not found or no longer visible; 503 is an outage, not a\ndeletion. Refresh metadata when needed. Never construct storage URLs or turn\nfiles into public shares for this flow.\n\nOnly on a user's download request, call `filesv2.content.download` with the\nselected Filesv2 ref's exact `id`. Its `data` is `{url,method:\"GET\",expires}`.\nOffer that returned URL unchanged to the requesting user; it is a private\nbearer credential, not a stable resource link. It expires after 60 seconds;\nuse the returned `expires` timestamp and request a fresh lease when needed.\nDo not prefetch leases for list rows, persist them in App/shared data, or\ninclude them in logs. Do not send Cloud cookies or authorization headers to\nthe storage host. Grids documents keep their authenticated download path from\ntheir canonical reader; never send a `grids.document` ID to Filesv2.\n\nEach lease request checks the current user's storage and Unix permissions.\nA 403 means access is denied; a 404 means the ref or file is missing or the\nbase is no longer visible. Refresh the list and do not bypass the denial.\n`not_file` (400) means a folder was selected. `identity_changed` (409) requires\nrefreshing access/identity state before trying again. Storage unavailability is an\nerror, not an empty list. If a lease expires or a transfer fails, discard the\nURL and request a fresh lease through the same capability; if that is denied,\nstop. Revoking Cloud access prevents new leases; an already issued bearer\nlease can remain usable until expiry, subject to storage checks.\n\nFor analysis inside code, use `filesv2.content.read` and\n`capabilities.streams.read` instead of fetching a bearer URL. See\n[Capability calls](capabilities.md) for binary budgets and consent rules.\n"
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"path": "references/finance.md",
|
package/src/ai/index.ts
CHANGED
|
@@ -128,17 +128,10 @@ export {
|
|
|
128
128
|
export { AI_IMAGE_INPUT_MAX_BYTES, AI_TURN_ATTACHMENT_MAX_ITEMS, AI_TURN_IMAGE_MAX_TOTAL_BYTES } from "./limits";
|
|
129
129
|
export {
|
|
130
130
|
AI_INVALIDATION_DOMAINS,
|
|
131
|
-
AI_LIVE_WS_TYPE,
|
|
132
131
|
type AiInvalidation,
|
|
133
132
|
type AiInvalidationDomain,
|
|
134
133
|
AiInvalidationDomainSchema,
|
|
135
134
|
AiInvalidationSchema,
|
|
136
|
-
type AiLiveClientMessage,
|
|
137
|
-
AiLiveClientMessageSchema,
|
|
138
|
-
AiLiveCursorSchema,
|
|
139
|
-
type AiLiveServerMessage,
|
|
140
|
-
AiLiveServerMessageSchema,
|
|
141
|
-
parseAiLiveServerMessage,
|
|
142
135
|
} from "./live-events";
|
|
143
136
|
export { AI_ENRICH_CRON_SETTING_KEY, AI_MEMORY_LEARNING_CRON_SETTING_KEY, aiMaintenanceJobs } from "./maintenance";
|
|
144
137
|
export {
|
package/src/ai/live-events.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import type { AiStreamEvent } from "./protocol";
|
|
3
2
|
|
|
4
3
|
export const AI_INVALIDATION_DOMAINS = [
|
|
5
4
|
"conversation-list",
|
|
@@ -17,8 +16,8 @@ export const AiInvalidationDomainSchema = z.enum(AI_INVALIDATION_DOMAINS);
|
|
|
17
16
|
export type AiInvalidationDomain = z.infer<typeof AiInvalidationDomainSchema>;
|
|
18
17
|
|
|
19
18
|
export const AiResourceIdSchema = z.string().regex(/^[0-9A-Za-z]{6}$/);
|
|
20
|
-
export const AiLiveCursorSchema = z.string().regex(/^s6t\.[a-zA-Z0-9_-]+\.\d+$/);
|
|
21
19
|
|
|
20
|
+
/** The data of one AI live update on the `user` channel of `/api/ai/live`: which views of the user's AI data changed. */
|
|
22
21
|
export const AiInvalidationSchema = z
|
|
23
22
|
.object({
|
|
24
23
|
type: z.literal("ai.invalidated"),
|
|
@@ -31,263 +30,3 @@ export const AiInvalidationSchema = z
|
|
|
31
30
|
.strict();
|
|
32
31
|
|
|
33
32
|
export type AiInvalidation = z.infer<typeof AiInvalidationSchema>;
|
|
34
|
-
|
|
35
|
-
export const AI_LIVE_WS_TYPE = {
|
|
36
|
-
subscribe: "ai.live.subscribe",
|
|
37
|
-
ready: "ai.live.ready",
|
|
38
|
-
event: "ai.live.event",
|
|
39
|
-
scopeChanged: "ai.live.scope_changed",
|
|
40
|
-
revoked: "ai.live.revoked",
|
|
41
|
-
error: "ai.live.error",
|
|
42
|
-
turnSubscribe: "ai.turn.subscribe",
|
|
43
|
-
turnUnsubscribe: "ai.turn.unsubscribe",
|
|
44
|
-
turnEvent: "ai.turn.event",
|
|
45
|
-
turnError: "ai.turn.error",
|
|
46
|
-
} as const;
|
|
47
|
-
|
|
48
|
-
const AiLiveSubscribeMessageSchema = z
|
|
49
|
-
.object({
|
|
50
|
-
type: z.literal(AI_LIVE_WS_TYPE.subscribe),
|
|
51
|
-
payload: z.object({ fromCursor: AiLiveCursorSchema.nullable(), recover: z.boolean() }).strict(),
|
|
52
|
-
})
|
|
53
|
-
.strict();
|
|
54
|
-
|
|
55
|
-
const AiTurnSubscribeMessageSchema = z
|
|
56
|
-
.object({
|
|
57
|
-
type: z.literal(AI_LIVE_WS_TYPE.turnSubscribe),
|
|
58
|
-
payload: z.object({ conversationId: AiResourceIdSchema }).strict(),
|
|
59
|
-
})
|
|
60
|
-
.strict();
|
|
61
|
-
|
|
62
|
-
const AiTurnUnsubscribeMessageSchema = z
|
|
63
|
-
.object({
|
|
64
|
-
type: z.literal(AI_LIVE_WS_TYPE.turnUnsubscribe),
|
|
65
|
-
payload: z.object({ conversationId: AiResourceIdSchema }).strict(),
|
|
66
|
-
})
|
|
67
|
-
.strict();
|
|
68
|
-
|
|
69
|
-
export const AiLiveClientMessageSchema = z.discriminatedUnion("type", [
|
|
70
|
-
AiLiveSubscribeMessageSchema,
|
|
71
|
-
AiTurnSubscribeMessageSchema,
|
|
72
|
-
AiTurnUnsubscribeMessageSchema,
|
|
73
|
-
]);
|
|
74
|
-
export type AiLiveClientMessage = z.infer<typeof AiLiveClientMessageSchema>;
|
|
75
|
-
|
|
76
|
-
const AiLiveRevocationCodeSchema = z.enum(["login_required", "access_denied"]);
|
|
77
|
-
const AiLiveErrorCodeSchema = z.enum(["invalid_json", "invalid_message", "backpressure", "internal_error", "stream_failed"]);
|
|
78
|
-
const AiTurnErrorCodeSchema = z.enum(["not_found", "access_denied", "stream_failed"]);
|
|
79
|
-
|
|
80
|
-
export type AiLiveRevocationCode = z.infer<typeof AiLiveRevocationCodeSchema>;
|
|
81
|
-
export type AiLiveErrorCode = z.infer<typeof AiLiveErrorCodeSchema>;
|
|
82
|
-
export type AiTurnErrorCode = z.infer<typeof AiTurnErrorCodeSchema>;
|
|
83
|
-
|
|
84
|
-
const AiTurnBlockSchema = z.discriminatedUnion("kind", [
|
|
85
|
-
z.object({ id: z.string(), kind: z.literal("text"), text: z.string() }).strict(),
|
|
86
|
-
z.object({ id: z.string(), kind: z.literal("thinking"), text: z.string() }).strict(),
|
|
87
|
-
z
|
|
88
|
-
.object({
|
|
89
|
-
id: z.string(),
|
|
90
|
-
kind: z.literal("steer_message"),
|
|
91
|
-
steerId: z.string(),
|
|
92
|
-
text: z.string(),
|
|
93
|
-
status: z.enum(["pending", "consumed", "failed"]),
|
|
94
|
-
})
|
|
95
|
-
.strict(),
|
|
96
|
-
z.object({ id: z.string(), kind: z.literal("steer_applied"), steerId: z.string() }).strict(),
|
|
97
|
-
z
|
|
98
|
-
.object({
|
|
99
|
-
id: z.string(),
|
|
100
|
-
kind: z.literal("tool"),
|
|
101
|
-
callId: z.string(),
|
|
102
|
-
name: z.string(),
|
|
103
|
-
args: z.unknown().optional(),
|
|
104
|
-
status: z.enum(["running", "awaiting_approval", "awaiting_client", "completed", "failed", "rejected"]),
|
|
105
|
-
result: z.unknown().optional(),
|
|
106
|
-
isError: z.boolean().optional(),
|
|
107
|
-
approval: z.unknown().optional(),
|
|
108
|
-
frontendMode: z.enum(["client", "client_view", "client_interaction"]).optional(),
|
|
109
|
-
presentation: z.unknown().optional(),
|
|
110
|
-
})
|
|
111
|
-
.strict(),
|
|
112
|
-
z
|
|
113
|
-
.object({
|
|
114
|
-
id: z.string(),
|
|
115
|
-
kind: z.literal("compaction"),
|
|
116
|
-
status: z.enum(["running", "completed", "skipped", "failed"]),
|
|
117
|
-
result: z.unknown().optional(),
|
|
118
|
-
})
|
|
119
|
-
.strict(),
|
|
120
|
-
]);
|
|
121
|
-
|
|
122
|
-
const AiConversationSchema = z
|
|
123
|
-
.object({
|
|
124
|
-
id: AiResourceIdSchema,
|
|
125
|
-
shortId: AiResourceIdSchema,
|
|
126
|
-
title: z.string(),
|
|
127
|
-
titleSource: z.enum(["default", "auto", "user"]),
|
|
128
|
-
description: z.string(),
|
|
129
|
-
descriptionSource: z.enum(["default", "auto", "user"]),
|
|
130
|
-
keywords: z.array(z.string()),
|
|
131
|
-
pinnedAt: z.string().nullable(),
|
|
132
|
-
archivedAt: z.string().nullable(),
|
|
133
|
-
done: z.boolean().nullable(),
|
|
134
|
-
isDone: z.boolean(),
|
|
135
|
-
lastUsedAt: z.string(),
|
|
136
|
-
runStatus: z.enum(["idle", "queued", "running", "needs_attention", "waiting_for_browser", "failed"]),
|
|
137
|
-
runError: z.string().nullable(),
|
|
138
|
-
unreadCompletion: z.boolean(),
|
|
139
|
-
projectId: z.string().nullable(),
|
|
140
|
-
draft: z.object({ content: z.array(z.unknown()), revision: z.number().int(), updatedAt: z.string().nullable() }).strict(),
|
|
141
|
-
createdByUserId: z.string().nullable(),
|
|
142
|
-
createdAt: z.string(),
|
|
143
|
-
updatedAt: z.string(),
|
|
144
|
-
})
|
|
145
|
-
.strict();
|
|
146
|
-
|
|
147
|
-
const AiStoredMessageSchema = z
|
|
148
|
-
.object({
|
|
149
|
-
id: AiResourceIdSchema,
|
|
150
|
-
shortId: AiResourceIdSchema,
|
|
151
|
-
conversationId: AiResourceIdSchema,
|
|
152
|
-
seq: z.number().int().nonnegative(),
|
|
153
|
-
kind: z.enum(["message", "summary"]),
|
|
154
|
-
message: z.unknown(),
|
|
155
|
-
loopId: AiResourceIdSchema.nullable(),
|
|
156
|
-
modelProfileId: z.string().nullable(),
|
|
157
|
-
providerModel: z.string().nullable(),
|
|
158
|
-
usage: z.unknown().nullable(),
|
|
159
|
-
stopReason: z.string().nullable(),
|
|
160
|
-
loopAggregate: z.unknown().nullable(),
|
|
161
|
-
loopDoneReason: z.string().nullable(),
|
|
162
|
-
compactedAt: z.string().nullable(),
|
|
163
|
-
meta: z.unknown().nullable(),
|
|
164
|
-
createdAt: z.string(),
|
|
165
|
-
})
|
|
166
|
-
.strict();
|
|
167
|
-
|
|
168
|
-
const AiTurnSnapshotSchema = z
|
|
169
|
-
.object({
|
|
170
|
-
turnId: AiResourceIdSchema,
|
|
171
|
-
attempt: z.number().int().nonnegative(),
|
|
172
|
-
status: z.enum(["queued", "running", "waiting_for_action", "completed", "failed", "aborted"]),
|
|
173
|
-
seq: z.number().int().nonnegative(),
|
|
174
|
-
blocks: z.array(AiTurnBlockSchema),
|
|
175
|
-
modelProfileId: z.string().nullable(),
|
|
176
|
-
createdAt: z.string(),
|
|
177
|
-
})
|
|
178
|
-
.strict();
|
|
179
|
-
|
|
180
|
-
const AiWireEventBaseSchema = {
|
|
181
|
-
v: z.literal(1),
|
|
182
|
-
conversationId: AiResourceIdSchema,
|
|
183
|
-
turnId: AiResourceIdSchema,
|
|
184
|
-
attempt: z.number().int().nonnegative(),
|
|
185
|
-
seq: z.number().int().nonnegative(),
|
|
186
|
-
} as const;
|
|
187
|
-
|
|
188
|
-
const AiStreamEventWireSchema = z.union([
|
|
189
|
-
z
|
|
190
|
-
.object({
|
|
191
|
-
type: z.literal("state"),
|
|
192
|
-
conversation: AiConversationSchema,
|
|
193
|
-
messages: z.array(AiStoredMessageSchema),
|
|
194
|
-
hasMoreMessages: z.boolean().optional(),
|
|
195
|
-
activeTurn: AiTurnSnapshotSchema.nullable(),
|
|
196
|
-
})
|
|
197
|
-
.strict(),
|
|
198
|
-
z
|
|
199
|
-
.object({
|
|
200
|
-
...AiWireEventBaseSchema,
|
|
201
|
-
type: z.literal("turn_started"),
|
|
202
|
-
modelProfileId: z.string(),
|
|
203
|
-
providerModel: z.string(),
|
|
204
|
-
blocks: z.array(AiTurnBlockSchema).optional(),
|
|
205
|
-
})
|
|
206
|
-
.strict(),
|
|
207
|
-
z.object({ ...AiWireEventBaseSchema, type: z.literal("block_set"), block: AiTurnBlockSchema }).strict(),
|
|
208
|
-
z
|
|
209
|
-
.object({
|
|
210
|
-
...AiWireEventBaseSchema,
|
|
211
|
-
type: z.literal("block_delta"),
|
|
212
|
-
blockId: z.string(),
|
|
213
|
-
blockKind: z.enum(["text", "thinking"]),
|
|
214
|
-
delta: z.string(),
|
|
215
|
-
})
|
|
216
|
-
.strict(),
|
|
217
|
-
z
|
|
218
|
-
.object({
|
|
219
|
-
...AiWireEventBaseSchema,
|
|
220
|
-
type: z.literal("turn_finished"),
|
|
221
|
-
status: z.enum(["completed", "failed", "aborted"]),
|
|
222
|
-
error: z.string().nullable(),
|
|
223
|
-
messages: z.array(AiStoredMessageSchema).optional(),
|
|
224
|
-
})
|
|
225
|
-
.strict(),
|
|
226
|
-
]);
|
|
227
|
-
|
|
228
|
-
export const AiStreamEventSchema = z.custom<AiStreamEvent>((value) => AiStreamEventWireSchema.safeParse(value).success);
|
|
229
|
-
|
|
230
|
-
export const AiLiveServerMessageSchema = z.discriminatedUnion("type", [
|
|
231
|
-
z
|
|
232
|
-
.object({
|
|
233
|
-
type: z.literal(AI_LIVE_WS_TYPE.ready),
|
|
234
|
-
payload: z.object({ cursor: AiLiveCursorSchema, recovered: z.boolean() }).strict(),
|
|
235
|
-
})
|
|
236
|
-
.strict(),
|
|
237
|
-
z
|
|
238
|
-
.object({
|
|
239
|
-
type: z.literal(AI_LIVE_WS_TYPE.event),
|
|
240
|
-
payload: z.object({ cursor: AiLiveCursorSchema, event: AiInvalidationSchema }).strict(),
|
|
241
|
-
})
|
|
242
|
-
.strict(),
|
|
243
|
-
z
|
|
244
|
-
.object({
|
|
245
|
-
type: z.literal(AI_LIVE_WS_TYPE.scopeChanged),
|
|
246
|
-
payload: z.object({ at: z.string().datetime() }).strict(),
|
|
247
|
-
})
|
|
248
|
-
.strict(),
|
|
249
|
-
z
|
|
250
|
-
.object({
|
|
251
|
-
type: z.literal(AI_LIVE_WS_TYPE.revoked),
|
|
252
|
-
payload: z.object({ code: AiLiveRevocationCodeSchema, message: z.string().min(1).max(500) }).strict(),
|
|
253
|
-
})
|
|
254
|
-
.strict(),
|
|
255
|
-
z
|
|
256
|
-
.object({
|
|
257
|
-
type: z.literal(AI_LIVE_WS_TYPE.error),
|
|
258
|
-
payload: z.object({ code: AiLiveErrorCodeSchema, message: z.string().min(1).max(500) }).strict(),
|
|
259
|
-
})
|
|
260
|
-
.strict(),
|
|
261
|
-
z
|
|
262
|
-
.object({
|
|
263
|
-
type: z.literal(AI_LIVE_WS_TYPE.turnEvent),
|
|
264
|
-
payload: z.object({ conversationId: AiResourceIdSchema, event: AiStreamEventSchema }).strict(),
|
|
265
|
-
})
|
|
266
|
-
.strict(),
|
|
267
|
-
z
|
|
268
|
-
.object({
|
|
269
|
-
type: z.literal(AI_LIVE_WS_TYPE.turnError),
|
|
270
|
-
payload: z.object({ conversationId: AiResourceIdSchema, code: AiTurnErrorCodeSchema, message: z.string().min(1).max(500) }).strict(),
|
|
271
|
-
})
|
|
272
|
-
.strict(),
|
|
273
|
-
]);
|
|
274
|
-
|
|
275
|
-
export type AiLiveServerMessage = z.infer<typeof AiLiveServerMessageSchema>;
|
|
276
|
-
|
|
277
|
-
export const aiTurnEventMessage = (conversationId: string, event: AiStreamEvent): AiLiveServerMessage => ({
|
|
278
|
-
type: AI_LIVE_WS_TYPE.turnEvent,
|
|
279
|
-
payload: { conversationId, event },
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
export const parseAiLiveServerMessage = (raw: string): AiLiveServerMessage | null => {
|
|
283
|
-
try {
|
|
284
|
-
const parsed = AiLiveServerMessageSchema.safeParse(JSON.parse(raw));
|
|
285
|
-
if (!parsed.success) return null;
|
|
286
|
-
if (parsed.data.type !== AI_LIVE_WS_TYPE.turnEvent) return parsed.data;
|
|
287
|
-
const eventConversationId =
|
|
288
|
-
parsed.data.payload.event.type === "state" ? parsed.data.payload.event.conversation.id : parsed.data.payload.event.conversationId;
|
|
289
|
-
return eventConversationId === parsed.data.payload.conversationId ? parsed.data : null;
|
|
290
|
-
} catch {
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
};
|
package/src/ai/live.ts
CHANGED
|
@@ -1,2 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineLive, type LiveViewer } from "../events/live";
|
|
3
|
+
import { AiInvalidationSchema } from "./live-events";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Live updates of a user's AI data. Core publishes and serves them at
|
|
7
|
+
* `/api/ai/live`; the AI's database triggers write them, keyed `u:<user ID>`,
|
|
8
|
+
* in the transaction of every change. Other applications, such as Assistant,
|
|
9
|
+
* read `cursor()` before their snapshot.
|
|
10
|
+
*/
|
|
11
|
+
export const aiLive = defineLive({ appId: "core", event: AiInvalidationSchema });
|
|
12
|
+
|
|
13
|
+
/** The user an AI request acts for: the user, or the user a credential is delegated by. */
|
|
14
|
+
const ownerOf = (viewer: LiveViewer): string | null => (viewer.accessSubject.type === "user" ? viewer.accessSubject.userId : null);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `user` follows the viewer's own AI data: conversations, their files, tasks and
|
|
18
|
+
* dictations, and the Projects the viewer can read. Its single key admits only
|
|
19
|
+
* that user; who receives a Project change was decided when it was written.
|
|
20
|
+
*/
|
|
21
|
+
export const aiLiveChannels = {
|
|
22
|
+
user: {
|
|
23
|
+
scope: z.object({}).strict(),
|
|
24
|
+
keys: async (_scope: unknown, viewer: LiveViewer) => {
|
|
25
|
+
const owner = ownerOf(viewer);
|
|
26
|
+
return owner ? [`u:${owner}`] : null;
|
|
27
|
+
},
|
|
28
|
+
authorize: async (key: string, viewers: readonly LiveViewer[]) =>
|
|
29
|
+
new Set(viewers.filter((viewer) => `u:${ownerOf(viewer)}` === key).map((viewer) => viewer.id)),
|
|
30
|
+
},
|
|
31
|
+
};
|
package/src/ai/migrate.ts
CHANGED
|
@@ -13,6 +13,14 @@ const backfillAiShortIds = async (
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export const migrateCloudAi = async (): Promise<void> => {
|
|
16
|
+
// AI writes its live updates to Core's platform outbox, in the transaction of every
|
|
17
|
+
// AI write: without events.enqueue(), every one of them would fail.
|
|
18
|
+
const [outbox] = await sql<{ ready: boolean }[]>`
|
|
19
|
+
SELECT to_regprocedure('events.enqueue(uuid,text,text,text,jsonb,text)') IS NOT NULL AS ready
|
|
20
|
+
`;
|
|
21
|
+
if (!outbox?.ready) {
|
|
22
|
+
throw new Error("AI live updates write to events.outbox, which does not exist. Run Core's events migration before the AI migration.");
|
|
23
|
+
}
|
|
16
24
|
await sql`CREATE SCHEMA IF NOT EXISTS ai`.simple();
|
|
17
25
|
console.log(" ✓ ai schema");
|
|
18
26
|
|
|
@@ -1722,10 +1730,12 @@ export const migrateCloudAi = async (): Promise<void> => {
|
|
|
1722
1730
|
WHERE project_id IS NOT NULL AND archived_at IS NULL
|
|
1723
1731
|
`.simple();
|
|
1724
1732
|
|
|
1725
|
-
//
|
|
1726
|
-
//
|
|
1727
|
-
//
|
|
1728
|
-
|
|
1733
|
+
// Live updates. Triggers live at the persistence seam so route, worker, tool,
|
|
1734
|
+
// enrichment, and scheduler writes cannot bypass them. They write to Core's
|
|
1735
|
+
// platform outbox in the transaction of the domain write (see the start of this migration).
|
|
1736
|
+
|
|
1737
|
+
// The previous outbox. Nothing writes it anymore; an older Core that still runs during
|
|
1738
|
+
// an update finds it empty. The cleanup release drops it with the topic cloud-ai-invalidations.
|
|
1729
1739
|
await sql`
|
|
1730
1740
|
CREATE TABLE IF NOT EXISTS ai.live_invalidation_outbox (
|
|
1731
1741
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
@@ -1786,6 +1796,10 @@ export const migrateCloudAi = async (): Promise<void> => {
|
|
|
1786
1796
|
DROP FUNCTION IF EXISTS ai.enqueue_live_for_user(UUID, TEXT, UUID, TEXT, TEXT, TEXT[])
|
|
1787
1797
|
`.simple();
|
|
1788
1798
|
|
|
1799
|
+
// Every AI live update goes through this function: one update for one user,
|
|
1800
|
+
// keyed `u:<user>`, on Core's live topic. Its data is an `AiInvalidation`.
|
|
1801
|
+
// No coalesce key: the last payload would win and drop the domains of earlier ones.
|
|
1802
|
+
// Gateway health reports a body without events.enqueue: an older Core restored its own.
|
|
1789
1803
|
await sql`
|
|
1790
1804
|
CREATE OR REPLACE FUNCTION ai.enqueue_live_for_user(
|
|
1791
1805
|
p_change_id UUID,
|
|
@@ -1796,11 +1810,23 @@ export const migrateCloudAi = async (): Promise<void> => {
|
|
|
1796
1810
|
) RETURNS void AS $$
|
|
1797
1811
|
BEGIN
|
|
1798
1812
|
IF p_user_id IS NULL OR cardinality(p_domains) = 0 THEN RETURN; END IF;
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1813
|
+
PERFORM events.enqueue(
|
|
1814
|
+
gen_random_uuid(),
|
|
1815
|
+
'core',
|
|
1816
|
+
'live',
|
|
1817
|
+
'u:' || p_user_id::text,
|
|
1818
|
+
jsonb_build_object(
|
|
1819
|
+
'v', 1,
|
|
1820
|
+
'k', 'u:' || p_user_id::text,
|
|
1821
|
+
'd', jsonb_build_object(
|
|
1822
|
+
'type', 'ai.invalidated',
|
|
1823
|
+
'changeId', p_change_id,
|
|
1824
|
+
'conversationId', p_conversation_short_id,
|
|
1825
|
+
'projectId', p_project_short_id,
|
|
1826
|
+
'domains', to_jsonb(ARRAY(SELECT DISTINCT domain FROM unnest(p_domains) domain ORDER BY domain)),
|
|
1827
|
+
'at', to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')
|
|
1828
|
+
)
|
|
1829
|
+
)
|
|
1804
1830
|
);
|
|
1805
1831
|
END
|
|
1806
1832
|
$$ LANGUAGE plpgsql
|
|
@@ -2213,13 +2239,6 @@ export const migrateCloudAi = async (): Promise<void> => {
|
|
|
2213
2239
|
)`.simple();
|
|
2214
2240
|
await sql`CREATE INDEX IF NOT EXISTS ai_dictations_pending ON ai.dictations(conversation_id, created_at, id) WHERE disposition = 'pending'`.simple();
|
|
2215
2241
|
await sql`CREATE INDEX IF NOT EXISTS ai_dictations_recovery ON ai.dictations(status, next_attempt_at, lease_until) WHERE status IN ('queued', 'running')`.simple();
|
|
2216
|
-
await sql.begin(async (tx) => {
|
|
2217
|
-
await tx`ALTER TABLE ai.live_invalidation_outbox DROP CONSTRAINT IF EXISTS ai_live_invalidation_domains_check`;
|
|
2218
|
-
await tx`ALTER TABLE ai.live_invalidation_outbox ADD CONSTRAINT ai_live_invalidation_domains_check CHECK (
|
|
2219
|
-
domains <@ ARRAY['conversation-list', 'conversation-detail', 'conversation-sources', 'conversation-files', 'conversation-tasks',
|
|
2220
|
-
'conversation-dictations', 'project-list', 'project-detail', 'project-context']::text[] AND cardinality(domains) > 0
|
|
2221
|
-
)`;
|
|
2222
|
-
});
|
|
2223
2242
|
await sql`DROP TRIGGER IF EXISTS ai_live_dictations_changed ON ai.dictations`.simple();
|
|
2224
2243
|
await sql`DROP TRIGGER IF EXISTS ai_live_dictations_state_changed ON ai.dictations`.simple();
|
|
2225
2244
|
await sql`CREATE TRIGGER ai_live_dictations_changed AFTER INSERT OR DELETE ON ai.dictations
|
package/src/ai/pdf-render.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { fileURLToPath } from "node:url";
|
|
2
1
|
import { z } from "zod";
|
|
2
|
+
import { spawnCanvasWorker } from "../_internal/canvas-worker";
|
|
3
3
|
|
|
4
4
|
declare const __CLOUD_PDF_RENDER_WORKER__: string | undefined;
|
|
5
5
|
let activeRenders = 0;
|
|
@@ -31,27 +31,7 @@ export async function renderPdfPages(bytes: Uint8Array, pages: number[] = [1], s
|
|
|
31
31
|
typeof __CLOUD_PDF_RENDER_WORKER__ === "string"
|
|
32
32
|
? new URL(__CLOUD_PDF_RENDER_WORKER__, import.meta.url)
|
|
33
33
|
: new URL("./pdf-render-worker.ts", import.meta.url);
|
|
34
|
-
|
|
35
|
-
// Positional arguments keep paths out of shell code. No elevated privileges.
|
|
36
|
-
const child = Bun.spawn(
|
|
37
|
-
["/bin/sh", "-c", 'ulimit -d 524288 || exit 70; exec "$@"', "pdf-decoder", process.execPath, "--no-env-file", fileURLToPath(worker)],
|
|
38
|
-
{
|
|
39
|
-
stdin: new Blob([JSON.stringify({ pdf: Buffer.from(bytes).toString("base64"), pages })]),
|
|
40
|
-
stdout: "pipe",
|
|
41
|
-
stderr: "ignore",
|
|
42
|
-
// No application credentials or configuration reach the decoder, only fixed
|
|
43
|
-
// thread pool sizes: the Bun work pool, canvas tokio runtime, and JSC GC
|
|
44
|
-
// markers otherwise grow with the host's cores, and RLIMIT_DATA also counts
|
|
45
|
-
// each thread's stack and allocator memory.
|
|
46
|
-
env: {
|
|
47
|
-
PATH: process.env.PATH,
|
|
48
|
-
LANG: "C.UTF-8",
|
|
49
|
-
UV_THREADPOOL_SIZE: "2",
|
|
50
|
-
TOKIO_WORKER_THREADS: "2",
|
|
51
|
-
BUN_JSC_numberOfGCMarkers: "2",
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
);
|
|
34
|
+
const child = spawnCanvasWorker(worker, "pdf-decoder", JSON.stringify({ pdf: Buffer.from(bytes).toString("base64"), pages }));
|
|
55
35
|
const stop = () => child.kill("SIGKILL");
|
|
56
36
|
boundedSignal.addEventListener("abort", stop, { once: true });
|
|
57
37
|
if (boundedSignal.aborted) stop();
|
package/src/ai/routes.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
respond,
|
|
17
17
|
v,
|
|
18
18
|
} from "../server";
|
|
19
|
+
import { isRequestCredentialCurrent } from "../server/middleware/auth";
|
|
19
20
|
import { logger } from "../services/logging";
|
|
20
21
|
import { coreSettings } from "../services/settings/api";
|
|
21
22
|
import type { AiToolApprovalContext } from "./approvals";
|
|
@@ -1108,11 +1109,11 @@ export const aiRoutes = (() => {
|
|
|
1108
1109
|
return createAiConversationStreamResponse({
|
|
1109
1110
|
conversation,
|
|
1110
1111
|
signal: c.req.raw.signal,
|
|
1111
|
-
//
|
|
1112
|
-
//
|
|
1113
|
-
//
|
|
1114
|
-
// a withdrawn grant but not a revoked credential — see r9358ceb.
|
|
1112
|
+
// The actor is a request-time snapshot: re-authenticate its
|
|
1113
|
+
// credential first, so a revoked session or API key ends the stream,
|
|
1114
|
+
// then re-check ownership and resource scope of the conversation.
|
|
1115
1115
|
revalidate: async () => {
|
|
1116
|
+
if (!(await isRequestCredentialCurrent(c))) return false;
|
|
1116
1117
|
const current = await resolveContext(c);
|
|
1117
1118
|
if (current instanceof Response) return false;
|
|
1118
1119
|
return Boolean(await loadConversation(c, current));
|
package/src/ai/runtime.ts
CHANGED
|
@@ -12,7 +12,6 @@ import { aiChatAccessSubject, selectAssistantAiModelId } from "./assistant-model
|
|
|
12
12
|
import { startAiDictationRuntime } from "./dictation-runtime";
|
|
13
13
|
import { AiTurnExecutor } from "./executor";
|
|
14
14
|
import { canonicalizeAiConversationAttachments, snapshotAiConversationFiles } from "./file-context";
|
|
15
|
-
import { startAiInvalidationRuntime, stopAiInvalidationRuntime } from "./live-outbox";
|
|
16
15
|
import { drainQueuedMessages } from "./message-queue";
|
|
17
16
|
import { aiQuotas } from "./quotas";
|
|
18
17
|
import { parseAiResourceMarker } from "./resource-markers";
|
|
@@ -577,7 +576,6 @@ export const startAiRuntime = (
|
|
|
577
576
|
},
|
|
578
577
|
};
|
|
579
578
|
running = state;
|
|
580
|
-
startAiInvalidationRuntime();
|
|
581
579
|
log.info("AI runtime started", { workerId: AI_WORKER_ID, concurrency });
|
|
582
580
|
return releaseAiRuntime(listener);
|
|
583
581
|
};
|
|
@@ -591,10 +589,7 @@ const releaseAiRuntime = (listener: ((event: AiTurnFinalizedEvent) => Promise<vo
|
|
|
591
589
|
if (!state) return;
|
|
592
590
|
if (listener) state.listeners.delete(listener);
|
|
593
591
|
refCount = Math.max(0, refCount - 1);
|
|
594
|
-
if (refCount === 0)
|
|
595
|
-
state.stop();
|
|
596
|
-
void stopAiInvalidationRuntime();
|
|
597
|
-
}
|
|
592
|
+
if (refCount === 0) state.stop();
|
|
598
593
|
};
|
|
599
594
|
};
|
|
600
595
|
|
package/src/ai/solid.ts
CHANGED
|
@@ -7,11 +7,6 @@ export {
|
|
|
7
7
|
createAiChatController,
|
|
8
8
|
} from "./client/controller";
|
|
9
9
|
export { conversationFileSource } from "./client/file-source";
|
|
10
|
-
export {
|
|
11
|
-
type AiLiveConnection,
|
|
12
|
-
type CreateAiLiveConnectionOptions,
|
|
13
|
-
createAiLiveConnection,
|
|
14
|
-
} from "./client/live-connection";
|
|
15
10
|
export {
|
|
16
11
|
type AiActiveTurn,
|
|
17
12
|
type AiChatProjection,
|