@jskit-ai/assistant-runtime 0.2.30 → 0.2.31

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/CHANGELOG.md CHANGED
@@ -3,6 +3,7 @@
3
3
  ## Unreleased
4
4
 
5
5
  - The conversations and messages repositories hand their handle calls only the caller's transaction, with their own context and settings: a caller's other settings (a format, `missing`, `returning`, `fields`) no longer ride into every read and write they make. They take `passedOn` from `@jskit-ai/resource-core/shared/callOptions`, so `@jskit-ai/assistant-runtime/server` still loads on file storage, where `@jskit-ai/resource-runtime` (an optional peer) is not installed.
6
+ - Its table resources declare the foreign keys their migrations always create (`foreignKeys`), so json-rest-api 3.1.0 can skip the visibility query a system-context read makes for a record such a key guarantees exists. `jskit test --databases` checks each against an app's test database: assistant_conversations.created_by_user_id and assistant_turn_requests.actor_user_id to users, and assistant_messages.conversation_id to assistant_conversations and actor_user_id to users. The workspace_id keys exist only where the application has a workspaces table, so none declares them. `usersForeignKeysMigration.test.js` proves the declarations match the migrations.
6
7
  - A database error in an assistant turn reaches the client only as the kernel's generic answer (`hideDatabaseError`: 409 `kernel.database.concurrentWrite` when the database refused the transaction for a concurrent one, else 500). A failure after the stream started was sent with its own message, which for a database error is the statement and its values, and the same message was saved with the turn and replayed, as an `AppError`, to a client that sent the message again; the chat route's error before the stream and a conversation subscription's acknowledgement carried the driver's code. Each now sends and saves the stand-in and logs the whole error; `createChatService` takes the runtime's `logger`.
7
8
  - The assistant settings pane shows **Save** only to people the server lets change the settings. It offered Save to anyone who could read them, so a person without the permission (an employee, say) saw Save and was refused with `assistant.runtime.settings.updateForbidden` when they used it. The settings read now carries `canUpdate`, answered by the new `assistant.settings` service method `canUpdateSettings(input, { context })`, which runs the same check as `updateSettings` (an app's `assistantServer.<surface>.settingsUpdatePermission`, else `workspace.settings.update`, the console owner or `assistant.settings.update`) and answers false where that check refuses; a saved change carries `canUpdate: true`. Without it, the pane shows the instructions read-only, with no Save in the app bar. Apps change nothing; an app that supplies its own `assistant.settings` service adds `canUpdateSettings`.
8
9
  - A conversation write through the SQL storage issues 8 statements on SQLite where it issued 12, whatever the conversation's length: the conversation and its transcript state come from one read, new messages take the next seq after the one the load read under the conversation's lock instead of reading it again, and one UPDATE writes the transcript state with the record's changes instead of an update, a read-back and a second update. A read issues 4 where it issued 5. `assistant.repositories`' conversations replace `readTranscriptState(id)` with `findWithTranscriptState(id, options)`, which answers `{ conversation, revision, summary }` (under a row lock with `forUpdate`), and `writeTranscriptState` takes the record's `changes` beside the revision and summary.
@@ -24,6 +25,7 @@
24
25
 
25
26
  ### Upgrading
26
27
 
28
+ - Register `AssistantStoreFeature` from `@jskit-ai/assistant-runtime/server` in the server providers beside `AssistantFeature`, unless the app provides `assistant.conversations` itself: the assistant's four tables are table resources that `AssistantStoreFeature` owns, and it provides the `assistant.repositories` the database-backed assistant keeps its conversations in, so `AssistantFeature` without it refuses to start, naming it.
27
29
  - Grant `assistant.settings.update` to the people who configure the assistant, or set `settingsUpdatePermission` to a permission they already hold.
28
30
 
29
31
  ## 0.2.0 - 2026-10-29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-runtime",
3
- "version": "0.2.30",
3
+ "version": "0.2.31",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "jskit test"
@@ -15,21 +15,21 @@
15
15
  "./locales/en.json": "./locales/en.json"
16
16
  },
17
17
  "dependencies": {
18
- "@jskit-ai/assistant-core": "0.2.28",
19
- "@jskit-ai/resource-core": "0.1.205",
18
+ "@jskit-ai/assistant-core": "0.2.29",
19
+ "@jskit-ai/resource-core": "0.1.206",
20
20
  "json-rest-schema": "^1.0.17"
21
21
  },
22
22
  "peerDependencies": {
23
- "@jskit-ai/http-runtime": "0.2.14",
24
- "@jskit-ai/http-web": "0.2.22",
25
- "@jskit-ai/kernel": "0.2.14",
26
- "@jskit-ai/realtime": "0.2.20",
27
- "@jskit-ai/shell-web": "0.2.17",
23
+ "@jskit-ai/http-runtime": "0.2.15",
24
+ "@jskit-ai/http-web": "0.2.23",
25
+ "@jskit-ai/kernel": "0.2.15",
26
+ "@jskit-ai/realtime": "0.2.21",
27
+ "@jskit-ai/shell-web": "0.2.18",
28
28
  "@tanstack/vue-query": "^5.90.5",
29
29
  "vue": "^3.5.13",
30
30
  "vuetify": "^4.0.0",
31
- "@jskit-ai/database-runtime": "0.1.263",
32
- "@jskit-ai/resource-runtime": "0.2.12"
31
+ "@jskit-ai/database-runtime": "0.1.264",
32
+ "@jskit-ai/resource-runtime": "0.2.13"
33
33
  },
