@things-factory/ai-assistant 10.1.6 → 10.1.8

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.
Files changed (44) hide show
  1. package/client/components/board-ai-chat.ts +165 -100
  2. package/client/components/chat-defaults.ts +13 -0
  3. package/client/components/chat-input-builder.test.ts +81 -0
  4. package/client/components/chat-input-builder.ts +43 -0
  5. package/client/components/chat-triggers.test.ts +72 -0
  6. package/client/components/chat-triggers.ts +62 -0
  7. package/dist-client/components/board-ai-chat.d.ts +8 -0
  8. package/dist-client/components/board-ai-chat.js +160 -100
  9. package/dist-client/components/board-ai-chat.js.map +1 -1
  10. package/dist-client/components/chat-defaults.d.ts +12 -0
  11. package/dist-client/components/chat-defaults.js +2 -0
  12. package/dist-client/components/chat-defaults.js.map +1 -1
  13. package/dist-client/components/chat-input-builder.d.ts +29 -0
  14. package/dist-client/components/chat-input-builder.js +22 -0
  15. package/dist-client/components/chat-input-builder.js.map +1 -1
  16. package/dist-client/components/chat-input-builder.test.d.ts +1 -0
  17. package/dist-client/components/chat-input-builder.test.js +67 -0
  18. package/dist-client/components/chat-input-builder.test.js.map +1 -0
  19. package/dist-client/components/chat-triggers.d.ts +16 -0
  20. package/dist-client/components/chat-triggers.js +42 -0
  21. package/dist-client/components/chat-triggers.js.map +1 -0
  22. package/dist-client/components/chat-triggers.test.d.ts +1 -0
  23. package/dist-client/components/chat-triggers.test.js +61 -0
  24. package/dist-client/components/chat-triggers.test.js.map +1 -0
  25. package/dist-client/tsconfig.tsbuildinfo +1 -1
  26. package/dist-server/service/assistant-chat-resolver.d.ts +32 -0
  27. package/dist-server/service/assistant-chat-resolver.js +342 -0
  28. package/dist-server/service/assistant-chat-resolver.js.map +1 -0
  29. package/dist-server/service/chat-session-resolver.js +18 -18
  30. package/dist-server/service/chat-session-resolver.js.map +1 -1
  31. package/dist-server/service/index.d.ts +3 -1
  32. package/dist-server/service/index.js +4 -0
  33. package/dist-server/service/index.js.map +1 -1
  34. package/dist-server/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +5 -5
  36. package/server/service/assistant-chat-resolver.ts +326 -0
  37. package/server/service/chat-session-resolver.ts +18 -18
  38. package/server/service/index.ts +4 -0
  39. package/test/translations.test.ts +79 -0
  40. package/translations/en.json +59 -0
  41. package/translations/ja.json +58 -0
  42. package/translations/ko.json +59 -0
  43. package/translations/ms.json +58 -0
  44. package/translations/zh.json +58 -0
