@open-mercato/ai-assistant 0.6.2-develop.3461.1.605f31c2c9 → 0.6.2

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 (63) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +2 -2
  3. package/dist/modules/ai_assistant/acl.js +1 -0
  4. package/dist/modules/ai_assistant/acl.js.map +2 -2
  5. package/dist/modules/ai_assistant/api/ai/chat/route.js +197 -2
  6. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
  7. package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js +272 -0
  8. package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js.map +7 -0
  9. package/dist/modules/ai_assistant/api/ai/conversations/import/route.js +108 -0
  10. package/dist/modules/ai_assistant/api/ai/conversations/import/route.js.map +7 -0
  11. package/dist/modules/ai_assistant/api/ai/conversations/route.js +207 -0
  12. package/dist/modules/ai_assistant/api/ai/conversations/route.js.map +7 -0
  13. package/dist/modules/ai_assistant/data/entities/AiChatConversation.js +5 -0
  14. package/dist/modules/ai_assistant/data/entities/AiChatConversation.js.map +7 -0
  15. package/dist/modules/ai_assistant/data/entities/AiChatConversationParticipant.js +5 -0
  16. package/dist/modules/ai_assistant/data/entities/AiChatConversationParticipant.js.map +7 -0
  17. package/dist/modules/ai_assistant/data/entities/AiChatMessage.js +5 -0
  18. package/dist/modules/ai_assistant/data/entities/AiChatMessage.js.map +7 -0
  19. package/dist/modules/ai_assistant/data/entities.js +200 -0
  20. package/dist/modules/ai_assistant/data/entities.js.map +2 -2
  21. package/dist/modules/ai_assistant/data/repositories/AiChatConversationRepository.js +448 -0
  22. package/dist/modules/ai_assistant/data/repositories/AiChatConversationRepository.js.map +7 -0
  23. package/dist/modules/ai_assistant/data/validators.js +72 -0
  24. package/dist/modules/ai_assistant/data/validators.js.map +7 -0
  25. package/dist/modules/ai_assistant/i18n/de.json +3 -0
  26. package/dist/modules/ai_assistant/i18n/en.json +3 -0
  27. package/dist/modules/ai_assistant/i18n/es.json +3 -0
  28. package/dist/modules/ai_assistant/i18n/pl.json +3 -0
  29. package/dist/modules/ai_assistant/lib/conversation-storage.js +43 -0
  30. package/dist/modules/ai_assistant/lib/conversation-storage.js.map +7 -0
  31. package/dist/modules/ai_assistant/migrations/Migration20260518092853_ai_assistant.js +28 -0
  32. package/dist/modules/ai_assistant/migrations/Migration20260518092853_ai_assistant.js.map +7 -0
  33. package/dist/modules/ai_assistant/setup.js +1 -0
  34. package/dist/modules/ai_assistant/setup.js.map +2 -2
  35. package/generated/entities/ai_chat_conversation/index.ts +15 -0
  36. package/generated/entities/ai_chat_conversation_participant/index.ts +9 -0
  37. package/generated/entities/ai_chat_message/index.ts +16 -0
  38. package/generated/entities.ids.generated.ts +4 -1
  39. package/generated/entity-fields-registry.ts +46 -0
  40. package/jest.config.cjs +3 -1
  41. package/package.json +14 -15
  42. package/src/modules/ai_assistant/acl.ts +1 -0
  43. package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +107 -0
  44. package/src/modules/ai_assistant/api/ai/chat/route.ts +245 -1
  45. package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/route.ts +320 -0
  46. package/src/modules/ai_assistant/api/ai/conversations/__tests__/route.test.ts +93 -0
  47. package/src/modules/ai_assistant/api/ai/conversations/import/route.ts +122 -0
  48. package/src/modules/ai_assistant/api/ai/conversations/route.ts +241 -0
  49. package/src/modules/ai_assistant/data/entities/AiChatConversation.ts +2 -0
  50. package/src/modules/ai_assistant/data/entities/AiChatConversationParticipant.ts +2 -0
  51. package/src/modules/ai_assistant/data/entities/AiChatMessage.ts +2 -0
  52. package/src/modules/ai_assistant/data/entities.ts +255 -0
  53. package/src/modules/ai_assistant/data/repositories/AiChatConversationRepository.ts +597 -0
  54. package/src/modules/ai_assistant/data/repositories/__tests__/AiChatConversationRepository.test.ts +592 -0
  55. package/src/modules/ai_assistant/data/validators.ts +134 -0
  56. package/src/modules/ai_assistant/i18n/de.json +3 -0
  57. package/src/modules/ai_assistant/i18n/en.json +3 -0
  58. package/src/modules/ai_assistant/i18n/es.json +3 -0
  59. package/src/modules/ai_assistant/i18n/pl.json +3 -0
  60. package/src/modules/ai_assistant/lib/conversation-storage.ts +93 -0
  61. package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +822 -0
  62. package/src/modules/ai_assistant/migrations/Migration20260518092853_ai_assistant.ts +39 -0
  63. package/src/modules/ai_assistant/setup.ts +1 -0
@@ -176,6 +176,7 @@
176
176
  "ai_assistant.chat.dock.close": "Cerrar el panel de IA",
177
177
  "ai_assistant.chat.dock.collapse": "Contraer el panel de IA",
178
178
  "ai_assistant.chat.dock.expand": "Ampliar el panel de IA",
179
+ "ai_assistant.chat.downloadFile": "Descargar {name}",
179
180
  "ai_assistant.chat.emptyTranscript": "Aún no hay mensajes. Pregúntele al agente cualquier cosa para empezar.",
180
181
  "ai_assistant.chat.errorTitle": "Error al enviar la solicitud al agente",
181
182
  "ai_assistant.chat.example.create": "Crear un producto nuevo",
@@ -183,6 +184,7 @@
183
184
  "ai_assistant.chat.example.show": "Mostrar pedidos recientes",
184
185
  "ai_assistant.chat.idleExamples": "Ejemplos:",
185
186
  "ai_assistant.chat.idleTitle": "Pregunte lo que necesite o describa lo que desea hacer.",
187
+ "ai_assistant.chat.imagePreviewDialogDescription": "Vista previa de imagen",
186
188
  "ai_assistant.chat.mutation_cards.confirmation.cancel": "Cancelar",
187
189
  "ai_assistant.chat.mutation_cards.confirmation.defaultSummary": "Aplicando los cambios solicitados...",
188
190
  "ai_assistant.chat.mutation_cards.confirmation.errorTitle": "La confirmación falló",
@@ -273,6 +275,7 @@
273
275
  "ai_assistant.chat.uiPartPending": "Parte de UI pendiente:",
274
276
  "ai_assistant.chat.userRoleLabel": "Usted",