34
34
  "description": "Capability-based assistant runtime with per-surface chat, transcripts, settings, and action tools.",
35
35
  "jskit": {
@@ -139,6 +139,6 @@
139
139
  },
140
140
  "devDependencies": {
141
141
  "@jskit-ai/material-web": "0.1.3",
142
- "@jskit-ai/resource-runtime": "0.2.12"
142
+ "@jskit-ai/resource-runtime": "0.2.13"
143
143
  }
144
144
  }
@@ -2,7 +2,10 @@ import { defineTableResource } from "@jskit-ai/resource-core/shared/tableResourc
2
2
 
3
3
  // The assistant's four tables. The repositories normalize every value before
4
4
  // they write it, so text is stored exactly as given (noTrim), and dates are
5
- // written as UTC database date-times.
5
+ // written as UTC database date-times. Each declares the foreign keys its
6
+ // migrations always create; the workspace_id keys exist only where the
7
+ // application has a workspaces table (constraints/assistant_workspaces_foreign_keys.cjs),
8
+ // so none declares them.
6
9
 
7
10
  /** @param {Record<string, unknown>} [definition] */
8
11
  function textField(definition = {}) {
@@ -36,6 +39,7 @@ const assistantConversationsResource = defineTableResource({
36
39
  name: "assistantConversations",
37
40
  tableName: "assistant_conversations",
38
41
  ownership: "user",
42
+ foreignKeys: [{ columns: ["userId"], referencedTableName: "users" }],
39
43
  queryMaxLimit: 200,
40
44
  schema: {
41
45
  workspaceId: { type: "id", nullable: true, search: true },
@@ -65,6 +69,10 @@ const assistantMessagesResource = defineTableResource({
65
69
  name: "assistantMessages",
66
70
  tableName: "assistant_messages",
67
71
  ownership: "workspace",
72
+ foreignKeys: [
73
+ { columns: ["conversationId"], referencedTableName: "assistant_conversations" },
74
+ { columns: ["actorUserId"], referencedTableName: "users" }
75
+ ],
68
76
  queryMaxLimit: 500,
69
77
  schema: {
70
78
  conversationId: { type: "id", search: true },
@@ -89,6 +97,7 @@ const assistantTurnRequestsResource = defineTableResource({
89
97
  name: "assistantTurnRequests",
90
98
  tableName: "assistant_turn_requests",
91
99
  ownership: "user",
100
+ foreignKeys: [{ columns: ["userId"], referencedTableName: "users" }],
92
101
  schema: {
93
102
  userId: { type: "id", search: true, storage: { column: "actor_user_id" } },
94
103
  scopeKey: textField({ search: true }),
@@ -5,6 +5,9 @@ import configMigration from "../migrations/assistant_config_initial.cjs";
5
5
  import transcriptsMigration from "../migrations/assistant_transcripts_initial.cjs";
6
6
  import turnRequestsMigration from "../migrations/assistant_turn_requests.cjs";
7
7
  import usersForeignKeysMigration from "../migrations/constraints/assistant_users_foreign_keys.cjs";
8
+ import workspacesForeignKeysMigration from "../migrations/constraints/assistant_workspaces_foreign_keys.cjs";
9
+ import { checkForeignKeys } from "@jskit-ai/resource-runtime/server";
10
+ import * as assistantResources from "../src/server/repositories/assistantResources.js";
8
11
 
9
12
  const USER_KEYS = [
10
13
  { table: "assistant_conversations", from: "created_by_user_id", on_delete: "SET NULL" },
@@ -44,3 +47,24 @@ test("the assistant tables are created before users exists and get their users k
44
47
  await usersForeignKeysMigration.down(db);
45
48
  assert.deepEqual(await usersKeys(db), []);
46
49
  });
50
+
51
+ test("the assistant table resources declare the foreign keys their migrations always create", async (t) => {
52
+ const db = knex({ client: "better-sqlite3", connection: { filename: ":memory:" }, useNullAsDefault: true });
53
+ t.after(() => db.destroy());
54
+ const resources = Object.values(assistantResources);
55
+ const tables = resources.map((resource) => resource.tableName);
56
+ for (const migration of [configMigration, transcriptsMigration, turnRequestsMigration]) await migration.up(db);
57
+ await db.schema.createTable("users", (table) => table.bigInteger("id").primary());
58
+ await usersForeignKeysMigration.up(db);
59
+ assert.deepEqual(await checkForeignKeys({ knex: db, resources, tables }), { missing: [], undeclared: [] });
60
+
61
+ // An application with workspaces also gets the workspace_id keys, which no resource declares: not every
62
+ // application has them.
63
+ await db.schema.createTable("workspaces", (table) => table.bigInteger("id").primary());
64
+ await workspacesForeignKeysMigration.up(db);
65
+ const { missing, undeclared } = await checkForeignKeys({ knex: db, resources, tables });
66
+ assert.deepEqual(missing, []);
67
+ assert.deepEqual(undeclared.map(({ tableName, columns, referencedTableName }) => ({ tableName, columns, referencedTableName })),
68
+ ["assistant_config", "assistant_conversations", "assistant_messages"]
69
+ .map((tableName) => ({ tableName, columns: ["workspace_id"], referencedTableName: "workspaces" })));
70
+ });