@@ -0,0 +1,32 @@
1
+ import '@things-factory/auth-base';
2
+ declare class AssistantMessageInput {
3
+ role: string;
4
+ content: string;
5
+ }
6
+ declare class AssistantChatInput {
7
+ sessionId?: string;
8
+ messages: AssistantMessageInput[];
9
+ systemPrompt: string;
10
+ toolCategories?: string[];
11
+ hostContext?: any;
12
+ truncateAfterMessageId?: string;
13
+ requireGroundingTools?: boolean;
14
+ model?: string;
15
+ temperature?: number;
16
+ maxTokens?: number;
17
+ }
18
+ declare class AssistantChatOutput {
19
+ reply: string;
20
+ clientId: string;
21
+ sessionId?: string;
22
+ userMessageId?: string;
23
+ assistantMessageId?: string;
24
+ toolUsages?: any;
25
+ offeredTools?: string[];
26
+ proposals?: any;
27
+ groundingWarnings?: any;
28
+ }
29
+ export declare class AssistantChatResolver {
30
+ assistantChat(input: AssistantChatInput, context: any): Promise<AssistantChatOutput>;
31
+ }
32
+ export {};
@@ -0,0 +1,342 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AssistantChatResolver = void 0;
4
+ const tslib_1 = require("tslib");
5
+ /*
6
+ * The neutral door into a conversation.
7
+ *
8
+ * ── Why this exists (2026-09-11) ───────────────────────────────────────────────
9
+ * The chat component moved here from `board-ai`, but the only mutation it could call stayed
10
+ * behind. operato-figure mounted the dock, registered four tools, and got
11
+ * `Cannot query field "boardAIChat" on type "Mutation"` — it does not load board-ai. The twin has
12
+ * the same shape and it does not show: its space deliberation runs through board-ai's door purely
13
+ * because the board authoring half of the same product already loaded that package.
14
+ *
15
+ * ── Why the board resolver was not moved ───────────────────────────────────────
16
+ * `board-ai-resolver.ts` is 692 lines and the larger part of it is board authoring itself:
17
+ * `currentBoard` as a BoardModel, PatchEntry, board patch broadcasts, refid injection for
18
+ * component mentions. Moving that here would teach this package the board document, which is the
19
+ * dependency we just cut, pointing the other way.
20
+ *
21
+ * So the door moved and the editor did not. `boardAIChat` stays in board-ai for board authoring;
22
+ * this one is for a surface that asks questions and runs registered tools.
23
+ *
24
+ * ── What this is not ───────────────────────────────────────────────────────────
25
+ * It has no document, so it takes no document and returns no patch. Tools that write to a
26
+ * document are not offered here — `registryAssistantChat` enforces that, and it decides what may
27
+ * be dispatched from the same list it offers, so the two cannot drift apart.
28
+ */
29
+ const type_graphql_1 = require("type-graphql");
30
+ const graphql_scalars_1 = require("graphql-scalars");
31
+ require("@things-factory/auth-base");
32
+ const env_1 = require("@things-factory/env");
33
+ const shell_1 = require("@things-factory/shell");
34
+ const ai_client_base_1 = require("@things-factory/ai-client-base");
35
+ const chat_session_js_1 = require("./chat-session/chat-session.js");
36
+ const chat_message_js_1 = require("./chat-message/chat-message.js");
37
+ const llm_history_js_1 = require("./chat-message/llm-history.js");
38
+ const chat_message_publish_js_1 = require("./chat-message/chat-message-publish.js");
39
+ const session_activity_publish_js_1 = require("./chat-session/session-activity-publish.js");
40
+ const session_inbox_js_1 = require("./chat-session/session-inbox.js");
41
+ /**
42
+ * How many stored messages reach the model.
43
+ *
44
+ * A deliberation runs for days. Without a cap the prompt grows without bound and the oldest turns
45
+ * bury the recent ones. Kept the same as the board conversation's cap so the two surfaces of one
46
+ * product do not remember differently.
47
+ */
48
+ const LLM_HISTORY_MAX_TURNS = 40;
49
+ let AssistantMessageInput = class AssistantMessageInput {
50
+ };
51
+ tslib_1.__decorate([
52
+ (0, type_graphql_1.Field)({ description: "Role: 'user' or 'assistant'" }),
53
+ tslib_1.__metadata("design:type", String)
54
+ ], AssistantMessageInput.prototype, "role", void 0);
55
+ tslib_1.__decorate([
56
+ (0, type_graphql_1.Field)({ description: 'Message content' }),
57
+ tslib_1.__metadata("design:type", String)
58
+ ], AssistantMessageInput.prototype, "content", void 0);
59
+ AssistantMessageInput = tslib_1.__decorate([
60
+ (0, type_graphql_1.InputType)()
61
+ ], AssistantMessageInput);
62
+ let AssistantChatInput = class AssistantChatInput {
63
+ };
64
+ tslib_1.__decorate([
65
+ (0, type_graphql_1.Field)({
66
+ nullable: true,
67
+ description: 'ChatSession id. Omit for a one-off question with no persistence.'
68
+ }),
69
+ tslib_1.__metadata("design:type", String)
70
+ ], AssistantChatInput.prototype, "sessionId", void 0);
71
+ tslib_1.__decorate([
72
+ (0, type_graphql_1.Field)(() => [AssistantMessageInput], {
73
+ description: 'Conversation, newest last. For a persisted session only the last user message is appended; the rest is read from storage.'
74
+ }),
75
+ tslib_1.__metadata("design:type", Array)
76
+ ], AssistantChatInput.prototype, "messages", void 0);
77
+ tslib_1.__decorate([
78
+ (0, type_graphql_1.Field)({
79
+ description: 'Who the assistant is on this surface, in the words of the product that owns the screen. The rules for using each tool come from whoever registered it and are appended by the server.'
80
+ }),
81
+ tslib_1.__metadata("design:type", String)
82
+ ], AssistantChatInput.prototype, "systemPrompt", void 0);
83
+ tslib_1.__decorate([
84
+ (0, type_graphql_1.Field)(() => [String], {
85
+ nullable: true,
86
+ description: 'Registered tool categories this conversation may use (registerToolCategory names, e.g. ["figure-authoring"]). Omit to allow every registered category. An empty list allows none — a plain conversation.'
87
+ }),
88
+ tslib_1.__metadata("design:type", Array)
89
+ ], AssistantChatInput.prototype, "toolCategories", void 0);
90
+ tslib_1.__decorate([
91
+ (0, type_graphql_1.Field)(() => graphql_scalars_1.GraphQLJSON, {
92
+ nullable: true,
93
+ description: 'What this surface is looking at (e.g. { figureId } while modelling). Tools read it so the model never has to ask the user for identifiers. Client-supplied — never used for authorization; a tool verifies the scope belongs to the caller tenant.'
94
+ }),
95
+ tslib_1.__metadata("design:type", Object)
96
+ ], AssistantChatInput.prototype, "hostContext", void 0);
97
+ tslib_1.__decorate([
98
+ (0, type_graphql_1.Field)({
99
+ nullable: true,
100
+ description: 'Last message to keep from the stored history. The client keeps its own truncation when a user edits a message, so pass the last kept id to hide the retracted span from the model.'
101
+ }),
102
+ tslib_1.__metadata("design:type", String)
103
+ ], AssistantChatInput.prototype, "truncateAfterMessageId", void 0);
104
+ tslib_1.__decorate([
105
+ (0, type_graphql_1.Field)({
106
+ nullable: true,
107
+ description: 'Require a tool call before the model may answer. For surfaces that report live state, where an unchecked assertion reads the same as a checked one.'
108
+ }),
109
+ tslib_1.__metadata("design:type", Boolean)
110
+ ], AssistantChatInput.prototype, "requireGroundingTools", void 0);
111
+ tslib_1.__decorate([
112
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Model identifier override. Falls back to the configured default.' }),
113
+ tslib_1.__metadata("design:type", String)
114
+ ], AssistantChatInput.prototype, "model", void 0);
115
+ tslib_1.__decorate([
116
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Sampling temperature override.' }),
117
+ tslib_1.__metadata("design:type", Number)
118
+ ], AssistantChatInput.prototype, "temperature", void 0);
119
+ tslib_1.__decorate([
120
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Max output tokens per call.' }),
121
+ tslib_1.__metadata("design:type", Number)
122
+ ], AssistantChatInput.prototype, "maxTokens", void 0);
123
+ AssistantChatInput = tslib_1.__decorate([
124
+ (0, type_graphql_1.InputType)({ description: 'One turn of a conversation that has no document of its own.' })
125
+ ], AssistantChatInput);
126
+ let AssistantChatOutput = class AssistantChatOutput {
127
+ };
128
+ tslib_1.__decorate([
129
+ (0, type_graphql_1.Field)({ description: 'The reply, including the notice when the loop stopped early.' }),
130
+ tslib_1.__metadata("design:type", String)
131
+ ], AssistantChatOutput.prototype, "reply", void 0);
132
+ tslib_1.__decorate([
133
+ (0, type_graphql_1.Field)({ description: 'AI client identifier (provider:model).' }),
134
+ tslib_1.__metadata("design:type", String)
135
+ ], AssistantChatOutput.prototype, "clientId", void 0);
136
+ tslib_1.__decorate([
137
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Echo of the session id, when persisted.' }),
138
+ tslib_1.__metadata("design:type", String)
139
+ ], AssistantChatOutput.prototype, "sessionId", void 0);
140
+ tslib_1.__decorate([
141
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Stored ChatMessage id of the user input.' }),
142
+ tslib_1.__metadata("design:type", String)
143
+ ], AssistantChatOutput.prototype, "userMessageId", void 0);
144
+ tslib_1.__decorate([
145
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Stored ChatMessage id of the reply.' }),
146
+ tslib_1.__metadata("design:type", String)
147
+ ], AssistantChatOutput.prototype, "assistantMessageId", void 0);
148
+ tslib_1.__decorate([
149
+ (0, type_graphql_1.Field)(() => graphql_scalars_1.GraphQLJSON, {
150
+ nullable: true,
151
+ description: 'Tool usages during the turn — {name, arguments, result, kind}. The surface shows them foldably.'
152
+ }),
153
+ tslib_1.__metadata("design:type", Object)
154
+ ], AssistantChatOutput.prototype, "toolUsages", void 0);
155
+ tslib_1.__decorate([
156
+ (0, type_graphql_1.Field)(() => [String], {
157
+ nullable: true,
158
+ description: 'Tool names offered to the model this turn. A surface that expected a category and sees it missing here knows the registration did not happen, rather than reading a vague answer as a refusal.'
159
+ }),
160
+ tslib_1.__metadata("design:type", Array)
161
+ ], AssistantChatOutput.prototype, "offeredTools", void 0);
162
+ tslib_1.__decorate([
163
+ (0, type_graphql_1.Field)(() => graphql_scalars_1.GraphQLJSON, {
164
+ nullable: true,
165
+ description: 'Actions a tool recorded without executing — each carries what the tool returned alongside `proposed: true`. The surface renders a confirm button; executing is the user\'s act.'
166
+ }),
167
+ tslib_1.__metadata("design:type", Object)
168
+ ], AssistantChatOutput.prototype, "proposals", void 0);
169
+ tslib_1.__decorate([
170
+ (0, type_graphql_1.Field)(() => graphql_scalars_1.GraphQLJSON, {
171
+ nullable: true,
172
+ description: 'Identifiers in the reply that appear in nothing the model received. Hallucination candidates surfaced to the user; the reply itself is not altered. Null when the answer is grounded.'
173
+ }),
174
+ tslib_1.__metadata("design:type", Object)
175
+ ], AssistantChatOutput.prototype, "groundingWarnings", void 0);
176
+ AssistantChatOutput = tslib_1.__decorate([
177
+ (0, type_graphql_1.ObjectType)()
178
+ ], AssistantChatOutput);
179
+ function resolveAIClient() {
180
+ const existing = (0, ai_client_base_1.getDefaultAIClient)();
181
+ if (existing)
182
+ return existing;
183
+ const cfg = env_1.config.get('aiClient', null);
184
+ if (cfg && cfg.provider)
185
+ return (0, ai_client_base_1.createAIClient)(cfg);
186
+ return undefined;
187
+ }
188
+ let AssistantChatResolver = class AssistantChatResolver {
189
+ /*
190
+ * No `@transaction`, for the same reason the board conversation has none: a model call takes
191
+ * seconds to tens of seconds, and holding a write lock for that long collides with everything
192
+ * else on SQLite. Writes are short and around the call. For a conversation, a partly stored
193
+ * history is safer than a lost one.
194
+ */
195
+ async assistantChat(input, context) {
196
+ const client = resolveAIClient();
197
+ if (!client) {
198
+ throw new Error('No AI client configured. Set config.aiClient = { provider, model, apiKey } or call setDefaultAIClient(...).');
199
+ }
200
+ const { domain, user } = context.state;
201
+ const sessionRepo = (0, shell_1.getRepository)(chat_session_js_1.ChatSession);
202
+ const messageRepo = (0, shell_1.getRepository)(chat_message_js_1.ChatMessage);
203
+ let session;
204
+ if (input.sessionId) {
205
+ const found = await sessionRepo.findOneBy({ id: input.sessionId, domain: { id: domain.id } });
206
+ if (!found)
207
+ throw new Error(`ChatSession ${input.sessionId} not found`);
208
+ session = found;
209
+ }
210
+ const lastUserMessage = input.messages[input.messages.length - 1];
211
+ let userMessageId;
212
+ if (session && lastUserMessage?.role === 'user') {
213
+ const previousLast = await messageRepo.findOne({
214
+ where: { session: { id: session.id } },
215
+ order: { createdAt: 'DESC' }
216
+ });
217
+ const saved = await messageRepo.save({
218
+ session: { id: session.id },
219
+ role: 'user',
220
+ content: lastUserMessage.content,
221
+ creator: user,
222
+ updater: user,
223
+ parentMessage: previousLast ? { id: previousLast.id } : undefined
224
+ });
225
+ userMessageId = saved.id;
226
+ /*
227
+ * Session activity is denormalised for the list: most-recent ordering and another
228
+ * participant's unread badge read it. Best effort — a conversation must not fail because a
229
+ * list sorts one position late.
230
+ */
231
+ try {
232
+ const patch = (0, session_inbox_js_1.activityPatch)(lastUserMessage.content);
233
+ if (!session.name)
234
+ patch.name = (0, session_inbox_js_1.autoSessionName)(lastUserMessage.content);
235
+ await sessionRepo.update(session.id, patch);
236
+ }
237
+ catch {
238
+ /* noop */
239
+ }
240
+ (0, chat_message_publish_js_1.publishChatMessage)({ domainId: domain.id, sessionId: session.id, message: saved });
241
+ (0, session_activity_publish_js_1.publishSessionActivity)({
242
+ domainId: domain.id,
243
+ sessionId: session.id,
244
+ kind: 'message',
245
+ anchorType: session.anchorType,
246
+ anchorId: session.anchorId,
247
+ preview: lastUserMessage.content,
248
+ actorId: user?.id
249
+ });
250
+ }
251
+ const conversation = session
252
+ ? (0, llm_history_js_1.buildLlmHistory)(await messageRepo.find({
253
+ where: { session: { id: session.id } },
254
+ order: { createdAt: 'ASC' },
255
+ relations: ['creator']
256
+ }), {
257
+ maxTurns: LLM_HISTORY_MAX_TURNS,
258
+ truncateAfterMessageId: input.truncateAfterMessageId,
259
+ /* The message just stored is this turn's question — truncation must never drop it. */
260
+ keepMessageIds: userMessageId ? [userMessageId] : undefined,
261
+ summary: session.lastSummary
262
+ ? { text: session.lastSummary, upToMessageId: session.summaryUpToMessageId }
263
+ : undefined
264
+ })
265
+ : input.messages.map(message => ({ role: message.role, content: message.content }));
266
+ const result = await (0, ai_client_base_1.registryAssistantChat)({
267
+ client,
268
+ messages: conversation,
269
+ systemPrompt: input.systemPrompt,
270
+ toolCategories: input.toolCategories,
271
+ hostContext: input.hostContext,
272
+ state: context.state,
273
+ model: input.model,
274
+ temperature: input.temperature,
275
+ maxTokens: input.maxTokens,
276
+ requireToolOnFirstTurn: input.requireGroundingTools
277
+ });
278
+ let assistantMessageId;
279
+ if (session) {
280
+ const stored = await messageRepo.save({
281
+ session: { id: session.id },
282
+ role: 'assistant',
283
+ content: result.reply,
284
+ toolUsages: result.loop.toolUsages?.length ? JSON.stringify(result.loop.toolUsages) : undefined,
285
+ groundingWarnings: result.groundingWarnings?.length ? JSON.stringify(result.groundingWarnings) : undefined,
286
+ creator: user,
287
+ updater: user,
288
+ parentMessage: userMessageId ? { id: userMessageId } : undefined
289
+ });
290
+ assistantMessageId = stored.id;
291
+ (0, chat_message_publish_js_1.publishChatMessage)({ domainId: domain.id, sessionId: session.id, message: stored });
292
+ /* The reply is session activity too. Leaving it out shows other participants "nothing new". */
293
+ (0, session_activity_publish_js_1.publishSessionActivity)({
294
+ domainId: domain.id,
295
+ sessionId: session.id,
296
+ kind: 'message',
297
+ anchorType: session.anchorType,
298
+ anchorId: session.anchorId,
299
+ preview: result.reply,
300
+ actorId: user?.id
301
+ });
302
+ await sessionRepo.update(session.id, {
303
+ aiClientId: client.id,
304
+ updater: user,
305
+ ...(0, session_inbox_js_1.activityPatch)(result.reply)
306
+ });
307
+ }
308
+ return {
309
+ reply: result.reply,
310
+ clientId: client.id,
311
+ sessionId: session?.id,
312
+ userMessageId,
313
+ assistantMessageId,
314
+ toolUsages: result.loop.toolUsages?.length ? result.loop.toolUsages : null,
315
+ offeredTools: result.offeredTools,
316
+ proposals: result.loop.proposals?.length ? result.loop.proposals : null,
317
+ groundingWarnings: result.groundingWarnings?.length ? result.groundingWarnings : null
318
+ };
319
+ }
320
+ };
321
+ exports.AssistantChatResolver = AssistantChatResolver;
322
+ tslib_1.__decorate([
323
+ (0, type_graphql_1.Mutation)(() => AssistantChatOutput, {
324
+ description: 'Ask the assistant a question on a surface that has no document of its own. Tools come from the registry; with a sessionId the conversation is persisted.'
325
+ })
326
+ /*
327
+ * `ai-assistant:mutation` — the same door as the board conversation's, which moved here on
328
+ * 2026-09-10. No `legacy` alias: this operation is new, so no one was ever passing under an old
329
+ * name, and declaring an alias would open a second name that nothing needs.
330
+ */
331
+ ,
332
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation")'),
333
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('input')),
334
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
335
+ tslib_1.__metadata("design:type", Function),
336
+ tslib_1.__metadata("design:paramtypes", [AssistantChatInput, Object]),
337
+ tslib_1.__metadata("design:returntype", Promise)
338
+ ], AssistantChatResolver.prototype, "assistantChat", null);
339
+ exports.AssistantChatResolver = AssistantChatResolver = tslib_1.__decorate([
340
+ (0, type_graphql_1.Resolver)()
341
+ ], AssistantChatResolver);
342
+ //# sourceMappingURL=assistant-chat-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assistant-chat-resolver.js","sourceRoot":"","sources":["../../server/service/assistant-chat-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The neutral door into a conversation.\n *\n * ── Why this exists (2026-09-11) ───────────────────────────────────────────────\n * The chat component moved here from `board-ai`, but the only mutation it could call stayed\n * behind. operato-figure mounted the dock, registered four tools, and got\n * `Cannot query field \"boardAIChat\" on type \"Mutation\"` — it does not load board-ai. The twin has\n * the same shape and it does not show: its space deliberation runs through board-ai's door purely\n * because the board authoring half of the same product already loaded that package.\n *\n * ── Why the board resolver was not moved ───────────────────────────────────────\n * `board-ai-resolver.ts` is 692 lines and the larger part of it is board authoring itself:\n * `currentBoard` as a BoardModel, PatchEntry, board patch broadcasts, refid injection for\n * component mentions. Moving that here would teach this package the board document, which is the\n * dependency we just cut, pointing the other way.\n *\n * So the door moved and the editor did not. `boardAIChat` stays in board-ai for board authoring;\n * this one is for a surface that asks questions and runs registered tools.\n *\n * ── What this is not ───────────────────────────────────────────────────────────\n * It has no document, so it takes no document and returns no patch. Tools that write to a\n * document are not offered here — `registryAssistantChat` enforces that, and it decides what may\n * be dispatched from the same list it offers, so the two cannot drift apart.\n */\nimport { Arg, Ctx, Directive, Field, InputType, Mutation, ObjectType, Resolver } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\nimport '@things-factory/auth-base'\n\nimport { config } from '@things-factory/env'\nimport { getRepository } from '@things-factory/shell'\nimport { createAIClient, getDefaultAIClient, registryAssistantChat, type AIClient } from '@things-factory/ai-client-base'\n\nimport { ChatSession } from './chat-session/chat-session.js'\nimport { ChatMessage } from './chat-message/chat-message.js'\nimport { buildLlmHistory } from './chat-message/llm-history.js'\nimport { publishChatMessage } from './chat-message/chat-message-publish.js'\nimport { publishSessionActivity } from './chat-session/session-activity-publish.js'\nimport { activityPatch, autoSessionName } from './chat-session/session-inbox.js'\n\n/**\n * How many stored messages reach the model.\n *\n * A deliberation runs for days. Without a cap the prompt grows without bound and the oldest turns\n * bury the recent ones. Kept the same as the board conversation's cap so the two surfaces of one\n * product do not remember differently.\n */\nconst LLM_HISTORY_MAX_TURNS = 40\n\n@InputType()\nclass AssistantMessageInput {\n @Field({ description: \"Role: 'user' or 'assistant'\" })\n role: string\n\n @Field({ description: 'Message content' })\n content: string\n}\n\n@InputType({ description: 'One turn of a conversation that has no document of its own.' })\nclass AssistantChatInput {\n @Field({\n nullable: true,\n description: 'ChatSession id. Omit for a one-off question with no persistence.'\n })\n sessionId?: string\n\n @Field(() => [AssistantMessageInput], {\n description:\n 'Conversation, newest last. For a persisted session only the last user message is appended; the rest is read from storage.'\n })\n messages: AssistantMessageInput[]\n\n @Field({\n description:\n 'Who the assistant is on this surface, in the words of the product that owns the screen. The rules for using each tool come from whoever registered it and are appended by the server.'\n })\n systemPrompt: string\n\n @Field(() => [String], {\n nullable: true,\n description:\n 'Registered tool categories this conversation may use (registerToolCategory names, e.g. [\"figure-authoring\"]). Omit to allow every registered category. An empty list allows none — a plain conversation.'\n })\n toolCategories?: string[]\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'What this surface is looking at (e.g. { figureId } while modelling). Tools read it so the model never has to ask the user for identifiers. Client-supplied — never used for authorization; a tool verifies the scope belongs to the caller tenant.'\n })\n hostContext?: any\n\n @Field({\n nullable: true,\n description:\n 'Last message to keep from the stored history. The client keeps its own truncation when a user edits a message, so pass the last kept id to hide the retracted span from the model.'\n })\n truncateAfterMessageId?: string\n\n @Field({\n nullable: true,\n description:\n 'Require a tool call before the model may answer. For surfaces that report live state, where an unchecked assertion reads the same as a checked one.'\n })\n requireGroundingTools?: boolean\n\n @Field({ nullable: true, description: 'Model identifier override. Falls back to the configured default.' })\n model?: string\n\n @Field({ nullable: true, description: 'Sampling temperature override.' })\n temperature?: number\n\n @Field({ nullable: true, description: 'Max output tokens per call.' })\n maxTokens?: number\n}\n\n@ObjectType()\nclass AssistantChatOutput {\n @Field({ description: 'The reply, including the notice when the loop stopped early.' })\n reply: string\n\n @Field({ description: 'AI client identifier (provider:model).' })\n clientId: string\n\n @Field({ nullable: true, description: 'Echo of the session id, when persisted.' })\n sessionId?: string\n\n @Field({ nullable: true, description: 'Stored ChatMessage id of the user input.' })\n userMessageId?: string\n\n @Field({ nullable: true, description: 'Stored ChatMessage id of the reply.' })\n assistantMessageId?: string\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description: 'Tool usages during the turn — {name, arguments, result, kind}. The surface shows them foldably.'\n })\n toolUsages?: any\n\n @Field(() => [String], {\n nullable: true,\n description:\n 'Tool names offered to the model this turn. A surface that expected a category and sees it missing here knows the registration did not happen, rather than reading a vague answer as a refusal.'\n })\n offeredTools?: string[]\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Actions a tool recorded without executing — each carries what the tool returned alongside `proposed: true`. The surface renders a confirm button; executing is the user\\'s act.'\n })\n proposals?: any\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Identifiers in the reply that appear in nothing the model received. Hallucination candidates surfaced to the user; the reply itself is not altered. Null when the answer is grounded.'\n })\n groundingWarnings?: any\n}\n\nfunction resolveAIClient(): AIClient | undefined {\n const existing = getDefaultAIClient()\n if (existing) return existing\n const cfg = config.get('aiClient', null) as any\n if (cfg && cfg.provider) return createAIClient(cfg)\n return undefined\n}\n\n@Resolver()\nexport class AssistantChatResolver {\n /*\n * No `@transaction`, for the same reason the board conversation has none: a model call takes\n * seconds to tens of seconds, and holding a write lock for that long collides with everything\n * else on SQLite. Writes are short and around the call. For a conversation, a partly stored\n * history is safer than a lost one.\n */\n @Mutation(() => AssistantChatOutput, {\n description:\n 'Ask the assistant a question on a surface that has no document of its own. Tools come from the registry; with a sessionId the conversation is persisted.'\n })\n /*\n * `ai-assistant:mutation` — the same door as the board conversation's, which moved here on\n * 2026-09-10. No `legacy` alias: this operation is new, so no one was ever passing under an old\n * name, and declaring an alias would open a second name that nothing needs.\n */\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"mutation\")')\n async assistantChat(@Arg('input') input: AssistantChatInput, @Ctx() context: any): Promise<AssistantChatOutput> {\n const client = resolveAIClient()\n if (!client) {\n throw new Error(\n 'No AI client configured. Set config.aiClient = { provider, model, apiKey } or call setDefaultAIClient(...).'\n )\n }\n\n const { domain, user } = context.state\n const sessionRepo = getRepository(ChatSession)\n const messageRepo = getRepository(ChatMessage)\n\n let session: ChatSession | undefined\n if (input.sessionId) {\n const found = await sessionRepo.findOneBy({ id: input.sessionId, domain: { id: domain.id } as any })\n if (!found) throw new Error(`ChatSession ${input.sessionId} not found`)\n session = found\n }\n\n const lastUserMessage = input.messages[input.messages.length - 1]\n let userMessageId: string | undefined\n\n if (session && lastUserMessage?.role === 'user') {\n const previousLast = await messageRepo.findOne({\n where: { session: { id: session.id } as any },\n order: { createdAt: 'DESC' }\n })\n const saved = await messageRepo.save({\n session: { id: session.id } as any,\n role: 'user',\n content: lastUserMessage.content,\n creator: user,\n updater: user,\n parentMessage: previousLast ? ({ id: previousLast.id } as any) : undefined\n })\n userMessageId = saved.id\n\n /*\n * Session activity is denormalised for the list: most-recent ordering and another\n * participant's unread badge read it. Best effort — a conversation must not fail because a\n * list sorts one position late.\n */\n try {\n const patch: any = activityPatch(lastUserMessage.content)\n if (!session.name) patch.name = autoSessionName(lastUserMessage.content)\n await sessionRepo.update(session.id!, patch)\n } catch {\n /* noop */\n }\n\n publishChatMessage({ domainId: domain.id, sessionId: session.id, message: saved } as any)\n publishSessionActivity({\n domainId: domain.id,\n sessionId: session.id!,\n kind: 'message',\n anchorType: (session as any).anchorType,\n anchorId: (session as any).anchorId,\n preview: lastUserMessage.content,\n actorId: user?.id\n })\n }\n\n const conversation = session\n ? buildLlmHistory(\n await messageRepo.find({\n where: { session: { id: session.id } as any },\n order: { createdAt: 'ASC' },\n relations: ['creator']\n }),\n {\n maxTurns: LLM_HISTORY_MAX_TURNS,\n truncateAfterMessageId: input.truncateAfterMessageId,\n /* The message just stored is this turn's question — truncation must never drop it. */\n keepMessageIds: userMessageId ? [userMessageId] : undefined,\n summary: (session as any).lastSummary\n ? { text: (session as any).lastSummary, upToMessageId: (session as any).summaryUpToMessageId }\n : undefined\n }\n )\n : input.messages.map(message => ({ role: message.role as any, content: message.content }))\n\n const result = await registryAssistantChat({\n client,\n messages: conversation as any,\n systemPrompt: input.systemPrompt,\n toolCategories: input.toolCategories,\n hostContext: input.hostContext,\n state: context.state,\n model: input.model,\n temperature: input.temperature,\n maxTokens: input.maxTokens,\n requireToolOnFirstTurn: input.requireGroundingTools\n })\n\n let assistantMessageId: string | undefined\n if (session) {\n const stored = await messageRepo.save({\n session: { id: session.id } as any,\n role: 'assistant',\n content: result.reply,\n toolUsages: result.loop.toolUsages?.length ? JSON.stringify(result.loop.toolUsages) : undefined,\n groundingWarnings: result.groundingWarnings?.length ? JSON.stringify(result.groundingWarnings) : undefined,\n creator: user,\n updater: user,\n parentMessage: userMessageId ? ({ id: userMessageId } as any) : undefined\n })\n assistantMessageId = stored.id\n\n publishChatMessage({ domainId: domain.id, sessionId: session.id, message: stored } as any)\n /* The reply is session activity too. Leaving it out shows other participants \"nothing new\". */\n publishSessionActivity({\n domainId: domain.id,\n sessionId: session.id!,\n kind: 'message',\n anchorType: (session as any).anchorType,\n anchorId: (session as any).anchorId,\n preview: result.reply,\n actorId: user?.id\n })\n\n await sessionRepo.update(session.id!, {\n aiClientId: client.id,\n updater: user,\n ...activityPatch(result.reply)\n } as any)\n }\n\n return {\n reply: result.reply,\n clientId: client.id,\n sessionId: session?.id,\n userMessageId,\n assistantMessageId,\n toolUsages: result.loop.toolUsages?.length ? result.loop.toolUsages : null,\n offeredTools: result.offeredTools,\n proposals: result.loop.proposals?.length ? result.loop.proposals : null,\n groundingWarnings: result.groundingWarnings?.length ? result.groundingWarnings : null\n }\n }\n}\n"]}
@@ -480,7 +480,7 @@ let ChatSessionResolver = class ChatSessionResolver {
480
480
  exports.ChatSessionResolver = ChatSessionResolver;
481
481
  tslib_1.__decorate([
482
482
  (0, type_graphql_1.Query)(() => chat_session_js_1.ChatSession, { nullable: true, description: 'Get AI chat session by id.' }),
483
- (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "query", legacy: "board-ai")'),
483
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "query")'),
484
484
  tslib_1.__param(0, (0, type_graphql_1.Arg)('id')),
485
485
  tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
486
486
  tslib_1.__metadata("design:type", Function),
@@ -491,7 +491,7 @@ tslib_1.__decorate([
491
491
  (0, type_graphql_1.Query)(() => [chat_session_js_1.ChatSession], {
492
492
  description: 'List AI chat sessions anchored to a target (oldest first). Anchor decides the nature and the history boundary of the conversation: board = authoring, space = the site and the twins running there. Board anchor also matches legacy rows that only have boardId.'
493
493
  }),
494
- (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "query", legacy: "board-ai")'),
494
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "query")'),
495
495
  tslib_1.__param(0, (0, type_graphql_1.Arg)('anchorType')),
496
496
  tslib_1.__param(1, (0, type_graphql_1.Arg)('anchorId')),
497
497
  tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
@@ -503,7 +503,7 @@ tslib_1.__decorate([
503
503
  (0, type_graphql_1.Query)(() => [ChatSessionInboxEntry], {
504
504
  description: "List sessions anchored to a target with this viewer's unread state, most recent activity first. Supports free-text search over name and last preview, a mine-only filter, hidden inclusion, and paging. Uses the denormalized last-activity timestamp compared against the viewer's read point — no per-session aggregation."
505
505
  }),
506
- (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "query", legacy: "board-ai")'),
506
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "query")'),
507
507
  tslib_1.__param(0, (0, type_graphql_1.Arg)('anchorType')),
508
508
  tslib_1.__param(1, (0, type_graphql_1.Arg)('anchorId')),
509
509
  tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
@@ -520,7 +520,7 @@ tslib_1.__decorate([
520
520
  (0, type_graphql_1.Query)(() => type_graphql_1.Int, {
521
521
  description: 'Total number of sessions matching the same filters as chatSessionInbox (before paging). Request it alongside the list so the UI can tell "end of page" from "end of data".'
522
522
  }),
523
- (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "query", legacy: "board-ai")'),
523
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "query")'),
524
524
  tslib_1.__param(0, (0, type_graphql_1.Arg)('anchorType')),
525
525
  tslib_1.__param(1, (0, type_graphql_1.Arg)('anchorId')),
526
526
  tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
@@ -535,7 +535,7 @@ tslib_1.__decorate([
535
535
  (0, type_graphql_1.Query)(() => type_graphql_1.Int, {
536
536
  description: "Number of sessions on this anchor that the caller has hidden and that have had no activity since. Meant to be requested alongside chatSessionInbox in the same operation so the filter can say how many are folded away."
537
537
  }),
538
- (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "query", legacy: "board-ai")'),
538
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "query")'),
539
539
  tslib_1.__param(0, (0, type_graphql_1.Arg)('anchorType')),
540
540
  tslib_1.__param(1, (0, type_graphql_1.Arg)('anchorId')),
541
541
  tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
@@ -547,7 +547,7 @@ tslib_1.__decorate([
547
547
  (0, type_graphql_1.Mutation)(() => Boolean, {
548
548
  description: 'Delete a chat session — creator only, since a shared deliberation cannot be un-deleted from the UI. Participants who are not the creator can hide it from their own list instead (hideAISession). Soft delete; broadcast as meta activity so every list drops it.'
549
549
  }),
550
- (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation", legacy: "board-ai")'),
550
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation")'),
551
551
  tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
552
552
  tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
553
553
  tslib_1.__metadata("design:type", Function),
@@ -558,7 +558,7 @@ tslib_1.__decorate([
558
558
  (0, type_graphql_1.Mutation)(() => Boolean, {
559
559
  description: 'Hide (or unhide) a session from the calling user\'s own list. Per-user and reversible — the session and other participants are unaffected. Activity newer than the hide makes it reappear; the inbox filter (showHidden) reveals hidden ones.'
560
560
  }),
561
- (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation", legacy: "board-ai")'),
561
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation")'),
562
562
  tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
563
563
  tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
564
564
  tslib_1.__param(2, (0, type_graphql_1.Arg)('hidden', { nullable: true })),
@@ -570,7 +570,7 @@ tslib_1.__decorate([
570
570
  (0, type_graphql_1.Mutation)(() => Boolean, {
571
571
  description: 'Rename a chat session. Any participant may rename it — the name is the topic label of a shared deliberation and the change is reversible. Broadcast as meta activity so every participant sees it without raising unread badges.'
572
572
  }),
573
- (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation", legacy: "board-ai")'),
573
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation")'),
574
574
  tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
575
575
  tslib_1.__param(1, (0, type_graphql_1.Arg)('name')),
576
576
  tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
@@ -582,7 +582,7 @@ tslib_1.__decorate([
582
582
  (0, type_graphql_1.Mutation)(() => Boolean, {
583
583
  description: "Mark a session as read up to now for the current user. Idempotent. Distinct from presence (lastSeenAt) — a window can be open without being read."
584
584
  }),
585
- (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation", legacy: "board-ai")'),
585
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation")'),
586
586
  tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
587
587
  tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
588
588
  tslib_1.__metadata("design:type", Function),
@@ -593,7 +593,7 @@ tslib_1.__decorate([
593
593
  (0, type_graphql_1.Query)(() => [auth_base_1.User], {
594
594
  description: 'List users in the current domain for `@` mention popup. board-ai 권한이면 누구나 멘션용 검색 가능 (관리자 전용 users() 와 별도).'
595
595
  }),
596
- (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "query", legacy: "board-ai")'),
596
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "query")'),
597
597
  tslib_1.__param(0, (0, type_graphql_1.Arg)('query', { nullable: true, description: 'Substring to match against name/email (case-insensitive). Empty → all.' })),
598
598
  tslib_1.__param(1, (0, type_graphql_1.Arg)('limit', () => type_graphql_1.Int, { nullable: true, defaultValue: 50 })),
599
599
  tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
@@ -605,7 +605,7 @@ tslib_1.__decorate([
605
605
  (0, type_graphql_1.Query)(() => [chat_session_participant_js_1.ChatSessionParticipant], {
606
606
  description: 'List participants of a ChatSession (members / owner).'
607
607
  }),
608
- (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "query", legacy: "board-ai")'),
608
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "query")'),
609
609
  tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
610
610
  tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
611
611
  tslib_1.__metadata("design:type", Function),
@@ -616,7 +616,7 @@ tslib_1.__decorate([
616
616
  (0, type_graphql_1.Mutation)(() => Boolean, {
617
617
  description: 'Presence heartbeat — bump current user\'s lastSeenAt in the session (online indicator).'
618
618
  }),
619
- (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation", legacy: "board-ai")'),
619
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation")'),
620
620
  tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
621
621
  tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
622
622
  tslib_1.__metadata("design:type", Function),
@@ -625,7 +625,7 @@ tslib_1.__decorate([
625
625
  ], ChatSessionResolver.prototype, "touchPresence", null);
626
626
  tslib_1.__decorate([
627
627
  (0, type_graphql_1.Query)(() => [chat_message_js_1.ChatMessage], { description: 'List chat messages of a session, oldest first.' }),
628
- (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "query", legacy: "board-ai")'),
628
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "query")'),
629
629
  tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
630
630
  tslib_1.__param(1, (0, type_graphql_1.Arg)('limit', () => type_graphql_1.Int, { nullable: true, defaultValue: 100 })),
631
631
  tslib_1.__param(2, (0, type_graphql_1.Arg)('offset', () => type_graphql_1.Int, { nullable: true, defaultValue: 0 })),
@@ -639,7 +639,7 @@ tslib_1.__decorate([
639
639
  (0, type_graphql_1.Mutation)(() => chat_session_js_1.ChatSession, {
640
640
  description: 'Start (or get existing) AI chat session anchored to a target. Idempotent — returns first existing or creates one. Use anchorType board for board authoring, space for a site (its co-located twins).'
641
641
  }),
642
- (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation", legacy: "board-ai")'),
642
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation")'),
643
643
  tslib_1.__param(0, (0, type_graphql_1.Arg)('anchorType')),
644
644
  tslib_1.__param(1, (0, type_graphql_1.Arg)('anchorId')),
645
645
  tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
@@ -652,7 +652,7 @@ tslib_1.__decorate([
652
652
  (0, type_graphql_1.Mutation)(() => chat_session_js_1.ChatSession, {
653
653
  description: 'Always create a new AI chat session anchored to a target (no idempotent reuse). For multi-session UX — 새 탭 열기.'
654
654
  }),
655
- (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation", legacy: "board-ai")'),
655
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation")'),
656
656
  tslib_1.__param(0, (0, type_graphql_1.Arg)('anchorType')),
657
657
  tslib_1.__param(1, (0, type_graphql_1.Arg)('anchorId')),
658
658
  tslib_1.__param(2, (0, type_graphql_1.Arg)('name', { nullable: true, description: 'Optional name (defaults to auto-generated `세션 N`).' })),
@@ -666,7 +666,7 @@ tslib_1.__decorate([
666
666
  (0, type_graphql_1.Mutation)(() => Boolean, {
667
667
  description: 'Rename a ChatSession (tab label).'
668
668
  }),
669
- (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation", legacy: "board-ai")'),
669
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation")'),
670
670
  tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
671
671
  tslib_1.__param(1, (0, type_graphql_1.Arg)('name')),
672
672
  tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
@@ -679,7 +679,7 @@ tslib_1.__decorate([
679
679
  (0, type_graphql_1.Mutation)(() => Boolean, {
680
680
  description: 'Append a system note to a session — context that did not come from a participant typing (e.g. a conversation carried over from another surface, an external state change). The AI sees it as part of the shared history.'
681
681
  }),
682
- (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation", legacy: "board-ai")'),
682
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation")'),
683
683
  tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
684
684
  tslib_1.__param(1, (0, type_graphql_1.Arg)('content')),
685
685
  tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
@@ -692,7 +692,7 @@ tslib_1.__decorate([
692
692
  (0, type_graphql_1.Mutation)(() => Boolean, {
693
693
  description: 'Carry a conversation from another surface into this session as its own turns (user / assistant), preserving order. Use when a lightweight exchange is promoted into a persistent discussion — the participants should read it as the conversation continuing, not as a machine dump.'
694
694
  }),
695
- (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation", legacy: "board-ai")'),
695
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation")'),
696
696
  tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
697
697
  tslib_1.__param(1, (0, type_graphql_1.Arg)('turns', () => graphql_scalars_1.GraphQLJSON)),
698
698
  tslib_1.__param(2, (0, type_graphql_1.Ctx)()),