275
277
  "ai_assistant.chat.welcomeTitle": "How can I help?",
278
+ "ai_assistant.chat.zoomImage": "Abrir vista previa de {name}",
276
279
  "ai_assistant.dock.bottom": "Anclar abajo",
277
280
  "ai_assistant.dock.close": "Cerrar",
278
281
  "ai_assistant.dock.floating": "Flotante",
@@ -176,6 +176,7 @@
176
176
  "ai_assistant.chat.dock.close": "Zamknij panel AI",
177
177
  "ai_assistant.chat.dock.collapse": "Zwiń panel AI",
178
178
  "ai_assistant.chat.dock.expand": "Rozwiń panel AI",
179
+ "ai_assistant.chat.downloadFile": "Pobierz {name}",
179
180
  "ai_assistant.chat.emptyTranscript": "Brak wiadomości. Zadaj agentowi dowolne pytanie, aby rozpocząć.",
180
181
  "ai_assistant.chat.errorTitle": "Nie udało się wysłać żądania do agenta",
181
182
  "ai_assistant.chat.example.create": "Utwórz nowy produkt",
@@ -183,6 +184,7 @@
183
184
  "ai_assistant.chat.example.show": "Pokaż ostatnie zamówienia",
184
185
  "ai_assistant.chat.idleExamples": "Przykłady:",
185
186
  "ai_assistant.chat.idleTitle": "Zapytaj mnie o cokolwiek lub opisz, co chcesz zrobić.",
187
+ "ai_assistant.chat.imagePreviewDialogDescription": "Podgląd obrazu",
186
188
  "ai_assistant.chat.mutation_cards.confirmation.cancel": "Anuluj",
187
189
  "ai_assistant.chat.mutation_cards.confirmation.defaultSummary": "Wdrażanie żądanych zmian...",
188
190
  "ai_assistant.chat.mutation_cards.confirmation.errorTitle": "Potwierdzenie nie powiodło się",
@@ -273,6 +275,7 @@
273
275
  "ai_assistant.chat.uiPartPending": "Oczekująca część UI:",
274
276
  "ai_assistant.chat.userRoleLabel": "Ty",
275
277
  "ai_assistant.chat.welcomeTitle": "How can I help?",
278
+ "ai_assistant.chat.zoomImage": "Otwórz podgląd {name}",
276
279
  "ai_assistant.dock.bottom": "Dokuj na dole",
277
280
  "ai_assistant.dock.close": "Zamknij",
278
281
  "ai_assistant.dock.floating": "Pływające",
@@ -0,0 +1,43 @@
1
+ import {
2
+ AiChatConversationAccessError,
3
+ AiChatConversationRepository
4
+ } from "../data/repositories/AiChatConversationRepository.js";
5
+ function createConversationStorage(container) {
6
+ const em = container.resolve("em");
7
+ return new AiChatConversationRepository(em);
8
+ }
9
+ function serializeAiChatConversation(row) {
10
+ return {
11
+ conversationId: row.conversationId,
12
+ agentId: row.agentId,
13
+ title: row.title ?? null,
14
+ status: row.status,
15
+ visibility: row.visibility,
16
+ pageContext: row.pageContext ?? null,
17
+ createdAt: row.createdAt.toISOString(),
18
+ updatedAt: row.updatedAt.toISOString(),
19
+ lastMessageAt: row.lastMessageAt ? row.lastMessageAt.toISOString() : null,
20
+ importedFromLocalAt: row.importedFromLocalAt ? row.importedFromLocalAt.toISOString() : null
21
+ };
22
+ }
23
+ function serializeAiChatMessage(row) {
24
+ return {
25
+ id: row.id,
26
+ clientMessageId: row.clientMessageId ?? null,
27
+ role: row.role,
28
+ content: row.content,
29
+ uiParts: Array.isArray(row.uiParts) ? row.uiParts : [],
30
+ attachmentIds: Array.isArray(row.attachmentIds) ? row.attachmentIds : [],
31
+ files: Array.isArray(row.filesMetadata) ? row.filesMetadata : [],
32
+ model: row.model ?? null,
33
+ metadata: row.metadata ?? null,
34
+ createdAt: row.createdAt.toISOString()
35
+ };
36
+ }
37
+ export {
38
+ AiChatConversationAccessError,
39
+ createConversationStorage,
40
+ serializeAiChatConversation,
41
+ serializeAiChatMessage
42
+ };
43
+ //# sourceMappingURL=conversation-storage.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/ai_assistant/lib/conversation-storage.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport {\n AiChatConversation,\n AiChatMessage,\n} from '../data/entities'\nimport {\n AiChatConversationAccessError,\n AiChatConversationRepository,\n type AiChatConversationContext,\n} from '../data/repositories/AiChatConversationRepository'\n\n/**\n * Thin service-layer wrapper that resolves the entity manager from the\n * Awilix container and exposes a typed API on top of\n * `AiChatConversationRepository`. The REST routes for the conversation APIs\n * call into this surface; the future chat dispatcher write path will reuse\n * the same helpers so persistence stays consistent across entry points.\n *\n * Spec: `2026-05-05-ai-chat-server-side-conversation-storage` \u00A7\"Commands\".\n *\n * Re-exports the access error so route handlers can map it to a 404 without\n * importing the repository directly.\n */\nexport { AiChatConversationAccessError }\nexport type { AiChatConversationContext }\n\nexport function createConversationStorage(\n container: AwilixContainer,\n): AiChatConversationRepository {\n const em = container.resolve<EntityManager>('em')\n return new AiChatConversationRepository(em)\n}\n\nexport interface SerializedAiChatConversation {\n conversationId: string\n agentId: string\n title: string | null\n status: 'open' | 'closed'\n visibility: 'private' | 'shared' | 'organization'\n pageContext: Record<string, unknown> | null\n createdAt: string\n updatedAt: string\n lastMessageAt: string | null\n importedFromLocalAt: string | null\n}\n\nexport interface SerializedAiChatMessage {\n id: string\n clientMessageId: string | null\n role: 'user' | 'assistant' | 'system'\n content: string\n uiParts: unknown[]\n attachmentIds: string[]\n files: Array<Record<string, unknown>>\n model: string | null\n metadata: Record<string, unknown> | null\n createdAt: string\n}\n\nexport function serializeAiChatConversation(\n row: AiChatConversation,\n): SerializedAiChatConversation {\n return {\n conversationId: row.conversationId,\n agentId: row.agentId,\n title: row.title ?? null,\n status: row.status,\n visibility: row.visibility,\n pageContext: row.pageContext ?? null,\n createdAt: row.createdAt.toISOString(),\n updatedAt: row.updatedAt.toISOString(),\n lastMessageAt: row.lastMessageAt ? row.lastMessageAt.toISOString() : null,\n importedFromLocalAt: row.importedFromLocalAt\n ? row.importedFromLocalAt.toISOString()\n : null,\n }\n}\n\nexport function serializeAiChatMessage(row: AiChatMessage): SerializedAiChatMessage {\n return {\n id: row.id,\n clientMessageId: row.clientMessageId ?? null,\n role: row.role,\n content: row.content,\n uiParts: Array.isArray(row.uiParts) ? row.uiParts : [],\n attachmentIds: Array.isArray(row.attachmentIds) ? row.attachmentIds : [],\n files: Array.isArray(row.filesMetadata) ? row.filesMetadata : [],\n model: row.model ?? null,\n metadata: row.metadata ?? null,\n createdAt: row.createdAt.toISOString(),\n }\n}\n"],
5
+ "mappings": "AAMA;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AAiBA,SAAS,0BACd,WAC8B;AAC9B,QAAM,KAAK,UAAU,QAAuB,IAAI;AAChD,SAAO,IAAI,6BAA6B,EAAE;AAC5C;AA4BO,SAAS,4BACd,KAC8B;AAC9B,SAAO;AAAA,IACL,gBAAgB,IAAI;AAAA,IACpB,SAAS,IAAI;AAAA,IACb,OAAO,IAAI,SAAS;AAAA,IACpB,QAAQ,IAAI;AAAA,IACZ,YAAY,IAAI;AAAA,IAChB,aAAa,IAAI,eAAe;AAAA,IAChC,WAAW,IAAI,UAAU,YAAY;AAAA,IACrC,WAAW,IAAI,UAAU,YAAY;AAAA,IACrC,eAAe,IAAI,gBAAgB,IAAI,cAAc,YAAY,IAAI;AAAA,IACrE,qBAAqB,IAAI,sBACrB,IAAI,oBAAoB,YAAY,IACpC;AAAA,EACN;AACF;AAEO,SAAS,uBAAuB,KAA6C;AAClF,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,iBAAiB,IAAI,mBAAmB;AAAA,IACxC,MAAM,IAAI;AAAA,IACV,SAAS,IAAI;AAAA,IACb,SAAS,MAAM,QAAQ,IAAI,OAAO,IAAI,IAAI,UAAU,CAAC;AAAA,IACrD,eAAe,MAAM,QAAQ,IAAI,aAAa,IAAI,IAAI,gBAAgB,CAAC;AAAA,IACvE,OAAO,MAAM,QAAQ,IAAI,aAAa,IAAI,IAAI,gBAAgB,CAAC;AAAA,IAC/D,OAAO,IAAI,SAAS;AAAA,IACpB,UAAU,IAAI,YAAY;AAAA,IAC1B,WAAW,IAAI,UAAU,YAAY;AAAA,EACvC;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,28 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ class Migration20260518092853_ai_assistant extends Migration {
3
+ async up() {
4
+ this.addSql(`create table "ai_chat_conversations" ("id" uuid not null default gen_random_uuid(), "tenant_id" uuid not null, "organization_id" uuid null, "conversation_id" text not null, "agent_id" text not null, "owner_user_id" uuid not null, "title" text null, "status" text not null default 'open', "visibility" text not null default 'private', "page_context" jsonb null, "last_message_at" timestamptz null, "imported_from_local_at" timestamptz null, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, primary key ("id"));`);
5
+ this.addSql(`create index "ai_chat_conversations_tenant_org_deleted_idx" on "ai_chat_conversations" ("tenant_id", "organization_id", "deleted_at");`);
6
+ this.addSql(`create index "ai_chat_conversations_tenant_org_owner_agent_idx" on "ai_chat_conversations" ("tenant_id", "organization_id", "owner_user_id", "agent_id", "status", "last_message_at");`);
7
+ this.addSql(`create unique index "ai_chat_conversations_tenant_conv_null_org_uq" on "ai_chat_conversations" ("tenant_id", "conversation_id") where "organization_id" is null and "deleted_at" is null;`);
8
+ this.addSql(`create unique index "ai_chat_conversations_tenant_org_conv_uq" on "ai_chat_conversations" ("tenant_id", "organization_id", "conversation_id") where "organization_id" is not null and "deleted_at" is null;`);
9
+ this.addSql(`create table "ai_chat_conversation_participants" ("id" uuid not null default gen_random_uuid(), "tenant_id" uuid not null, "organization_id" uuid null, "conversation_id" text not null, "user_id" uuid not null, "role" text not null default 'owner', "last_read_at" timestamptz null, "created_at" timestamptz not null, "updated_at" timestamptz not null, primary key ("id"));`);
10
+ this.addSql(`create index "ai_chat_conv_participants_tenant_org_user_conv_idx" on "ai_chat_conversation_participants" ("tenant_id", "organization_id", "user_id", "conversation_id");`);
11
+ this.addSql(`create unique index "ai_chat_conv_participants_tenant_conv_user_null_org_uq" on "ai_chat_conversation_participants" ("tenant_id", "conversation_id", "user_id") where "organization_id" is null;`);
12
+ this.addSql(`create unique index "ai_chat_conv_participants_tenant_org_conv_user_uq" on "ai_chat_conversation_participants" ("tenant_id", "organization_id", "conversation_id", "user_id") where "organization_id" is not null;`);
13
+ this.addSql(`create table "ai_chat_messages" ("id" uuid not null default gen_random_uuid(), "tenant_id" uuid not null, "organization_id" uuid null, "conversation_id" text not null, "client_message_id" text null, "role" text not null, "content" text not null, "ui_parts" jsonb null, "attachment_ids" jsonb null, "files_metadata" jsonb null, "model" text null, "metadata" jsonb null, "created_by_user_id" uuid null, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, primary key ("id"));`);
14
+ this.addSql(`create index "ai_chat_messages_tenant_org_deleted_idx" on "ai_chat_messages" ("tenant_id", "organization_id", "deleted_at");`);
15
+ this.addSql(`create index "ai_chat_messages_tenant_org_conv_created_idx" on "ai_chat_messages" ("tenant_id", "organization_id", "conversation_id", "created_at");`);
16
+ this.addSql(`create unique index "ai_chat_messages_tenant_conv_client_id_null_org_uq" on "ai_chat_messages" ("tenant_id", "conversation_id", "client_message_id") where "organization_id" is null and "client_message_id" is not null and "deleted_at" is null;`);
17
+ this.addSql(`create unique index "ai_chat_messages_tenant_org_conv_client_id_uq" on "ai_chat_messages" ("tenant_id", "organization_id", "conversation_id", "client_message_id") where "organization_id" is not null and "client_message_id" is not null and "deleted_at" is null;`);
18
+ }
19
+ async down() {
20
+ this.addSql(`drop table if exists "ai_chat_messages" cascade;`);
21
+ this.addSql(`drop table if exists "ai_chat_conversation_participants" cascade;`);
22
+ this.addSql(`drop table if exists "ai_chat_conversations" cascade;`);
23
+ }
24
+ }
25
+ export {
26
+ Migration20260518092853_ai_assistant
27
+ };
28
+ //# sourceMappingURL=Migration20260518092853_ai_assistant.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/ai_assistant/migrations/Migration20260518092853_ai_assistant.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\n/**\n * Server-side AI chat conversation storage.\n *\n * Adds the three additive tables introduced by spec\n * `2026-05-05-ai-chat-server-side-conversation-storage`:\n * - `ai_chat_conversations`: durable session record per `(tenant, org, owner)`.\n * - `ai_chat_conversation_participants`: per-user membership, prepares for sharing.\n * - `ai_chat_messages`: append-only transcript with idempotent `client_message_id`.\n */\nexport class Migration20260518092853_ai_assistant extends Migration {\n\n override async up(): Promise<void> {\n this.addSql(`create table \"ai_chat_conversations\" (\"id\" uuid not null default gen_random_uuid(), \"tenant_id\" uuid not null, \"organization_id\" uuid null, \"conversation_id\" text not null, \"agent_id\" text not null, \"owner_user_id\" uuid not null, \"title\" text null, \"status\" text not null default 'open', \"visibility\" text not null default 'private', \"page_context\" jsonb null, \"last_message_at\" timestamptz null, \"imported_from_local_at\" timestamptz null, \"created_at\" timestamptz not null, \"updated_at\" timestamptz not null, \"deleted_at\" timestamptz null, primary key (\"id\"));`);\n this.addSql(`create index \"ai_chat_conversations_tenant_org_deleted_idx\" on \"ai_chat_conversations\" (\"tenant_id\", \"organization_id\", \"deleted_at\");`);\n this.addSql(`create index \"ai_chat_conversations_tenant_org_owner_agent_idx\" on \"ai_chat_conversations\" (\"tenant_id\", \"organization_id\", \"owner_user_id\", \"agent_id\", \"status\", \"last_message_at\");`);\n this.addSql(`create unique index \"ai_chat_conversations_tenant_conv_null_org_uq\" on \"ai_chat_conversations\" (\"tenant_id\", \"conversation_id\") where \"organization_id\" is null and \"deleted_at\" is null;`);\n this.addSql(`create unique index \"ai_chat_conversations_tenant_org_conv_uq\" on \"ai_chat_conversations\" (\"tenant_id\", \"organization_id\", \"conversation_id\") where \"organization_id\" is not null and \"deleted_at\" is null;`);\n\n this.addSql(`create table \"ai_chat_conversation_participants\" (\"id\" uuid not null default gen_random_uuid(), \"tenant_id\" uuid not null, \"organization_id\" uuid null, \"conversation_id\" text not null, \"user_id\" uuid not null, \"role\" text not null default 'owner', \"last_read_at\" timestamptz null, \"created_at\" timestamptz not null, \"updated_at\" timestamptz not null, primary key (\"id\"));`);\n this.addSql(`create index \"ai_chat_conv_participants_tenant_org_user_conv_idx\" on \"ai_chat_conversation_participants\" (\"tenant_id\", \"organization_id\", \"user_id\", \"conversation_id\");`);\n this.addSql(`create unique index \"ai_chat_conv_participants_tenant_conv_user_null_org_uq\" on \"ai_chat_conversation_participants\" (\"tenant_id\", \"conversation_id\", \"user_id\") where \"organization_id\" is null;`);\n this.addSql(`create unique index \"ai_chat_conv_participants_tenant_org_conv_user_uq\" on \"ai_chat_conversation_participants\" (\"tenant_id\", \"organization_id\", \"conversation_id\", \"user_id\") where \"organization_id\" is not null;`);\n\n this.addSql(`create table \"ai_chat_messages\" (\"id\" uuid not null default gen_random_uuid(), \"tenant_id\" uuid not null, \"organization_id\" uuid null, \"conversation_id\" text not null, \"client_message_id\" text null, \"role\" text not null, \"content\" text not null, \"ui_parts\" jsonb null, \"attachment_ids\" jsonb null, \"files_metadata\" jsonb null, \"model\" text null, \"metadata\" jsonb null, \"created_by_user_id\" uuid null, \"created_at\" timestamptz not null, \"updated_at\" timestamptz not null, \"deleted_at\" timestamptz null, primary key (\"id\"));`);\n this.addSql(`create index \"ai_chat_messages_tenant_org_deleted_idx\" on \"ai_chat_messages\" (\"tenant_id\", \"organization_id\", \"deleted_at\");`);\n this.addSql(`create index \"ai_chat_messages_tenant_org_conv_created_idx\" on \"ai_chat_messages\" (\"tenant_id\", \"organization_id\", \"conversation_id\", \"created_at\");`);\n this.addSql(`create unique index \"ai_chat_messages_tenant_conv_client_id_null_org_uq\" on \"ai_chat_messages\" (\"tenant_id\", \"conversation_id\", \"client_message_id\") where \"organization_id\" is null and \"client_message_id\" is not null and \"deleted_at\" is null;`);\n this.addSql(`create unique index \"ai_chat_messages_tenant_org_conv_client_id_uq\" on \"ai_chat_messages\" (\"tenant_id\", \"organization_id\", \"conversation_id\", \"client_message_id\") where \"organization_id\" is not null and \"client_message_id\" is not null and \"deleted_at\" is null;`);\n }\n\n override async down(): Promise<void> {\n this.addSql(`drop table if exists \"ai_chat_messages\" cascade;`);\n this.addSql(`drop table if exists \"ai_chat_conversation_participants\" cascade;`);\n this.addSql(`drop table if exists \"ai_chat_conversations\" cascade;`);\n }\n\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAWnB,MAAM,6CAA6C,UAAU;AAAA,EAElE,MAAe,KAAoB;AACjC,SAAK,OAAO,mjBAAmjB;AAC/jB,SAAK,OAAO,wIAAwI;AACpJ,SAAK,OAAO,wLAAwL;AACpM,SAAK,OAAO,2LAA2L;AACvM,SAAK,OAAO,6MAA6M;AAEzN,SAAK,OAAO,qXAAqX;AACjY,SAAK,OAAO,0KAA0K;AACtL,SAAK,OAAO,kMAAkM;AAC9M,SAAK,OAAO,oNAAoN;AAEhO,SAAK,OAAO,4gBAA4gB;AACxhB,SAAK,OAAO,8HAA8H;AAC1I,SAAK,OAAO,sJAAsJ;AAClK,SAAK,OAAO,oPAAoP;AAChQ,SAAK,OAAO,sQAAsQ;AAAA,EACpR;AAAA,EAEA,MAAe,OAAsB;AACnC,SAAK,OAAO,kDAAkD;AAC9D,SAAK,OAAO,mEAAmE;AAC/E,SAAK,OAAO,uDAAuD;AAAA,EACrE;AAEF;",
6
+ "names": []
7
+ }
@@ -69,6 +69,7 @@ const setup = {
69
69
  admin: [
70
70
  "ai_assistant.view",
71
71
  "ai_assistant.settings.manage",
72
+ "ai_assistant.conversations.manage",
72
73
  "ai_assistant.mcp.serve",
73
74
  "ai_assistant.tools.list",
74
75
  "ai_assistant.mcp_servers.view",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/ai_assistant/setup.ts"],
4
- "sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\n\nconst PENDING_ACTION_CLEANUP_SCHEDULE_ID = 'ai_assistant:pending-action-cleanup'\nconst TOKEN_USAGE_PRUNE_SCHEDULE_ID = 'ai_assistant:token-usage-prune'\n\n/**\n * System-scoped recurring schedule: every 5 minutes, enqueue a job to the\n * `ai-pending-action-cleanup` queue so the worker can sweep rows whose TTL\n * elapsed without any confirm/cancel activity (Step 5.12). The schedule id\n * is stable and `scheduler.register()` is an upsert, so calling this from\n * every tenant bootstrap stays idempotent.\n */\nasync function ensurePendingActionCleanupSchedule(\n container: import('awilix').AwilixContainer | undefined,\n): Promise<void> {\n if (!container) return\n let schedulerService:\n | {\n register: (registration: Record<string, unknown>) => Promise<void>\n }\n | undefined\n try {\n schedulerService = container.resolve('schedulerService')\n } catch {\n schedulerService = undefined\n }\n if (!schedulerService) return\n try {\n await schedulerService.register({\n id: PENDING_ACTION_CLEANUP_SCHEDULE_ID,\n name: 'AI pending-action cleanup',\n description:\n 'Sweep pending AI mutation approvals whose TTL elapsed without confirm/cancel and flip them to expired.',\n scopeType: 'system',\n scheduleType: 'interval',\n scheduleValue: '5m',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'ai-pending-action-cleanup',\n targetPayload: {},\n sourceType: 'module',\n sourceModule: 'ai_assistant',\n isEnabled: true,\n })\n } catch (error) {\n console.warn(\n '[ai_assistant] Failed to register pending-action cleanup schedule:',\n error instanceof Error ? error.message : error,\n )\n }\n}\n\n/**\n * System-scoped daily schedule: enqueue a job to the `ai-token-usage-prune`\n * queue to prune events older than the retention window and reconcile the\n * daily rollup session counts.\n *\n * Phase 6.4 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\nasync function ensureTokenUsagePruneSchedule(\n container: import('awilix').AwilixContainer | undefined,\n): Promise<void> {\n if (!container) return\n let schedulerService:\n | {\n register: (registration: Record<string, unknown>) => Promise<void>\n }\n | undefined\n try {\n schedulerService = container.resolve('schedulerService')\n } catch {\n schedulerService = undefined\n }\n if (!schedulerService) return\n try {\n await schedulerService.register({\n id: TOKEN_USAGE_PRUNE_SCHEDULE_ID,\n name: 'AI token-usage prune',\n description:\n 'Delete ai_token_usage_events rows older than AI_TOKEN_USAGE_EVENTS_RETENTION_DAYS (default 90) and reconcile session_count on the daily rollup.',\n scopeType: 'system',\n scheduleType: 'interval',\n scheduleValue: '24h',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'ai-token-usage-prune',\n targetPayload: {},\n sourceType: 'module',\n sourceModule: 'ai_assistant',\n isEnabled: true,\n })\n } catch (error) {\n console.warn(\n '[ai_assistant] Failed to register token-usage prune schedule:',\n error instanceof Error ? error.message : error,\n )\n }\n}\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n admin: [\n 'ai_assistant.view',\n 'ai_assistant.settings.manage',\n 'ai_assistant.mcp.serve',\n 'ai_assistant.tools.list',\n 'ai_assistant.mcp_servers.view',\n 'ai_assistant.mcp_servers.manage',\n ],\n employee: ['ai_assistant.view'],\n },\n\n async seedDefaults({ container }) {\n await ensurePendingActionCleanupSchedule(container)\n await ensureTokenUsagePruneSchedule(container)\n },\n}\n\nexport default setup\n"],
5
- "mappings": "AAEA,MAAM,qCAAqC;AAC3C,MAAM,gCAAgC;AAStC,eAAe,mCACb,WACe;AACf,MAAI,CAAC,UAAW;AAChB,MAAI;AAKJ,MAAI;AACF,uBAAmB,UAAU,QAAQ,kBAAkB;AAAA,EACzD,QAAQ;AACN,uBAAmB;AAAA,EACrB;AACA,MAAI,CAAC,iBAAkB;AACvB,MAAI;AACF,UAAM,iBAAiB,SAAS;AAAA,MAC9B,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,aACE;AAAA,MACF,WAAW;AAAA,MACX,cAAc;AAAA,MACd,eAAe;AAAA,MACf,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,eAAe,CAAC;AAAA,MAChB,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,WAAW;AAAA,IACb,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AAAA,EACF;AACF;AASA,eAAe,8BACb,WACe;AACf,MAAI,CAAC,UAAW;AAChB,MAAI;AAKJ,MAAI;AACF,uBAAmB,UAAU,QAAQ,kBAAkB;AAAA,EACzD,QAAQ;AACN,uBAAmB;AAAA,EACrB;AACA,MAAI,CAAC,iBAAkB;AACvB,MAAI;AACF,UAAM,iBAAiB,SAAS;AAAA,MAC9B,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,aACE;AAAA,MACF,WAAW;AAAA,MACX,cAAc;AAAA,MACd,eAAe;AAAA,MACf,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,eAAe,CAAC;AAAA,MAChB,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,WAAW;AAAA,IACb,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AAAA,EACF;AACF;AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,UAAU,CAAC,mBAAmB;AAAA,EAChC;AAAA,EAEA,MAAM,aAAa,EAAE,UAAU,GAAG;AAChC,UAAM,mCAAmC,SAAS;AAClD,UAAM,8BAA8B,SAAS;AAAA,EAC/C;AACF;AAEA,IAAO,gBAAQ;",
4
+ "sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\n\nconst PENDING_ACTION_CLEANUP_SCHEDULE_ID = 'ai_assistant:pending-action-cleanup'\nconst TOKEN_USAGE_PRUNE_SCHEDULE_ID = 'ai_assistant:token-usage-prune'\n\n/**\n * System-scoped recurring schedule: every 5 minutes, enqueue a job to the\n * `ai-pending-action-cleanup` queue so the worker can sweep rows whose TTL\n * elapsed without any confirm/cancel activity (Step 5.12). The schedule id\n * is stable and `scheduler.register()` is an upsert, so calling this from\n * every tenant bootstrap stays idempotent.\n */\nasync function ensurePendingActionCleanupSchedule(\n container: import('awilix').AwilixContainer | undefined,\n): Promise<void> {\n if (!container) return\n let schedulerService:\n | {\n register: (registration: Record<string, unknown>) => Promise<void>\n }\n | undefined\n try {\n schedulerService = container.resolve('schedulerService')\n } catch {\n schedulerService = undefined\n }\n if (!schedulerService) return\n try {\n await schedulerService.register({\n id: PENDING_ACTION_CLEANUP_SCHEDULE_ID,\n name: 'AI pending-action cleanup',\n description:\n 'Sweep pending AI mutation approvals whose TTL elapsed without confirm/cancel and flip them to expired.',\n scopeType: 'system',\n scheduleType: 'interval',\n scheduleValue: '5m',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'ai-pending-action-cleanup',\n targetPayload: {},\n sourceType: 'module',\n sourceModule: 'ai_assistant',\n isEnabled: true,\n })\n } catch (error) {\n console.warn(\n '[ai_assistant] Failed to register pending-action cleanup schedule:',\n error instanceof Error ? error.message : error,\n )\n }\n}\n\n/**\n * System-scoped daily schedule: enqueue a job to the `ai-token-usage-prune`\n * queue to prune events older than the retention window and reconcile the\n * daily rollup session counts.\n *\n * Phase 6.4 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\nasync function ensureTokenUsagePruneSchedule(\n container: import('awilix').AwilixContainer | undefined,\n): Promise<void> {\n if (!container) return\n let schedulerService:\n | {\n register: (registration: Record<string, unknown>) => Promise<void>\n }\n | undefined\n try {\n schedulerService = container.resolve('schedulerService')\n } catch {\n schedulerService = undefined\n }\n if (!schedulerService) return\n try {\n await schedulerService.register({\n id: TOKEN_USAGE_PRUNE_SCHEDULE_ID,\n name: 'AI token-usage prune',\n description:\n 'Delete ai_token_usage_events rows older than AI_TOKEN_USAGE_EVENTS_RETENTION_DAYS (default 90) and reconcile session_count on the daily rollup.',\n scopeType: 'system',\n scheduleType: 'interval',\n scheduleValue: '24h',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'ai-token-usage-prune',\n targetPayload: {},\n sourceType: 'module',\n sourceModule: 'ai_assistant',\n isEnabled: true,\n })\n } catch (error) {\n console.warn(\n '[ai_assistant] Failed to register token-usage prune schedule:',\n error instanceof Error ? error.message : error,\n )\n }\n}\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n admin: [\n 'ai_assistant.view',\n 'ai_assistant.settings.manage',\n 'ai_assistant.conversations.manage',\n 'ai_assistant.mcp.serve',\n 'ai_assistant.tools.list',\n 'ai_assistant.mcp_servers.view',\n 'ai_assistant.mcp_servers.manage',\n ],\n employee: ['ai_assistant.view'],\n },\n\n async seedDefaults({ container }) {\n await ensurePendingActionCleanupSchedule(container)\n await ensureTokenUsagePruneSchedule(container)\n },\n}\n\nexport default setup\n"],
5
+ "mappings": "AAEA,MAAM,qCAAqC;AAC3C,MAAM,gCAAgC;AAStC,eAAe,mCACb,WACe;AACf,MAAI,CAAC,UAAW;AAChB,MAAI;AAKJ,MAAI;AACF,uBAAmB,UAAU,QAAQ,kBAAkB;AAAA,EACzD,QAAQ;AACN,uBAAmB;AAAA,EACrB;AACA,MAAI,CAAC,iBAAkB;AACvB,MAAI;AACF,UAAM,iBAAiB,SAAS;AAAA,MAC9B,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,aACE;AAAA,MACF,WAAW;AAAA,MACX,cAAc;AAAA,MACd,eAAe;AAAA,MACf,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,eAAe,CAAC;AAAA,MAChB,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,WAAW;AAAA,IACb,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AAAA,EACF;AACF;AASA,eAAe,8BACb,WACe;AACf,MAAI,CAAC,UAAW;AAChB,MAAI;AAKJ,MAAI;AACF,uBAAmB,UAAU,QAAQ,kBAAkB;AAAA,EACzD,QAAQ;AACN,uBAAmB;AAAA,EACrB;AACA,MAAI,CAAC,iBAAkB;AACvB,MAAI;AACF,UAAM,iBAAiB,SAAS;AAAA,MAC9B,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,aACE;AAAA,MACF,WAAW;AAAA,MACX,cAAc;AAAA,MACd,eAAe;AAAA,MACf,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,eAAe,CAAC;AAAA,MAChB,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,WAAW;AAAA,IACb,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AAAA,EACF;AACF;AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,UAAU,CAAC,mBAAmB;AAAA,EAChC;AAAA,EAEA,MAAM,aAAa,EAAE,UAAU,GAAG;AAChC,UAAM,mCAAmC,SAAS;AAClD,UAAM,8BAA8B,SAAS;AAAA,EAC/C;AACF;AAEA,IAAO,gBAAQ;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,15 @@
1
+ export const id = "id";
2
+ export const tenant_id = "tenant_id";
3
+ export const organization_id = "organization_id";
4
+ export const conversation_id = "conversation_id";
5
+ export const agent_id = "agent_id";
6
+ export const owner_user_id = "owner_user_id";
7
+ export const title = "title";
8
+ export const status = "status";
9
+ export const visibility = "visibility";
10
+ export const page_context = "page_context";
11
+ export const last_message_at = "last_message_at";
12
+ export const imported_from_local_at = "imported_from_local_at";
13
+ export const created_at = "created_at";
14
+ export const updated_at = "updated_at";
15
+ export const deleted_at = "deleted_at";
@@ -0,0 +1,9 @@
1
+ export const id = "id";
2
+ export const tenant_id = "tenant_id";
3
+ export const organization_id = "organization_id";
4
+ export const conversation_id = "conversation_id";
5
+ export const user_id = "user_id";
6
+ export const role = "role";
7
+ export const last_read_at = "last_read_at";
8
+ export const created_at = "created_at";
9
+ export const updated_at = "updated_at";
@@ -0,0 +1,16 @@
1
+ export const id = "id";
2
+ export const tenant_id = "tenant_id";
3
+ export const organization_id = "organization_id";
4
+ export const conversation_id = "conversation_id";
5
+ export const client_message_id = "client_message_id";
6
+ export const role = "role";
7
+ export const content = "content";
8
+ export const ui_parts = "ui_parts";
9
+ export const attachment_ids = "attachment_ids";
10
+ export const files_metadata = "files_metadata";
11
+ export const model = "model";
12
+ export const metadata = "metadata";
13
+ export const created_by_user_id = "created_by_user_id";
14
+ export const created_at = "created_at";
15
+ export const updated_at = "updated_at";
16
+ export const deleted_at = "deleted_at";
@@ -9,7 +9,10 @@ export const M = {
9
9
  "ai_token_usage_event": "ai_assistant:ai_token_usage_event",
10
10
  "ai_token_usage_daily": "ai_assistant:ai_token_usage_daily",
11
11
  "ai_tenant_model_allowlist": "ai_assistant:ai_tenant_model_allowlist",
12
- "ai_agent_mutation_policy_override": "ai_assistant:ai_agent_mutation_policy_override"
12
+ "ai_agent_mutation_policy_override": "ai_assistant:ai_agent_mutation_policy_override",
13
+ "ai_chat_conversation": "ai_assistant:ai_chat_conversation",
14
+ "ai_chat_conversation_participant": "ai_assistant:ai_chat_conversation_participant",
15
+ "ai_chat_message": "ai_assistant:ai_chat_message"
13
16
  }
14
17
  } as const;
15
18
 
@@ -46,6 +46,52 @@ export const entityFieldsRegistry: Record<string, Record<string, string>> = {
46
46
  "loop_stop_when_json": "loop_stop_when_json",
47
47
  "loop_active_tools_json": "loop_active_tools_json"
48
48
  },
49
+ "ai_chat_conversation": {
50
+ "id": "id",
51
+ "tenant_id": "tenant_id",
52
+ "organization_id": "organization_id",
53
+ "conversation_id": "conversation_id",
54
+ "agent_id": "agent_id",
55
+ "owner_user_id": "owner_user_id",
56
+ "title": "title",
57
+ "status": "status",
58
+ "visibility": "visibility",
59
+ "page_context": "page_context",
60
+ "last_message_at": "last_message_at",
61
+ "imported_from_local_at": "imported_from_local_at",
62
+ "created_at": "created_at",
63
+ "updated_at": "updated_at",
64
+ "deleted_at": "deleted_at"
65
+ },
66
+ "ai_chat_conversation_participant": {
67
+ "id": "id",
68
+ "tenant_id": "tenant_id",
69
+ "organization_id": "organization_id",
70
+ "conversation_id": "conversation_id",
71
+ "user_id": "user_id",
72
+ "role": "role",
73
+ "last_read_at": "last_read_at",
74
+ "created_at": "created_at",
75
+ "updated_at": "updated_at"
76
+ },
77
+ "ai_chat_message": {
78
+ "id": "id",
79
+ "tenant_id": "tenant_id",
80
+ "organization_id": "organization_id",
81
+ "conversation_id": "conversation_id",
82
+ "client_message_id": "client_message_id",
83
+ "role": "role",
84
+ "content": "content",
85
+ "ui_parts": "ui_parts",
86
+ "attachment_ids": "attachment_ids",
87
+ "files_metadata": "files_metadata",
88
+ "model": "model",
89
+ "metadata": "metadata",
90
+ "created_by_user_id": "created_by_user_id",
91
+ "created_at": "created_at",
92
+ "updated_at": "updated_at",
93
+ "deleted_at": "deleted_at"
94
+ },
49
95
  "ai_pending_action": {
50
96
  "id": "id",
51
97
  "tenant_id": "tenant_id",
package/jest.config.cjs CHANGED
@@ -23,12 +23,14 @@ module.exports = {
23
23
  {
24
24
  tsconfig: {
25
25
  jsx: 'react-jsx',
26
+ rootDir: '.',
27
+ ignoreDeprecations: '6.0',
26
28
  },
27
29
  },
28
30
  ],
29
31
  },
30
32
  transformIgnorePatterns: [
31
- 'node_modules/(?!(@mikro-orm)/)',
33
+ 'node_modules/(?!(@mikro-orm|kysely)/)',
32
34
  ],
33
35
  testMatch: ['<rootDir>/src/**/__tests__/**/*.test.(ts|tsx)'],
34
36
  passWithNoTests: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ai-assistant",
3
- "version": "0.6.2-develop.3461.1.605f31c2c9",
3
+ "version": "0.6.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -85,35 +85,35 @@
85
85
  }
86
86
  },
87
87
  "dependencies": {
88
- "@ai-sdk/anthropic": "^3.0.76",
89
- "@ai-sdk/google": "^3.0.71",
90
- "@ai-sdk/openai": "^3.0.63",
88
+ "@ai-sdk/anthropic": "^3.0.78",
89
+ "@ai-sdk/google": "^3.0.75",
90
+ "@ai-sdk/openai": "^3.0.64",
91
91
  "@modelcontextprotocol/sdk": "^1.29.0",
92
- "ai": "^6.0.177",
92
+ "ai": "^6.0.185",
93
93
  "cmdk": "^1.0.0",
94
- "framer-motion": "^12.38.0",
94
+ "framer-motion": "^12.39.0",
95
95
  "isolated-vm": "^6.1.2",
96
96
  "react-json-view-lite": "^2.5.0",
97
97
  "react-markdown": "^10.1.0",
98
98
  "zod-to-json-schema": "^3.25.2"
99
99
  },
100
100
  "peerDependencies": {
101
- "@open-mercato/shared": "0.6.2-develop.3461.1.605f31c2c9",
102
- "@open-mercato/ui": "0.6.2-develop.3461.1.605f31c2c9",
101
+ "@open-mercato/shared": "0.6.2",
102
+ "@open-mercato/ui": "0.6.2",
103
103
  "react": "^19.0.0",
104
104
  "react-dom": "^19.0.0",
105
105
  "zod": ">=3.23.0"
106
106
  },
107
107
  "devDependencies": {
108
- "@open-mercato/cli": "0.6.2-develop.3461.1.605f31c2c9",
109
- "@open-mercato/shared": "0.6.2-develop.3461.1.605f31c2c9",
110
- "@open-mercato/ui": "0.6.2-develop.3461.1.605f31c2c9",
108
+ "@open-mercato/cli": "0.6.2",
109
+ "@open-mercato/shared": "0.6.2",
110
+ "@open-mercato/ui": "0.6.2",
111
111
  "@types/react": "^19.2.14",
112
112
  "@types/react-dom": "^19.2.3",
113
113
  "react": "19.2.6",
114
114
  "react-dom": "19.2.6",
115
- "tsx": "^4.21.0",
116
- "zod": "^4.3.6"
115
+ "tsx": "^4.22.2",
116
+ "zod": "^4.4.3"
117
117
  },
118
118
  "publishConfig": {
119
119
  "access": "public"
@@ -122,6 +122,5 @@
122
122
  "type": "git",
123
123
  "url": "https://github.com/open-mercato/open-mercato",
124
124
  "directory": "packages/ai-assistant"
125
- },
126
- "stableVersion": "0.6.1"
125
+ }
127
126
  }
@@ -1,6 +1,7 @@
1
1
  export const features = [
2
2
  { id: 'ai_assistant.view', title: 'View AI Assistant Settings', module: 'ai_assistant' },
3
3
  { id: 'ai_assistant.settings.manage', title: 'Manage AI Assistant Settings', module: 'ai_assistant' },
4
+ { id: 'ai_assistant.conversations.manage', title: 'Manage AI Assistant Conversations', module: 'ai_assistant' },
4
5
  { id: 'ai_assistant.mcp.serve', title: 'Start MCP Server', module: 'ai_assistant' },
5
6
  { id: 'ai_assistant.tools.list', title: 'List MCP Tools', module: 'ai_assistant' },
6
7
  { id: 'ai_assistant.mcp_servers.view', title: 'View MCP Server Configurations', module: 'ai_assistant' },
@@ -11,6 +11,8 @@ const authMock = jest.fn()
11
11
  const loadAclMock = jest.fn()
12
12
  const createRequestContainerMock = jest.fn()
13
13
  const runAiAgentTextMock = jest.fn()
14
+ const createOrGetConversationMock = jest.fn()
15
+ const appendConversationMessageMock = jest.fn()
14
16
 
15
17
  jest.mock('@open-mercato/shared/lib/auth/server', () => ({
16
18
  getAuthFromRequest: (...args: unknown[]) => authMock(...args),
@@ -28,6 +30,13 @@ jest.mock('../../../../lib/agent-runtime', () => {
28
30
  }
29
31
  })
30
32
 
33
+ jest.mock('../../../../lib/conversation-storage', () => ({
34
+ createConversationStorage: jest.fn(() => ({
35
+ createOrGet: (...args: unknown[]) => createOrGetConversationMock(...args),
36
+ appendMessage: (...args: unknown[]) => appendConversationMessageMock(...args),
37
+ })),
38
+ }))
39
+
31
40
  const getMock = jest.fn()
32
41
  const listMock = jest.fn()
33
42
 
@@ -128,6 +137,8 @@ describe('POST /api/ai/chat', () => {
128
137
  })
129
138
  tenantAllowlistGetSnapshotMock.mockResolvedValue(null)
130
139
  agentRuntimeOverrideGetExactMock.mockResolvedValue(null)
140
+ createOrGetConversationMock.mockResolvedValue({})
141
+ appendConversationMessageMock.mockResolvedValue({})
131
142
  runAiAgentTextMock.mockResolvedValue(
132
143
  new Response('data: {"type":"text","content":"ok"}\n\ndata: [DONE]\n\n', {
133
144
  status: 200,
@@ -304,6 +315,102 @@ describe('POST /api/ai/chat', () => {
304
315
  expect(callArg.container).toBeDefined()
305
316
  })
306
317
 
318
+ it('persists the user message before dispatch and the assistant message after the stream finishes', async () => {
319
+ seedAgentRegistryForTests([
320
+ makeAgent({
321
+ id: 'customers.assistant',
322
+ moduleId: 'customers',
323
+ }),
324
+ ])
325
+ runAiAgentTextMock.mockResolvedValueOnce(
326
+ new Response(
327
+ [
328
+ 'data: {"type":"text-delta","delta":"Hello"}\n\n',
329
+ 'data: {"type":"text-delta","delta":" world"}\n\n',
330
+ 'data: [DONE]\n\n',
331
+ ].join(''),
332
+ {
333
+ status: 200,
334
+ headers: { 'Content-Type': 'text/event-stream' },
335
+ },
336
+ ),
337
+ )
338
+
339
+ const response = await POST(
340
+ buildRequest({
341
+ agent: 'customers.assistant',
342
+ body: {
343
+ conversationId: 'conv-persist-1',
344
+ attachmentIds: ['att-image-1'],
345
+ messages: [
346
+ {
347
+ id: 'msg-user-1',
348
+ role: 'user',
349
+ content: 'Hello assistant',
350
+ files: [
351
+ {
352
+ id: 'att-image-1',
353
+ name: 'IMG_5328.JPEG',
354
+ type: 'image/jpeg',
355
+ },
356
+ ],
357
+ },
358
+ ],
359
+ },
360
+ }) as any,
361
+ )
362
+ await response.text()
363
+
364
+ expect(createOrGetConversationMock).toHaveBeenCalledWith(
365
+ {
366
+ conversationId: 'conv-persist-1',
367
+ agentId: 'customers.assistant',
368
+ pageContext: null,
369
+ },
370
+ {
371
+ tenantId: 'tenant-1',
372
+ organizationId: 'org-1',
373
+ userId: 'user-1',
374
+ },
375
+ )
376
+ expect(appendConversationMessageMock).toHaveBeenNthCalledWith(
377
+ 1,
378
+ 'conv-persist-1',
379
+ expect.objectContaining({
380
+ clientMessageId: 'msg-user-1',
381
+ role: 'user',
382
+ content: 'Hello assistant',
383
+ attachmentIds: ['att-image-1'],
384
+ files: [
385
+ {
386
+ id: 'att-image-1',
387
+ name: 'IMG_5328.JPEG',
388
+ mimeType: 'image/jpeg',
389
+ },
390
+ ],
391
+ }),
392
+ expect.objectContaining({
393
+ tenantId: 'tenant-1',
394
+ organizationId: 'org-1',
395
+ userId: 'user-1',
396
+ }),
397
+ )
398
+ expect(appendConversationMessageMock).toHaveBeenNthCalledWith(
399
+ 2,
400
+ 'conv-persist-1',
401
+ expect.objectContaining({
402
+ clientMessageId: 'msg-user-1:assistant',
403
+ role: 'assistant',
404
+ content: 'Hello world',
405
+ }),
406
+ expect.objectContaining({
407
+ tenantId: 'tenant-1',
408
+ organizationId: 'org-1',
409
+ userId: 'user-1',
410
+ }),
411
+ )
412
+ })
413
+
307
414
  it('maps AgentPolicyError thrown by the runtime to the canonical HTTP status', async () => {
308
415
  const { AgentPolicyError } = await import('../../../../lib/agent-tools')
309
416
  seedAgentRegistryForTests([