@happyvertical/smrt-chat 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/AGENTS.md +35 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +163 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/chunks/ChatService-Dpzc1Pa5.js +2044 -0
  8. package/dist/chunks/ChatService-Dpzc1Pa5.js.map +1 -0
  9. package/dist/collections/AgentSessionCollection.d.ts +57 -0
  10. package/dist/collections/AgentSessionCollection.d.ts.map +1 -0
  11. package/dist/collections/ChatMessageCollection.d.ts +79 -0
  12. package/dist/collections/ChatMessageCollection.d.ts.map +1 -0
  13. package/dist/collections/ChatParticipantCollection.d.ts +26 -0
  14. package/dist/collections/ChatParticipantCollection.d.ts.map +1 -0
  15. package/dist/collections/ChatReactionCollection.d.ts +23 -0
  16. package/dist/collections/ChatReactionCollection.d.ts.map +1 -0
  17. package/dist/collections/ChatRoomCollection.d.ts +43 -0
  18. package/dist/collections/ChatRoomCollection.d.ts.map +1 -0
  19. package/dist/collections/ChatThreadCollection.d.ts +9 -0
  20. package/dist/collections/ChatThreadCollection.d.ts.map +1 -0
  21. package/dist/index.d.ts +5 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +18 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/internal/agent-runtime.d.ts +16 -0
  26. package/dist/internal/agent-runtime.d.ts.map +1 -0
  27. package/dist/internal/agent-runtime.js +5 -0
  28. package/dist/internal/agent-runtime.js.map +1 -0
  29. package/dist/manifest.json +2811 -0
  30. package/dist/models/AgentSession.d.ts +70 -0
  31. package/dist/models/AgentSession.d.ts.map +1 -0
  32. package/dist/models/ChatMessage.d.ts +55 -0
  33. package/dist/models/ChatMessage.d.ts.map +1 -0
  34. package/dist/models/ChatParticipant.d.ts +32 -0
  35. package/dist/models/ChatParticipant.d.ts.map +1 -0
  36. package/dist/models/ChatReaction.d.ts +19 -0
  37. package/dist/models/ChatReaction.d.ts.map +1 -0
  38. package/dist/models/ChatRoom.d.ts +44 -0
  39. package/dist/models/ChatRoom.d.ts.map +1 -0
  40. package/dist/models/ChatThread.d.ts +24 -0
  41. package/dist/models/ChatThread.d.ts.map +1 -0
  42. package/dist/models/index.d.ts +7 -0
  43. package/dist/models/index.d.ts.map +1 -0
  44. package/dist/playground.d.ts +2 -0
  45. package/dist/playground.d.ts.map +1 -0
  46. package/dist/playground.js +166 -0
  47. package/dist/playground.js.map +1 -0
  48. package/dist/services/ChatService.d.ts +390 -0
  49. package/dist/services/ChatService.d.ts.map +1 -0
  50. package/dist/services/index.d.ts +2 -0
  51. package/dist/services/index.d.ts.map +1 -0
  52. package/dist/smrt-knowledge.json +1507 -0
  53. package/dist/svelte/components/agent/AgentChat.svelte +542 -0
  54. package/dist/svelte/components/agent/AgentChat.svelte.d.ts +21 -0
  55. package/dist/svelte/components/agent/AgentChat.svelte.d.ts.map +1 -0
  56. package/dist/svelte/components/agent/AgentSelector.svelte +175 -0
  57. package/dist/svelte/components/agent/AgentSelector.svelte.d.ts +11 -0
  58. package/dist/svelte/components/agent/AgentSelector.svelte.d.ts.map +1 -0
  59. package/dist/svelte/components/agent/AgentSessionPanel.svelte +322 -0
  60. package/dist/svelte/components/agent/AgentSessionPanel.svelte.d.ts +15 -0
  61. package/dist/svelte/components/agent/AgentSessionPanel.svelte.d.ts.map +1 -0
  62. package/dist/svelte/components/agent/ToolCallDisplay.svelte +335 -0
  63. package/dist/svelte/components/agent/ToolCallDisplay.svelte.d.ts +9 -0
  64. package/dist/svelte/components/agent/ToolCallDisplay.svelte.d.ts.map +1 -0
  65. package/dist/svelte/components/agent/message-blocks.d.ts +12 -0
  66. package/dist/svelte/components/agent/message-blocks.d.ts.map +1 -0
  67. package/dist/svelte/components/agent/message-blocks.js +41 -0
  68. package/dist/svelte/components/agent/message-blocks.test.js +31 -0
  69. package/dist/svelte/components/dialogs/RoomCreateDialog.svelte +403 -0
  70. package/dist/svelte/components/dialogs/RoomCreateDialog.svelte.d.ts +16 -0
  71. package/dist/svelte/components/dialogs/RoomCreateDialog.svelte.d.ts.map +1 -0
  72. package/dist/svelte/components/dialogs/SearchMessages.svelte +457 -0
  73. package/dist/svelte/components/dialogs/SearchMessages.svelte.d.ts +17 -0
  74. package/dist/svelte/components/dialogs/SearchMessages.svelte.d.ts.map +1 -0
  75. package/dist/svelte/components/layout/ChatLayout.svelte +150 -0
  76. package/dist/svelte/components/layout/ChatLayout.svelte.d.ts +18 -0
  77. package/dist/svelte/components/layout/ChatLayout.svelte.d.ts.map +1 -0
  78. package/dist/svelte/components/layout/MemberList.svelte +389 -0
  79. package/dist/svelte/components/layout/MemberList.svelte.d.ts +11 -0
  80. package/dist/svelte/components/layout/MemberList.svelte.d.ts.map +1 -0
  81. package/dist/svelte/components/layout/RoomHeader.svelte +241 -0
  82. package/dist/svelte/components/layout/RoomHeader.svelte.d.ts +15 -0
  83. package/dist/svelte/components/layout/RoomHeader.svelte.d.ts.map +1 -0
  84. package/dist/svelte/components/layout/RoomList.svelte +471 -0
  85. package/dist/svelte/components/layout/RoomList.svelte.d.ts +15 -0
  86. package/dist/svelte/components/layout/RoomList.svelte.d.ts.map +1 -0
  87. package/dist/svelte/components/messages/MessageInput.svelte +232 -0
  88. package/dist/svelte/components/messages/MessageInput.svelte.d.ts +20 -0
  89. package/dist/svelte/components/messages/MessageInput.svelte.d.ts.map +1 -0
  90. package/dist/svelte/components/messages/MessageItem.svelte +431 -0
  91. package/dist/svelte/components/messages/MessageItem.svelte.d.ts +19 -0
  92. package/dist/svelte/components/messages/MessageItem.svelte.d.ts.map +1 -0
  93. package/dist/svelte/components/messages/MessageList.svelte +129 -0
  94. package/dist/svelte/components/messages/MessageList.svelte.d.ts +17 -0
  95. package/dist/svelte/components/messages/MessageList.svelte.d.ts.map +1 -0
  96. package/dist/svelte/components/messages/ThreadPanel.svelte +156 -0
  97. package/dist/svelte/components/messages/ThreadPanel.svelte.d.ts +17 -0
  98. package/dist/svelte/components/messages/ThreadPanel.svelte.d.ts.map +1 -0
  99. package/dist/svelte/components/messages/__tests__/MessageInput.test.js +38 -0
  100. package/dist/svelte/components/shared/Avatar.svelte +30 -0
  101. package/dist/svelte/components/shared/Avatar.svelte.d.ts +14 -0
  102. package/dist/svelte/components/shared/Avatar.svelte.d.ts.map +1 -0
  103. package/dist/svelte/components/shared/FileUpload.svelte +382 -0
  104. package/dist/svelte/components/shared/FileUpload.svelte.d.ts +14 -0
  105. package/dist/svelte/components/shared/FileUpload.svelte.d.ts.map +1 -0
  106. package/dist/svelte/components/shared/LinkPreview.svelte +108 -0
  107. package/dist/svelte/components/shared/LinkPreview.svelte.d.ts +18 -0
  108. package/dist/svelte/components/shared/LinkPreview.svelte.d.ts.map +1 -0
  109. package/dist/svelte/components/shared/MentionAutocomplete.svelte +168 -0
  110. package/dist/svelte/components/shared/MentionAutocomplete.svelte.d.ts +18 -0
  111. package/dist/svelte/components/shared/MentionAutocomplete.svelte.d.ts.map +1 -0
  112. package/dist/svelte/components/shared/MessageBubble.svelte +81 -0
  113. package/dist/svelte/components/shared/MessageBubble.svelte.d.ts +16 -0
  114. package/dist/svelte/components/shared/MessageBubble.svelte.d.ts.map +1 -0
  115. package/dist/svelte/components/shared/ReactionPicker.svelte +103 -0
  116. package/dist/svelte/components/shared/ReactionPicker.svelte.d.ts +10 -0
  117. package/dist/svelte/components/shared/ReactionPicker.svelte.d.ts.map +1 -0
  118. package/dist/svelte/components/shared/ReadReceipts.svelte +127 -0
  119. package/dist/svelte/components/shared/ReadReceipts.svelte.d.ts +13 -0
  120. package/dist/svelte/components/shared/ReadReceipts.svelte.d.ts.map +1 -0
  121. package/dist/svelte/components/shared/TypingIndicator.svelte +90 -0
  122. package/dist/svelte/components/shared/TypingIndicator.svelte.d.ts +12 -0
  123. package/dist/svelte/components/shared/TypingIndicator.svelte.d.ts.map +1 -0
  124. package/dist/svelte/components/shared/UserPresence.svelte +65 -0
  125. package/dist/svelte/components/shared/UserPresence.svelte.d.ts +13 -0
  126. package/dist/svelte/components/shared/UserPresence.svelte.d.ts.map +1 -0
  127. package/dist/svelte/components/shared/__tests__/Avatar.test.js +20 -0
  128. package/dist/svelte/components/shared/__tests__/LinkPreview.test.js +29 -0
  129. package/dist/svelte/components/shared/__tests__/MessageBubble.test.js +21 -0
  130. package/dist/svelte/components/shared/__tests__/ReactionPicker.test.js +35 -0
  131. package/dist/svelte/components/shared/__tests__/ReadReceipts.test.js +28 -0
  132. package/dist/svelte/components/shared/__tests__/TypingIndicator.test.js +27 -0
  133. package/dist/svelte/components/shared/__tests__/UserPresence.test.js +23 -0
  134. package/dist/svelte/components/tabs/ChatTab.svelte +240 -0
  135. package/dist/svelte/components/tabs/ChatTab.svelte.d.ts +21 -0
  136. package/dist/svelte/components/tabs/ChatTab.svelte.d.ts.map +1 -0
  137. package/dist/svelte/components/tabs/ChatTabList.svelte +158 -0
  138. package/dist/svelte/components/tabs/ChatTabList.svelte.d.ts +13 -0
  139. package/dist/svelte/components/tabs/ChatTabList.svelte.d.ts.map +1 -0
  140. package/dist/svelte/components/tabs/ChatTabs.svelte +88 -0
  141. package/dist/svelte/components/tabs/ChatTabs.svelte.d.ts +21 -0
  142. package/dist/svelte/components/tabs/ChatTabs.svelte.d.ts.map +1 -0
  143. package/dist/svelte/components/tabs/MiniChat.svelte +253 -0
  144. package/dist/svelte/components/tabs/MiniChat.svelte.d.ts +15 -0
  145. package/dist/svelte/components/tabs/MiniChat.svelte.d.ts.map +1 -0
  146. package/dist/svelte/i18n.d.ts +51 -0
  147. package/dist/svelte/i18n.d.ts.map +1 -0
  148. package/dist/svelte/i18n.js +72 -0
  149. package/dist/svelte/i18n.messages.d.ts +50 -0
  150. package/dist/svelte/i18n.messages.d.ts.map +1 -0
  151. package/dist/svelte/i18n.messages.js +69 -0
  152. package/dist/svelte/index.d.ts +48 -0
  153. package/dist/svelte/index.d.ts.map +1 -0
  154. package/dist/svelte/index.js +117 -0
  155. package/dist/svelte/playground.d.ts +171 -0
  156. package/dist/svelte/playground.d.ts.map +1 -0
  157. package/dist/svelte/playground.js +161 -0
  158. package/dist/svelte/types.d.ts +116 -0
  159. package/dist/svelte/types.d.ts.map +1 -0
  160. package/dist/svelte/types.js +1 -0
  161. package/dist/types.d.ts +99 -0
  162. package/dist/types.d.ts.map +1 -0
  163. package/dist/types.js +2 -0
  164. package/dist/types.js.map +1 -0
  165. package/dist/ui.d.ts +4 -0
  166. package/dist/ui.d.ts.map +1 -0
  167. package/dist/ui.js +92 -0
  168. package/dist/ui.js.map +1 -0
  169. package/package.json +95 -0
@@ -0,0 +1,2044 @@
1
+ import { field, crossPackageRef, foreignKey, smrt, SmrtObject, SmrtCollection, ObjectRegistry } from "@happyvertical/smrt-core";
2
+ import { tenantId, TenantScoped } from "@happyvertical/smrt-tenancy";
3
+ import { createHash } from "node:crypto";
4
+ var __defProp$5 = Object.defineProperty;
5
+ var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
6
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp$5(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
7
+ var __decorateClass$5 = (decorators, target, key, kind) => {
8
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(target, key) : target;
9
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
10
+ if (decorator = decorators[i])
11
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
12
+ if (kind && result) __defProp$5(target, key, result);
13
+ return result;
14
+ };
15
+ var __publicField = (obj, key, value) => __defNormalProp(obj, key + "", value);
16
+ let AgentSession = class extends SmrtObject {
17
+ tenantId = null;
18
+ agentId = "";
19
+ participantProfileId = "";
20
+ chatRoomId = null;
21
+ status = "active";
22
+ allowedTools = "[]";
23
+ sessionContext = "{}";
24
+ systemPrompt = "";
25
+ messageCount = 0;
26
+ totalTokensUsed = 0;
27
+ maxTokens = 0;
28
+ maxMessages = 0;
29
+ lastMessageAt = null;
30
+ expiresAt = null;
31
+ closedAt = null;
32
+ constructor(options = {}) {
33
+ super(options);
34
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
35
+ if (options.agentId !== void 0) this.agentId = options.agentId;
36
+ if (options.participantProfileId !== void 0)
37
+ this.participantProfileId = options.participantProfileId;
38
+ if (options.chatRoomId !== void 0) this.chatRoomId = options.chatRoomId;
39
+ if (options.status !== void 0) this.status = options.status;
40
+ if (options.allowedTools !== void 0)
41
+ this.allowedTools = options.allowedTools;
42
+ if (options.sessionContext !== void 0)
43
+ this.sessionContext = options.sessionContext;
44
+ if (options.systemPrompt !== void 0)
45
+ this.systemPrompt = options.systemPrompt;
46
+ if (options.messageCount !== void 0)
47
+ this.messageCount = options.messageCount;
48
+ if (options.totalTokensUsed !== void 0)
49
+ this.totalTokensUsed = options.totalTokensUsed;
50
+ if (options.maxTokens !== void 0) this.maxTokens = options.maxTokens;
51
+ if (options.maxMessages !== void 0)
52
+ this.maxMessages = options.maxMessages;
53
+ if (options.lastMessageAt !== void 0)
54
+ this.lastMessageAt = options.lastMessageAt;
55
+ if (options.expiresAt !== void 0) this.expiresAt = options.expiresAt;
56
+ if (options.closedAt !== void 0) this.closedAt = options.closedAt;
57
+ }
58
+ getAllowedTools() {
59
+ try {
60
+ const parsed = JSON.parse(this.allowedTools);
61
+ return Array.isArray(parsed) ? parsed.filter((t) => typeof t === "string") : [];
62
+ } catch {
63
+ return [];
64
+ }
65
+ }
66
+ setAllowedTools(tools) {
67
+ this.allowedTools = JSON.stringify(tools);
68
+ }
69
+ /**
70
+ * Fail-closed authorization check for an agent tool call (S5 #1392).
71
+ *
72
+ * A tool may only be invoked when it appears in this session's allow-list.
73
+ * If the allow-list is empty or unparseable, NO tools are permitted. This is
74
+ * deliberately conservative: an empty whitelist means "no tools", never
75
+ * "all tools".
76
+ */
77
+ isToolAllowed(toolName) {
78
+ if (typeof toolName !== "string" || toolName.length === 0) return false;
79
+ return this.getAllowedTools().includes(toolName);
80
+ }
81
+ isActive() {
82
+ if (this.status !== "active") return false;
83
+ if (this.expiresAt && /* @__PURE__ */ new Date() >= this.expiresAt) return false;
84
+ if (this.maxMessages > 0 && this.messageCount >= this.maxMessages)
85
+ return false;
86
+ if (this.maxTokens > 0 && this.totalTokensUsed >= this.maxTokens)
87
+ return false;
88
+ return true;
89
+ }
90
+ isExpired() {
91
+ return this.expiresAt !== null && /* @__PURE__ */ new Date() >= this.expiresAt;
92
+ }
93
+ async close() {
94
+ this.status = "closed";
95
+ this.closedAt = /* @__PURE__ */ new Date();
96
+ await this.save();
97
+ }
98
+ async expire() {
99
+ this.status = "expired";
100
+ this.closedAt = /* @__PURE__ */ new Date();
101
+ await this.save();
102
+ }
103
+ getSessionContext() {
104
+ try {
105
+ return JSON.parse(this.sessionContext);
106
+ } catch {
107
+ return {};
108
+ }
109
+ }
110
+ setSessionContext(ctx) {
111
+ this.sessionContext = JSON.stringify(ctx);
112
+ }
113
+ /**
114
+ * Stable identity key that scopes this session to a conversation subject
115
+ * (S5 #1392). Returns `null` when the session is not subject-scoped. Used by
116
+ * the reuse lookup so a session opened for one subject is never reused for a
117
+ * request about a different subject.
118
+ */
119
+ getSessionKey() {
120
+ const value = this.getSessionContext()[AgentSession.SESSION_KEY_CONTEXT_FIELD];
121
+ return typeof value === "string" && value.length > 0 ? value : null;
122
+ }
123
+ async updateSessionContext(updates) {
124
+ const current = this.getSessionContext();
125
+ this.sessionContext = JSON.stringify({ ...current, ...updates });
126
+ await this.save();
127
+ }
128
+ async recordMessage(tokensUsed = 0) {
129
+ this.messageCount++;
130
+ this.totalTokensUsed += tokensUsed;
131
+ this.lastMessageAt = /* @__PURE__ */ new Date();
132
+ await this.save();
133
+ }
134
+ };
135
+ __publicField(AgentSession, "SESSION_KEY_CONTEXT_FIELD", "__sessionKey");
136
+ __decorateClass$5([
137
+ tenantId({ nullable: true })
138
+ ], AgentSession.prototype, "tenantId", 2);
139
+ __decorateClass$5([
140
+ field({ required: true })
141
+ ], AgentSession.prototype, "agentId", 2);
142
+ __decorateClass$5([
143
+ crossPackageRef("@happyvertical/smrt-profiles:Profile", { required: true })
144
+ ], AgentSession.prototype, "participantProfileId", 2);
145
+ __decorateClass$5([
146
+ foreignKey("ChatRoom")
147
+ ], AgentSession.prototype, "chatRoomId", 2);
148
+ __decorateClass$5([
149
+ field({ required: true })
150
+ ], AgentSession.prototype, "status", 2);
151
+ __decorateClass$5([
152
+ field()
153
+ ], AgentSession.prototype, "allowedTools", 2);
154
+ __decorateClass$5([
155
+ field()
156
+ ], AgentSession.prototype, "sessionContext", 2);
157
+ __decorateClass$5([
158
+ field()
159
+ ], AgentSession.prototype, "systemPrompt", 2);
160
+ __decorateClass$5([
161
+ field()
162
+ ], AgentSession.prototype, "messageCount", 2);
163
+ __decorateClass$5([
164
+ field()
165
+ ], AgentSession.prototype, "totalTokensUsed", 2);
166
+ __decorateClass$5([
167
+ field()
168
+ ], AgentSession.prototype, "maxTokens", 2);
169
+ __decorateClass$5([
170
+ field()
171
+ ], AgentSession.prototype, "maxMessages", 2);
172
+ __decorateClass$5([
173
+ field()
174
+ ], AgentSession.prototype, "lastMessageAt", 2);
175
+ __decorateClass$5([
176
+ field()
177
+ ], AgentSession.prototype, "expiresAt", 2);
178
+ __decorateClass$5([
179
+ field()
180
+ ], AgentSession.prototype, "closedAt", 2);
181
+ AgentSession = __decorateClass$5([
182
+ TenantScoped({ mode: "optional" }),
183
+ smrt({
184
+ tableName: "agent_sessions",
185
+ api: { include: ["list", "get"] },
186
+ mcp: { include: ["list", "get"] },
187
+ cli: true
188
+ })
189
+ ], AgentSession);
190
+ var __defProp$4 = Object.defineProperty;
191
+ var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
192
+ var __decorateClass$4 = (decorators, target, key, kind) => {
193
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
194
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
195
+ if (decorator = decorators[i])
196
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
197
+ if (kind && result) __defProp$4(target, key, result);
198
+ return result;
199
+ };
200
+ let ChatMessage = class extends SmrtObject {
201
+ tenantId = "";
202
+ roomId = "";
203
+ threadId = null;
204
+ senderProfileId = "";
205
+ agentSessionId = null;
206
+ content = "";
207
+ messageType = "text";
208
+ role = "user";
209
+ isEdited = false;
210
+ editedAt = null;
211
+ isDeleted = false;
212
+ replyToMessageId = null;
213
+ metadata = "{}";
214
+ toolCallData = null;
215
+ attachments = "[]";
216
+ constructor(options = {}) {
217
+ super(options);
218
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
219
+ if (options.roomId !== void 0) this.roomId = options.roomId;
220
+ if (options.threadId !== void 0) this.threadId = options.threadId;
221
+ if (options.senderProfileId !== void 0)
222
+ this.senderProfileId = options.senderProfileId;
223
+ if (options.agentSessionId !== void 0)
224
+ this.agentSessionId = options.agentSessionId;
225
+ if (options.content !== void 0) this.content = options.content;
226
+ if (options.messageType !== void 0)
227
+ this.messageType = options.messageType;
228
+ if (options.role !== void 0) this.role = options.role;
229
+ if (options.isEdited !== void 0) this.isEdited = options.isEdited;
230
+ if (options.editedAt !== void 0) this.editedAt = options.editedAt;
231
+ if (options.isDeleted !== void 0) this.isDeleted = options.isDeleted;
232
+ if (options.replyToMessageId !== void 0)
233
+ this.replyToMessageId = options.replyToMessageId;
234
+ if (options.metadata !== void 0)
235
+ this.metadata = typeof options.metadata === "string" ? options.metadata : JSON.stringify(options.metadata);
236
+ if (options.toolCallData !== void 0)
237
+ this.toolCallData = options.toolCallData === null ? null : typeof options.toolCallData === "string" ? options.toolCallData : JSON.stringify(options.toolCallData);
238
+ if (options.attachments !== void 0)
239
+ this.attachments = options.attachments;
240
+ }
241
+ getAttachments() {
242
+ try {
243
+ return JSON.parse(this.attachments);
244
+ } catch {
245
+ return [];
246
+ }
247
+ }
248
+ setAttachments(items) {
249
+ this.attachments = JSON.stringify(items);
250
+ }
251
+ getMetadata() {
252
+ try {
253
+ return JSON.parse(this.metadata);
254
+ } catch {
255
+ return {};
256
+ }
257
+ }
258
+ setMetadata(data) {
259
+ this.metadata = JSON.stringify(data);
260
+ }
261
+ getToolCallData() {
262
+ if (!this.toolCallData) return null;
263
+ try {
264
+ return JSON.parse(this.toolCallData);
265
+ } catch {
266
+ return null;
267
+ }
268
+ }
269
+ setToolCallData(data) {
270
+ this.toolCallData = data ? JSON.stringify(data) : null;
271
+ }
272
+ hasAttachments() {
273
+ return this.getAttachments().length > 0;
274
+ }
275
+ isToolCall() {
276
+ return this.messageType === "tool_call";
277
+ }
278
+ isToolResult() {
279
+ return this.messageType === "tool_result";
280
+ }
281
+ isFromAgent() {
282
+ return this.role === "assistant";
283
+ }
284
+ isSystemMessage() {
285
+ return this.role === "system";
286
+ }
287
+ async edit(newContent) {
288
+ this.content = newContent;
289
+ this.isEdited = true;
290
+ this.editedAt = /* @__PURE__ */ new Date();
291
+ await this.save();
292
+ }
293
+ async softDelete() {
294
+ this.isDeleted = true;
295
+ this.content = "";
296
+ await this.save();
297
+ }
298
+ getPreview(maxLength = 100) {
299
+ if (this.isDeleted) return "(deleted)";
300
+ const text = this.content || "";
301
+ if (text.length <= maxLength) return text;
302
+ return `${text.slice(0, maxLength)}...`;
303
+ }
304
+ };
305
+ __decorateClass$4([
306
+ tenantId()
307
+ ], ChatMessage.prototype, "tenantId", 2);
308
+ __decorateClass$4([
309
+ foreignKey("ChatRoom", { required: true })
310
+ ], ChatMessage.prototype, "roomId", 2);
311
+ __decorateClass$4([
312
+ foreignKey("ChatThread")
313
+ ], ChatMessage.prototype, "threadId", 2);
314
+ __decorateClass$4([
315
+ crossPackageRef("@happyvertical/smrt-profiles:Profile", { required: true })
316
+ ], ChatMessage.prototype, "senderProfileId", 2);
317
+ __decorateClass$4([
318
+ foreignKey("AgentSession")
319
+ ], ChatMessage.prototype, "agentSessionId", 2);
320
+ __decorateClass$4([
321
+ field()
322
+ ], ChatMessage.prototype, "content", 2);
323
+ __decorateClass$4([
324
+ field({ required: true })
325
+ ], ChatMessage.prototype, "messageType", 2);
326
+ __decorateClass$4([
327
+ field({ required: true })
328
+ ], ChatMessage.prototype, "role", 2);
329
+ __decorateClass$4([
330
+ field()
331
+ ], ChatMessage.prototype, "isEdited", 2);
332
+ __decorateClass$4([
333
+ field()
334
+ ], ChatMessage.prototype, "editedAt", 2);
335
+ __decorateClass$4([
336
+ field()
337
+ ], ChatMessage.prototype, "isDeleted", 2);
338
+ __decorateClass$4([
339
+ foreignKey("ChatMessage")
340
+ ], ChatMessage.prototype, "replyToMessageId", 2);
341
+ __decorateClass$4([
342
+ field()
343
+ ], ChatMessage.prototype, "metadata", 2);
344
+ __decorateClass$4([
345
+ field()
346
+ ], ChatMessage.prototype, "toolCallData", 2);
347
+ __decorateClass$4([
348
+ field()
349
+ ], ChatMessage.prototype, "attachments", 2);
350
+ ChatMessage = __decorateClass$4([
351
+ TenantScoped({ mode: "required" }),
352
+ smrt({
353
+ tableName: "chat_messages",
354
+ api: { include: ["list", "get"] },
355
+ mcp: { include: ["list", "get"] },
356
+ cli: true
357
+ })
358
+ ], ChatMessage);
359
+ var __defProp$3 = Object.defineProperty;
360
+ var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
361
+ var __decorateClass$3 = (decorators, target, key, kind) => {
362
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
363
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
364
+ if (decorator = decorators[i])
365
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
366
+ if (kind && result) __defProp$3(target, key, result);
367
+ return result;
368
+ };
369
+ let ChatParticipant = class extends SmrtObject {
370
+ tenantId = "";
371
+ roomId = "";
372
+ profileId = "";
373
+ role = "member";
374
+ status = "active";
375
+ onlineStatus = "offline";
376
+ lastReadMessageId = null;
377
+ lastSeenAt = null;
378
+ joinedAt = null;
379
+ nickname = "";
380
+ isMuted = false;
381
+ isPinned = false;
382
+ constructor(options = {}) {
383
+ super(options);
384
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
385
+ if (options.roomId !== void 0) this.roomId = options.roomId;
386
+ if (options.profileId !== void 0) this.profileId = options.profileId;
387
+ if (options.role !== void 0) this.role = options.role;
388
+ if (options.status !== void 0) this.status = options.status;
389
+ if (options.onlineStatus !== void 0)
390
+ this.onlineStatus = options.onlineStatus;
391
+ if (options.lastReadMessageId !== void 0)
392
+ this.lastReadMessageId = options.lastReadMessageId;
393
+ if (options.lastSeenAt !== void 0) this.lastSeenAt = options.lastSeenAt;
394
+ if (options.joinedAt !== void 0) this.joinedAt = options.joinedAt;
395
+ if (options.nickname !== void 0) this.nickname = options.nickname;
396
+ if (options.isMuted !== void 0) this.isMuted = options.isMuted;
397
+ if (options.isPinned !== void 0) this.isPinned = options.isPinned;
398
+ }
399
+ isActive() {
400
+ return this.status === "active";
401
+ }
402
+ isOwner() {
403
+ return this.role === "owner";
404
+ }
405
+ isAdmin() {
406
+ return this.role === "admin" || this.role === "owner";
407
+ }
408
+ async markRead(messageId) {
409
+ this.lastReadMessageId = messageId;
410
+ this.lastSeenAt = /* @__PURE__ */ new Date();
411
+ await this.save();
412
+ }
413
+ async leave() {
414
+ this.status = "left";
415
+ await this.save();
416
+ }
417
+ async setOnline(status) {
418
+ this.onlineStatus = status;
419
+ this.lastSeenAt = /* @__PURE__ */ new Date();
420
+ await this.save();
421
+ }
422
+ };
423
+ __decorateClass$3([
424
+ tenantId()
425
+ ], ChatParticipant.prototype, "tenantId", 2);
426
+ __decorateClass$3([
427
+ foreignKey("ChatRoom", { required: true })
428
+ ], ChatParticipant.prototype, "roomId", 2);
429
+ __decorateClass$3([
430
+ crossPackageRef("@happyvertical/smrt-profiles:Profile", { required: true })
431
+ ], ChatParticipant.prototype, "profileId", 2);
432
+ __decorateClass$3([
433
+ field({ required: true })
434
+ ], ChatParticipant.prototype, "role", 2);
435
+ __decorateClass$3([
436
+ field({ required: true })
437
+ ], ChatParticipant.prototype, "status", 2);
438
+ __decorateClass$3([
439
+ field({ required: true })
440
+ ], ChatParticipant.prototype, "onlineStatus", 2);
441
+ __decorateClass$3([
442
+ foreignKey("ChatMessage")
443
+ ], ChatParticipant.prototype, "lastReadMessageId", 2);
444
+ __decorateClass$3([
445
+ field()
446
+ ], ChatParticipant.prototype, "lastSeenAt", 2);
447
+ __decorateClass$3([
448
+ field()
449
+ ], ChatParticipant.prototype, "joinedAt", 2);
450
+ __decorateClass$3([
451
+ field()
452
+ ], ChatParticipant.prototype, "nickname", 2);
453
+ __decorateClass$3([
454
+ field()
455
+ ], ChatParticipant.prototype, "isMuted", 2);
456
+ __decorateClass$3([
457
+ field()
458
+ ], ChatParticipant.prototype, "isPinned", 2);
459
+ ChatParticipant = __decorateClass$3([
460
+ TenantScoped({ mode: "required" }),
461
+ smrt({
462
+ tableName: "chat_participants",
463
+ api: { include: ["list", "get"] },
464
+ mcp: { include: ["list", "get"] },
465
+ cli: true
466
+ })
467
+ ], ChatParticipant);
468
+ var __defProp$2 = Object.defineProperty;
469
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
470
+ var __decorateClass$2 = (decorators, target, key, kind) => {
471
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
472
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
473
+ if (decorator = decorators[i])
474
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
475
+ if (kind && result) __defProp$2(target, key, result);
476
+ return result;
477
+ };
478
+ let ChatReaction = class extends SmrtObject {
479
+ tenantId = "";
480
+ messageId = "";
481
+ profileId = "";
482
+ emoji = "";
483
+ constructor(options = {}) {
484
+ super(options);
485
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
486
+ if (options.messageId !== void 0) this.messageId = options.messageId;
487
+ if (options.profileId !== void 0) this.profileId = options.profileId;
488
+ if (options.emoji !== void 0) this.emoji = options.emoji;
489
+ }
490
+ };
491
+ __decorateClass$2([
492
+ tenantId()
493
+ ], ChatReaction.prototype, "tenantId", 2);
494
+ __decorateClass$2([
495
+ foreignKey("ChatMessage", { required: true })
496
+ ], ChatReaction.prototype, "messageId", 2);
497
+ __decorateClass$2([
498
+ crossPackageRef("@happyvertical/smrt-profiles:Profile", { required: true })
499
+ ], ChatReaction.prototype, "profileId", 2);
500
+ __decorateClass$2([
501
+ field({ required: true })
502
+ ], ChatReaction.prototype, "emoji", 2);
503
+ ChatReaction = __decorateClass$2([
504
+ TenantScoped({ mode: "required" }),
505
+ smrt({
506
+ tableName: "chat_reactions",
507
+ api: { include: ["list"] },
508
+ mcp: { include: ["list"] },
509
+ cli: false
510
+ })
511
+ ], ChatReaction);
512
+ var __defProp$1 = Object.defineProperty;
513
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
514
+ var __decorateClass$1 = (decorators, target, key, kind) => {
515
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
516
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
517
+ if (decorator = decorators[i])
518
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
519
+ if (kind && result) __defProp$1(target, key, result);
520
+ return result;
521
+ };
522
+ let ChatRoom = class extends SmrtObject {
523
+ tenantId = "";
524
+ name = "";
525
+ description = "";
526
+ roomType = "public";
527
+ status = "active";
528
+ topic = "";
529
+ avatarUrl = "";
530
+ isArchived = false;
531
+ maxParticipants = 0;
532
+ metadata = "{}";
533
+ createdByProfileId = "";
534
+ lastMessageAt = null;
535
+ constructor(options = {}) {
536
+ super(options);
537
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
538
+ if (options.name !== void 0) this.name = options.name;
539
+ if (options.description !== void 0)
540
+ this.description = options.description;
541
+ if (options.roomType !== void 0) this.roomType = options.roomType;
542
+ if (options.status !== void 0) this.status = options.status;
543
+ if (options.topic !== void 0) this.topic = options.topic;
544
+ if (options.avatarUrl !== void 0) this.avatarUrl = options.avatarUrl;
545
+ if (options.isArchived !== void 0) this.isArchived = options.isArchived;
546
+ if (options.maxParticipants !== void 0)
547
+ this.maxParticipants = options.maxParticipants;
548
+ if (options.metadata !== void 0)
549
+ this.metadata = typeof options.metadata === "string" ? options.metadata : JSON.stringify(options.metadata);
550
+ if (options.createdByProfileId !== void 0)
551
+ this.createdByProfileId = options.createdByProfileId;
552
+ if (options.lastMessageAt !== void 0)
553
+ this.lastMessageAt = options.lastMessageAt;
554
+ }
555
+ getMetadata() {
556
+ try {
557
+ return JSON.parse(this.metadata);
558
+ } catch {
559
+ return {};
560
+ }
561
+ }
562
+ setMetadata(data) {
563
+ this.metadata = JSON.stringify(data);
564
+ }
565
+ updateMetadata(updates) {
566
+ const current = this.getMetadata();
567
+ this.metadata = JSON.stringify({ ...current, ...updates });
568
+ }
569
+ isDM() {
570
+ return this.roomType === "dm";
571
+ }
572
+ isAgentRoom() {
573
+ return this.roomType === "agent";
574
+ }
575
+ isPublic() {
576
+ return this.roomType === "public";
577
+ }
578
+ isActive() {
579
+ return this.status === "active";
580
+ }
581
+ async archive() {
582
+ this.isArchived = true;
583
+ this.status = "archived";
584
+ await this.save();
585
+ }
586
+ async unarchive() {
587
+ this.isArchived = false;
588
+ this.status = "active";
589
+ await this.save();
590
+ }
591
+ };
592
+ __decorateClass$1([
593
+ tenantId()
594
+ ], ChatRoom.prototype, "tenantId", 2);
595
+ __decorateClass$1([
596
+ field()
597
+ ], ChatRoom.prototype, "name", 2);
598
+ __decorateClass$1([
599
+ field()
600
+ ], ChatRoom.prototype, "description", 2);
601
+ __decorateClass$1([
602
+ field({ required: true })
603
+ ], ChatRoom.prototype, "roomType", 2);
604
+ __decorateClass$1([
605
+ field({ required: true })
606
+ ], ChatRoom.prototype, "status", 2);
607
+ __decorateClass$1([
608
+ field()
609
+ ], ChatRoom.prototype, "topic", 2);
610
+ __decorateClass$1([
611
+ field()
612
+ ], ChatRoom.prototype, "avatarUrl", 2);
613
+ __decorateClass$1([
614
+ field()
615
+ ], ChatRoom.prototype, "isArchived", 2);
616
+ __decorateClass$1([
617
+ field()
618
+ ], ChatRoom.prototype, "maxParticipants", 2);
619
+ __decorateClass$1([
620
+ field()
621
+ ], ChatRoom.prototype, "metadata", 2);
622
+ __decorateClass$1([
623
+ crossPackageRef("@happyvertical/smrt-profiles:Profile")
624
+ ], ChatRoom.prototype, "createdByProfileId", 2);
625
+ __decorateClass$1([
626
+ field()
627
+ ], ChatRoom.prototype, "lastMessageAt", 2);
628
+ ChatRoom = __decorateClass$1([
629
+ TenantScoped({ mode: "required" }),
630
+ smrt({
631
+ tableName: "chat_rooms",
632
+ api: { include: ["list", "get"] },
633
+ mcp: { include: ["list", "get"] },
634
+ cli: true
635
+ })
636
+ ], ChatRoom);
637
+ var __defProp = Object.defineProperty;
638
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
639
+ var __decorateClass = (decorators, target, key, kind) => {
640
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
641
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
642
+ if (decorator = decorators[i])
643
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
644
+ if (kind && result) __defProp(target, key, result);
645
+ return result;
646
+ };
647
+ let ChatThread = class extends SmrtObject {
648
+ tenantId = "";
649
+ roomId = "";
650
+ rootMessageId = null;
651
+ title = "";
652
+ isResolved = false;
653
+ messageCount = 0;
654
+ lastMessageAt = null;
655
+ participantCount = 0;
656
+ constructor(options = {}) {
657
+ super(options);
658
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
659
+ if (options.roomId !== void 0) this.roomId = options.roomId;
660
+ if (options.rootMessageId !== void 0)
661
+ this.rootMessageId = options.rootMessageId;
662
+ if (options.title !== void 0) this.title = options.title;
663
+ if (options.isResolved !== void 0) this.isResolved = options.isResolved;
664
+ if (options.messageCount !== void 0)
665
+ this.messageCount = options.messageCount;
666
+ if (options.lastMessageAt !== void 0)
667
+ this.lastMessageAt = options.lastMessageAt;
668
+ if (options.participantCount !== void 0)
669
+ this.participantCount = options.participantCount;
670
+ }
671
+ async resolve() {
672
+ this.isResolved = true;
673
+ await this.save();
674
+ }
675
+ async reopen() {
676
+ this.isResolved = false;
677
+ await this.save();
678
+ }
679
+ };
680
+ __decorateClass([
681
+ tenantId()
682
+ ], ChatThread.prototype, "tenantId", 2);
683
+ __decorateClass([
684
+ foreignKey("ChatRoom", { required: true })
685
+ ], ChatThread.prototype, "roomId", 2);
686
+ __decorateClass([
687
+ foreignKey("ChatMessage", { nullable: true })
688
+ ], ChatThread.prototype, "rootMessageId", 2);
689
+ __decorateClass([
690
+ field()
691
+ ], ChatThread.prototype, "title", 2);
692
+ __decorateClass([
693
+ field()
694
+ ], ChatThread.prototype, "isResolved", 2);
695
+ __decorateClass([
696
+ field()
697
+ ], ChatThread.prototype, "messageCount", 2);
698
+ __decorateClass([
699
+ field()
700
+ ], ChatThread.prototype, "lastMessageAt", 2);
701
+ __decorateClass([
702
+ field()
703
+ ], ChatThread.prototype, "participantCount", 2);
704
+ ChatThread = __decorateClass([
705
+ TenantScoped({ mode: "required" }),
706
+ smrt({
707
+ tableName: "chat_threads",
708
+ api: { include: ["list", "get"] },
709
+ mcp: { include: ["list", "get"] },
710
+ cli: true
711
+ })
712
+ ], ChatThread);
713
+ class AgentSessionCollection extends SmrtCollection {
714
+ static _itemClass = AgentSession;
715
+ async findActiveByParticipant(participantProfileId) {
716
+ const sessions = await this.list({
717
+ where: { participantProfileId, status: "active" }
718
+ });
719
+ return sessions.filter((s) => s.isActive());
720
+ }
721
+ /**
722
+ * Resolve the active agent session for an (agent, participant) pair within a
723
+ * tenant (S5 #1392).
724
+ *
725
+ * `tenantId` is REQUIRED and always bound into the WHERE clause — including the
726
+ * `null` (untenanted) case — so the lookup can never silently drop its tenant
727
+ * predicate and resolve a session from another tenant. AgentSession uses
728
+ * optional tenancy, so `null` is a legitimate, explicitly-bound scope rather
729
+ * than "any tenant".
730
+ *
731
+ * When `sessionKey` is supplied the result is additionally narrowed to the
732
+ * session whose stored {@link AgentSession.getSessionKey} matches EXACTLY
733
+ * (S5 #1392). This binds session identity to a conversation subject (e.g. a
734
+ * content id) so a session opened for one subject is never reused for another;
735
+ * `sessionContext` is a JSON blob so the discriminator is matched in memory
736
+ * after the tenant-bound SQL filter.
737
+ */
738
+ async findActiveSession(agentId, participantProfileId, tenantId2, sessionKey) {
739
+ const where = {
740
+ agentId,
741
+ participantProfileId,
742
+ status: "active",
743
+ tenantId: tenantId2
744
+ };
745
+ const sessions = await this.list({ where });
746
+ const active = sessions.find(
747
+ (s) => s.isActive() && (sessionKey === void 0 || s.getSessionKey() === (sessionKey ?? null))
748
+ );
749
+ return active ?? null;
750
+ }
751
+ async findOrCreate(params) {
752
+ const existing = await this.findActiveSession(
753
+ params.agentId,
754
+ params.participantProfileId,
755
+ params.tenantId,
756
+ params.sessionKey
757
+ );
758
+ if (existing) return existing;
759
+ const sessionContext = params.sessionKey != null ? JSON.stringify({
760
+ [AgentSession.SESSION_KEY_CONTEXT_FIELD]: params.sessionKey
761
+ }) : void 0;
762
+ const session = await this.create({
763
+ agentId: params.agentId,
764
+ participantProfileId: params.participantProfileId,
765
+ tenantId: params.tenantId ?? null,
766
+ allowedTools: JSON.stringify(params.allowedTools ?? []),
767
+ chatRoomId: params.chatRoomId ?? null,
768
+ systemPrompt: params.systemPrompt ?? "",
769
+ status: "active",
770
+ ...sessionContext !== void 0 ? { sessionContext } : {}
771
+ });
772
+ return session;
773
+ }
774
+ async findByAgent(agentId) {
775
+ return this.list({ where: { agentId } });
776
+ }
777
+ /**
778
+ * Expire active sessions whose last activity predates `olderThan`.
779
+ *
780
+ * Caller-scoping (S5 #1392): pass `scope` to restrict the sweep to a single
781
+ * tenant and/or agent. Without a scope this expires stale sessions across the
782
+ * whole (tenant-filtered) collection — only safe for trusted maintenance
783
+ * callers, never for a per-tenant request handler.
784
+ *
785
+ * The candidate set is narrowed in SQL (status + activity timestamp) rather
786
+ * than loading every active session into memory and filtering in JS
787
+ * (DoS hardening). `lastMessageAt < olderThan` is applied server-side; rows
788
+ * that never recorded a message fall back to `created_at`.
789
+ */
790
+ async expireStale(olderThan, scope) {
791
+ const baseWhere = { status: "active" };
792
+ if (scope?.tenantId !== void 0) baseWhere.tenantId = scope.tenantId;
793
+ if (scope?.agentId !== void 0) baseWhere.agentId = scope.agentId;
794
+ const byLastMessage = await this.list({
795
+ where: { ...baseWhere, "lastMessageAt <": olderThan }
796
+ });
797
+ const neverMessaged = await this.list({
798
+ where: { ...baseWhere, lastMessageAt: null, "created_at <": olderThan }
799
+ });
800
+ const seen = /* @__PURE__ */ new Set();
801
+ let expired = 0;
802
+ for (const session of [...byLastMessage, ...neverMessaged]) {
803
+ const id = session.id;
804
+ if (!id || seen.has(id)) continue;
805
+ seen.add(id);
806
+ await session.expire();
807
+ expired++;
808
+ }
809
+ return expired;
810
+ }
811
+ }
812
+ class ChatMessageCollection extends SmrtCollection {
813
+ static _itemClass = ChatMessage;
814
+ /**
815
+ * Get messages for a room (newest first), excluding threads and deleted.
816
+ *
817
+ * Filtering, ordering and pagination are pushed into SQL rather than loading
818
+ * the full room history into memory (S5 #1392, DoS hardening). Thread replies
819
+ * are excluded via `threadId IS NULL` (the WHERE API maps a `null` value to
820
+ * `IS NULL`).
821
+ *
822
+ * `tenantId` is REQUIRED and bound into BOTH the message window AND the cursor
823
+ * lookup (S5 #1392): room ids are not globally unique, so a roomId-only read
824
+ * could surface another tenant's messages for a same-id room. The caller's
825
+ * membership gate is tenant-scoped, so the read MUST be too.
826
+ */
827
+ async getByRoom(roomId, tenantId2, options) {
828
+ const where = {
829
+ roomId,
830
+ tenantId: tenantId2,
831
+ isDeleted: false,
832
+ threadId: null
833
+ };
834
+ if (options?.before) {
835
+ const cursorMsg = await this.get({
836
+ id: options.before,
837
+ roomId,
838
+ tenantId: tenantId2,
839
+ isDeleted: false,
840
+ threadId: null
841
+ });
842
+ if (cursorMsg?.created_at) {
843
+ where["created_at <"] = cursorMsg.created_at;
844
+ }
845
+ }
846
+ return this.list({
847
+ where,
848
+ orderBy: "created_at DESC",
849
+ limit: options?.limit
850
+ });
851
+ }
852
+ /**
853
+ * Get messages in a thread, tenant-bound (S5 #1392).
854
+ *
855
+ * `tenantId` is bound into the query so a thread id from another tenant can
856
+ * never surface that tenant's thread history.
857
+ */
858
+ async getByThread(threadId, tenantId2) {
859
+ return this.list({ where: { threadId, tenantId: tenantId2, isDeleted: false } });
860
+ }
861
+ /**
862
+ * Get messages for an agent session, tenant-bound (S5 #1392).
863
+ *
864
+ * `tenantId` is bound into the query so a session id from another tenant can
865
+ * never surface that tenant's session messages.
866
+ */
867
+ async getByAgentSession(agentSessionId, tenantId2) {
868
+ return this.list({ where: { agentSessionId, tenantId: tenantId2, isDeleted: false } });
869
+ }
870
+ /**
871
+ * Search messages with filters.
872
+ *
873
+ * All structural filters (tenant/room/thread/sender/type/role/date/text) are
874
+ * pushed into SQL instead of loading the full message set and filtering in JS
875
+ * (S5 #1392, DoS hardening). `tenantId` is REQUIRED and always bound so the
876
+ * search can never cross a tenant boundary.
877
+ *
878
+ * `hasAttachments` is derived from the stored `attachments` JSON column and is
879
+ * pushed into SQL as a `!=`/`=` pre-filter against the empty-array sentinel
880
+ * (`'[]'`, the column default) so the `LIMIT` applies to the ALREADY-FILTERED
881
+ * set, not the raw window (S5 #1392). Previously the limit truncated the
882
+ * newest-N window first and the JS filter ran afterwards, so a room whose
883
+ * newest messages lacked attachments could return fewer (or zero)
884
+ * attachment-bearing rows than existed. A precise JS pass on
885
+ * `hasAttachments()` still runs to reject any malformed/empty column value the
886
+ * coarse SQL sentinel can't distinguish, and the requested `limit` is enforced
887
+ * on the filtered result.
888
+ */
889
+ async search(filters) {
890
+ const where = {
891
+ tenantId: filters.tenantId,
892
+ isDeleted: false
893
+ };
894
+ if (filters.roomId) where.roomId = filters.roomId;
895
+ if (filters.threadId) where.threadId = filters.threadId;
896
+ if (filters.senderProfileId)
897
+ where.senderProfileId = filters.senderProfileId;
898
+ if (filters.messageType) where.messageType = filters.messageType;
899
+ if (filters.role) where.role = filters.role;
900
+ if (filters.query) where["content like"] = `%${filters.query}%`;
901
+ if (filters.sinceDate) where["created_at >="] = filters.sinceDate;
902
+ if (filters.beforeDate) where["created_at <"] = filters.beforeDate;
903
+ const limit = filters.limit ?? 100;
904
+ if (filters.hasAttachments === true) {
905
+ where["attachments !="] = "[]";
906
+ } else if (filters.hasAttachments === false) {
907
+ where.attachments = "[]";
908
+ }
909
+ const messages = await this.list({
910
+ where,
911
+ orderBy: "created_at DESC",
912
+ limit
913
+ });
914
+ if (filters.hasAttachments !== void 0) {
915
+ return messages.filter((m) => m.hasAttachments() === filters.hasAttachments).slice(0, limit);
916
+ }
917
+ return messages;
918
+ }
919
+ /**
920
+ * Get unread count for a participant in a room (excludes thread replies).
921
+ *
922
+ * Counting is pushed into SQL via `count()` rather than loading the whole
923
+ * room history into memory (S5 #1392, DoS hardening). `tenantId` is REQUIRED
924
+ * and bound into both the count and the read-cursor lookup so the count can
925
+ * never include another tenant's messages for a same-id room.
926
+ */
927
+ async getUnreadCount(roomId, tenantId2, lastReadMessageId) {
928
+ const base = { roomId, tenantId: tenantId2, isDeleted: false, threadId: null };
929
+ if (!lastReadMessageId) return this.count({ where: base });
930
+ const lastReadMsg = await this.get({
931
+ id: lastReadMessageId,
932
+ roomId,
933
+ tenantId: tenantId2,
934
+ isDeleted: false,
935
+ threadId: null
936
+ });
937
+ if (!lastReadMsg?.created_at) return this.count({ where: base });
938
+ return this.count({
939
+ where: { ...base, "created_at >": lastReadMsg.created_at }
940
+ });
941
+ }
942
+ /**
943
+ * Get most recent root message for each room (for room list preview).
944
+ *
945
+ * Each room is fetched with `orderBy created_at DESC, limit 1` so we never
946
+ * load the full per-room history into memory (S5 #1392, DoS hardening).
947
+ * `tenantId` is REQUIRED and bound into each per-room read so a same-id room
948
+ * from another tenant can never leak a preview message.
949
+ */
950
+ async getLatestPerRoom(roomIds, tenantId2) {
951
+ const result = /* @__PURE__ */ new Map();
952
+ for (const roomId of roomIds) {
953
+ const latest = await this.list({
954
+ where: { roomId, tenantId: tenantId2, isDeleted: false, threadId: null },
955
+ orderBy: "created_at DESC",
956
+ limit: 1
957
+ });
958
+ if (latest.length > 0) {
959
+ result.set(roomId, latest[0]);
960
+ }
961
+ }
962
+ return result;
963
+ }
964
+ }
965
+ class ChatParticipantCollection extends SmrtCollection {
966
+ static _itemClass = ChatParticipant;
967
+ async getByRoom(roomId) {
968
+ return this.list({ where: { roomId, status: "active" } });
969
+ }
970
+ async getByProfile(profileId) {
971
+ return this.list({ where: { profileId, status: "active" } });
972
+ }
973
+ async findMembership(roomId, profileId, tenantId2) {
974
+ const where = { roomId, profileId };
975
+ if (tenantId2 !== void 0) where.tenantId = tenantId2;
976
+ const results = await this.list({ where });
977
+ return results[0] ?? null;
978
+ }
979
+ /**
980
+ * Returns the participant row only if the profile is an ACTIVE member of the
981
+ * room (not left/kicked/banned). Used by ChatService to gate sends and reads
982
+ * on room membership (S5 #1392, IDOR hardening).
983
+ *
984
+ * `tenantId` is REQUIRED and always bound into the WHERE clause so a membership
985
+ * lookup can never resolve a row belonging to another tenant, even when no ALS
986
+ * tenant context is active to drive the tenancy interceptor (defense in depth).
987
+ * Making it a required parameter (rather than optional) closes the hole where a
988
+ * caller could omit it and silently drop the tenant predicate from the query.
989
+ */
990
+ async findActiveMembership(roomId, profileId, tenantId2) {
991
+ const where = {
992
+ roomId,
993
+ profileId,
994
+ status: "active",
995
+ tenantId: tenantId2
996
+ };
997
+ const results = await this.list({ where, limit: 1 });
998
+ return results[0] ?? null;
999
+ }
1000
+ /** True when the profile is an active member of the room (tenant-bound). */
1001
+ async isActiveMember(roomId, profileId, tenantId2) {
1002
+ return await this.findActiveMembership(roomId, profileId, tenantId2) !== null;
1003
+ }
1004
+ async getOnlineInRoom(roomId) {
1005
+ const participants = await this.list({
1006
+ where: { roomId, status: "active" }
1007
+ });
1008
+ return participants.filter((p) => p.onlineStatus !== "offline");
1009
+ }
1010
+ async getAdminsInRoom(roomId) {
1011
+ const participants = await this.list({
1012
+ where: { roomId, status: "active" }
1013
+ });
1014
+ return participants.filter((p) => p.isAdmin());
1015
+ }
1016
+ async countInRoom(roomId) {
1017
+ const participants = await this.list({
1018
+ where: { roomId, status: "active" }
1019
+ });
1020
+ return participants.length;
1021
+ }
1022
+ }
1023
+ class ChatReactionCollection extends SmrtCollection {
1024
+ static _itemClass = ChatReaction;
1025
+ async getByMessage(messageId) {
1026
+ return this.list({ where: { messageId } });
1027
+ }
1028
+ /** Get reaction counts grouped by emoji for a message */
1029
+ async getReactionCounts(messageId) {
1030
+ const reactions = await this.list({ where: { messageId } });
1031
+ const counts = /* @__PURE__ */ new Map();
1032
+ for (const reaction of reactions) {
1033
+ const existing = counts.get(reaction.emoji);
1034
+ if (existing) {
1035
+ existing.count++;
1036
+ existing.profileIds.push(reaction.profileId);
1037
+ } else {
1038
+ counts.set(reaction.emoji, {
1039
+ count: 1,
1040
+ profileIds: [reaction.profileId]
1041
+ });
1042
+ }
1043
+ }
1044
+ return counts;
1045
+ }
1046
+ /**
1047
+ * Toggle a reaction: add if not present, remove if already reacted.
1048
+ *
1049
+ * `tenantId` is bound into the existence lookup (S5 #1392) so the toggle can
1050
+ * never match or delete a reaction row belonging to another tenant. Prefer the
1051
+ * membership-checked {@link ChatService.addReaction}/{@link ChatService.removeReaction}
1052
+ * for request-driven flows; this low-level helper performs no membership check.
1053
+ */
1054
+ async toggle(messageId, profileId, emoji, tenantId2) {
1055
+ const existing = await this.list({
1056
+ where: { messageId, profileId, emoji, tenantId: tenantId2 }
1057
+ });
1058
+ if (existing.length > 0) {
1059
+ await existing[0].delete();
1060
+ return { added: false };
1061
+ }
1062
+ await this.create({
1063
+ tenantId: tenantId2,
1064
+ messageId,
1065
+ profileId,
1066
+ emoji
1067
+ });
1068
+ return { added: true };
1069
+ }
1070
+ }
1071
+ function canonicalDmRoomId(tenantId2, profileId1, profileId2) {
1072
+ const [a, b] = [profileId1, profileId2].sort();
1073
+ const key = `dm ${tenantId2} ${a} ${b}`;
1074
+ const hash = createHash("sha1").update(key).digest("hex");
1075
+ return [
1076
+ hash.slice(0, 8),
1077
+ hash.slice(8, 12),
1078
+ `5${hash.slice(13, 16)}`,
1079
+ (Number.parseInt(hash.slice(16, 18), 16) & 63 | 128).toString(16).padStart(2, "0") + hash.slice(18, 20),
1080
+ hash.slice(20, 32)
1081
+ ].join("-");
1082
+ }
1083
+ class ChatRoomCollection extends SmrtCollection {
1084
+ static _itemClass = ChatRoom;
1085
+ async findByType(roomType) {
1086
+ return this.list({ where: { roomType, status: "active" } });
1087
+ }
1088
+ async findPublic() {
1089
+ return this.list({ where: { roomType: "public", status: "active" } });
1090
+ }
1091
+ async findDMs() {
1092
+ return this.list({ where: { roomType: "dm", status: "active" } });
1093
+ }
1094
+ async findAgentRooms() {
1095
+ return this.list({ where: { roomType: "agent", status: "active" } });
1096
+ }
1097
+ /**
1098
+ * Search rooms by name/description/topic.
1099
+ *
1100
+ * Filtering and limiting are pushed into SQL via `LIKE` instead of loading
1101
+ * the entire tenant room set and filtering in JS (S5 #1392, DoS hardening).
1102
+ * The WHERE API does not support OR, so we issue one bounded query per
1103
+ * searchable column and merge the results, capping the total returned.
1104
+ */
1105
+ async search(query, options) {
1106
+ const limit = options?.limit ?? 50;
1107
+ const like = `%${query}%`;
1108
+ const columns = ["name", "description", "topic"];
1109
+ const merged = /* @__PURE__ */ new Map();
1110
+ for (const column of columns) {
1111
+ const matches = await this.list({
1112
+ where: { status: "active", [`${column} like`]: like },
1113
+ orderBy: "created_at DESC",
1114
+ limit
1115
+ });
1116
+ for (const room of matches) {
1117
+ if (room.id) merged.set(room.id, room);
1118
+ }
1119
+ if (merged.size >= limit) break;
1120
+ }
1121
+ return Array.from(merged.values()).slice(0, limit);
1122
+ }
1123
+ /**
1124
+ * Find an existing 1:1 DM room between two profiles, or create one.
1125
+ *
1126
+ * DM identity is derived from the authoritative `chat_participants` join
1127
+ * (server-controlled) rather than client-mutable room metadata (S5 #1392).
1128
+ * Callers are responsible for attaching both profiles as participants after
1129
+ * creation; {@link ChatService.getOrCreateDM} does this.
1130
+ */
1131
+ async findOrCreateDM(profileId1, profileId2, tenantId2, participants) {
1132
+ const dmId = canonicalDmRoomId(tenantId2, profileId1, profileId2);
1133
+ const canonical = await this.get({ id: dmId, tenantId: tenantId2 });
1134
+ if (canonical && canonical.roomType === "dm") {
1135
+ return canonical;
1136
+ }
1137
+ const memberships1 = await participants.list({
1138
+ where: { profileId: profileId1, status: "active", tenantId: tenantId2 }
1139
+ });
1140
+ const candidateRoomIds = memberships1.map((m) => m.roomId);
1141
+ for (const roomId of candidateRoomIds) {
1142
+ const dm = await this.get({ id: roomId, tenantId: tenantId2 });
1143
+ if (!dm || dm.roomType !== "dm" || dm.status !== "active") continue;
1144
+ const others = await participants.list({
1145
+ where: { roomId, profileId: profileId2, status: "active", tenantId: tenantId2 }
1146
+ });
1147
+ if (others.length > 0) {
1148
+ return dm;
1149
+ }
1150
+ }
1151
+ const room = await this.create({
1152
+ id: dmId,
1153
+ tenantId: tenantId2,
1154
+ name: "",
1155
+ roomType: "dm",
1156
+ status: "active",
1157
+ maxParticipants: 2
1158
+ });
1159
+ return room;
1160
+ }
1161
+ }
1162
+ class ChatThreadCollection extends SmrtCollection {
1163
+ static _itemClass = ChatThread;
1164
+ async getByRoom(roomId) {
1165
+ return this.list({ where: { roomId } });
1166
+ }
1167
+ async getActive(roomId) {
1168
+ const threads = await this.list({ where: { roomId } });
1169
+ return threads.filter((t) => !t.isResolved);
1170
+ }
1171
+ async getUnresolved() {
1172
+ const threads = await this.list({});
1173
+ return threads.filter((t) => !t.isResolved);
1174
+ }
1175
+ }
1176
+ const RUN_AGENT_REPLY = /* @__PURE__ */ Symbol("smrt-chat.runAgentReply");
1177
+ class ChatService {
1178
+ // Raw persistence collections are PRIVATE (S5 #1392). They can author/mutate
1179
+ // any row with no actor/membership check, so they must NOT appear on the
1180
+ // public `ChatService` type, and the package index no longer re-exports the
1181
+ // collection classes for direct construction around the service. Every
1182
+ // external read/write goes through the authorized facade methods below.
1183
+ #rooms;
1184
+ #messages;
1185
+ #participants;
1186
+ #threads;
1187
+ #agentSessions;
1188
+ #reactions;
1189
+ constructor(rooms, messages, participants, threads, agentSessions, reactions) {
1190
+ this.#rooms = rooms;
1191
+ this.#messages = messages;
1192
+ this.#participants = participants;
1193
+ this.#threads = threads;
1194
+ this.#agentSessions = agentSessions;
1195
+ this.#reactions = reactions;
1196
+ }
1197
+ static async create(options) {
1198
+ ObjectRegistry.registerCollection(
1199
+ "@happyvertical/smrt-chat:ChatRoom",
1200
+ ChatRoomCollection
1201
+ );
1202
+ ObjectRegistry.registerCollection(
1203
+ "@happyvertical/smrt-chat:ChatMessage",
1204
+ ChatMessageCollection
1205
+ );
1206
+ ObjectRegistry.registerCollection(
1207
+ "@happyvertical/smrt-chat:ChatParticipant",
1208
+ ChatParticipantCollection
1209
+ );
1210
+ ObjectRegistry.registerCollection(
1211
+ "@happyvertical/smrt-chat:ChatThread",
1212
+ ChatThreadCollection
1213
+ );
1214
+ ObjectRegistry.registerCollection(
1215
+ "@happyvertical/smrt-chat:AgentSession",
1216
+ AgentSessionCollection
1217
+ );
1218
+ ObjectRegistry.registerCollection(
1219
+ "@happyvertical/smrt-chat:ChatReaction",
1220
+ ChatReactionCollection
1221
+ );
1222
+ const rooms = await ObjectRegistry.getCollection(
1223
+ "@happyvertical/smrt-chat:ChatRoom",
1224
+ options
1225
+ );
1226
+ const messages = await ObjectRegistry.getCollection(
1227
+ "@happyvertical/smrt-chat:ChatMessage",
1228
+ options
1229
+ );
1230
+ const participants = await ObjectRegistry.getCollection(
1231
+ "@happyvertical/smrt-chat:ChatParticipant",
1232
+ options
1233
+ );
1234
+ const threads = await ObjectRegistry.getCollection(
1235
+ "@happyvertical/smrt-chat:ChatThread",
1236
+ options
1237
+ );
1238
+ const agentSessions = await ObjectRegistry.getCollection(
1239
+ "@happyvertical/smrt-chat:AgentSession",
1240
+ options
1241
+ );
1242
+ const reactions = await ObjectRegistry.getCollection(
1243
+ "@happyvertical/smrt-chat:ChatReaction",
1244
+ options
1245
+ );
1246
+ return new ChatService(
1247
+ rooms,
1248
+ messages,
1249
+ participants,
1250
+ threads,
1251
+ agentSessions,
1252
+ reactions
1253
+ );
1254
+ }
1255
+ /** Initialize all underlying collections (table creation) */
1256
+ async initialize() {
1257
+ await this.#rooms.initialize();
1258
+ await this.#messages.initialize();
1259
+ await this.#participants.initialize();
1260
+ await this.#threads.initialize();
1261
+ await this.#agentSessions.initialize();
1262
+ await this.#reactions.initialize();
1263
+ }
1264
+ /**
1265
+ * Create a room and add the creating actor as owner (S5 #1392).
1266
+ *
1267
+ * The acting identity is the server-supplied `actorProfileId` (the
1268
+ * authenticated principal the route injects). The creator/owner is ALWAYS the
1269
+ * actor — a caller cannot supply a `createdByProfileId` to attribute the room
1270
+ * to (and enroll as owner) some other profile.
1271
+ */
1272
+ async createRoom(params) {
1273
+ const room = await this.#rooms.create({
1274
+ tenantId: params.tenantId,
1275
+ name: params.name,
1276
+ roomType: params.roomType,
1277
+ createdByProfileId: params.actorProfileId,
1278
+ description: params.description ?? "",
1279
+ topic: params.topic ?? "",
1280
+ status: "active"
1281
+ });
1282
+ await this.#enrollParticipant({
1283
+ tenantId: params.tenantId,
1284
+ roomId: room.id,
1285
+ profileId: params.actorProfileId,
1286
+ role: "owner"
1287
+ });
1288
+ return room;
1289
+ }
1290
+ /**
1291
+ * Send a USER message to a room as the authenticated caller (S5 #1392).
1292
+ *
1293
+ * The acting identity is the server-supplied `actorProfileId` (the
1294
+ * authenticated principal the route injects). The message is ALWAYS authored
1295
+ * as `actorProfileId` with `role: 'user'` — the caller cannot supply a
1296
+ * `senderProfileId` to impersonate another profile or the agent, and cannot
1297
+ * supply a privileged `role` (assistant/system/tool). Agent-authored messages
1298
+ * go exclusively through the internal {@link ChatService.sendAgentReply}.
1299
+ *
1300
+ * Authorization: `actorProfileId` must be an ACTIVE participant of the target
1301
+ * room, preventing cross-room IDOR within a tenant. There is no public
1302
+ * membership-skip parameter; system-authored writes use the internal
1303
+ * {@link ChatService.writeMessage} path.
1304
+ */
1305
+ async sendMessage(params) {
1306
+ return this.#writeMessage({
1307
+ tenantId: params.tenantId,
1308
+ roomId: params.roomId,
1309
+ senderProfileId: params.actorProfileId,
1310
+ content: params.content,
1311
+ role: "user",
1312
+ messageType: params.messageType ?? "text",
1313
+ threadId: params.threadId ?? null,
1314
+ agentSessionId: params.agentSessionId ?? null,
1315
+ replyToMessageId: params.replyToMessageId ?? null
1316
+ });
1317
+ }
1318
+ /**
1319
+ * INTERNAL persistence path for all message writes. Not exposed publicly: it
1320
+ * accepts an arbitrary author/role and an internal-only `skipMembershipCheck`
1321
+ * (S5 #1392). Every public entry point (`sendMessage`, `sendAgentUserMessage`,
1322
+ * `sendAgentReply`) funnels through here with a server-derived author/role.
1323
+ *
1324
+ * Membership is enforced unless `skipMembershipCheck` is set, which only the
1325
+ * in-class system-authored callers may do.
1326
+ */
1327
+ async #writeMessage(write) {
1328
+ if (!write.skipMembershipCheck) {
1329
+ const isMember = await this.#participants.isActiveMember(
1330
+ write.roomId,
1331
+ write.senderProfileId,
1332
+ write.tenantId
1333
+ );
1334
+ if (!isMember) {
1335
+ throw new Error(
1336
+ "Sender is not an active member of the room (authorization denied)"
1337
+ );
1338
+ }
1339
+ }
1340
+ const thread = write.threadId ? await this.#threads.get({
1341
+ id: write.threadId,
1342
+ roomId: write.roomId,
1343
+ tenantId: write.tenantId
1344
+ }) : null;
1345
+ if (write.threadId && !thread) {
1346
+ throw new Error(
1347
+ "threadId does not belong to this room/tenant (authorization denied)"
1348
+ );
1349
+ }
1350
+ const session = write.agentSessionId ? await this.#agentSessions.get({
1351
+ id: write.agentSessionId,
1352
+ chatRoomId: write.roomId,
1353
+ tenantId: write.tenantId
1354
+ }) : null;
1355
+ if (write.agentSessionId && !session) {
1356
+ throw new Error(
1357
+ "agentSessionId does not belong to this room/tenant (authorization denied)"
1358
+ );
1359
+ }
1360
+ if (write.replyToMessageId) {
1361
+ const replyTo = await this.#messages.get({
1362
+ id: write.replyToMessageId,
1363
+ roomId: write.roomId,
1364
+ tenantId: write.tenantId
1365
+ });
1366
+ if (!replyTo) {
1367
+ throw new Error(
1368
+ "replyToMessageId does not belong to this room/tenant (authorization denied)"
1369
+ );
1370
+ }
1371
+ }
1372
+ const message = await this.#messages.create({
1373
+ tenantId: write.tenantId,
1374
+ roomId: write.roomId,
1375
+ senderProfileId: write.senderProfileId,
1376
+ content: write.content,
1377
+ messageType: write.messageType ?? "text",
1378
+ role: write.role,
1379
+ threadId: write.threadId ?? null,
1380
+ agentSessionId: write.agentSessionId ?? null,
1381
+ replyToMessageId: write.replyToMessageId ?? null,
1382
+ toolCallData: write.toolCallData ? JSON.stringify(write.toolCallData) : null
1383
+ });
1384
+ const room = await this.#rooms.get({
1385
+ id: write.roomId,
1386
+ tenantId: write.tenantId
1387
+ });
1388
+ if (room) {
1389
+ room.lastMessageAt = /* @__PURE__ */ new Date();
1390
+ await room.save();
1391
+ }
1392
+ if (thread) {
1393
+ thread.messageCount++;
1394
+ thread.lastMessageAt = /* @__PURE__ */ new Date();
1395
+ await thread.save();
1396
+ }
1397
+ if (session) {
1398
+ await session.recordMessage();
1399
+ }
1400
+ return message;
1401
+ }
1402
+ /**
1403
+ * Start a thread in a room (S5 #1392).
1404
+ *
1405
+ * The acting identity is the server-supplied `actorProfileId`, which must be
1406
+ * an active member of the room. Generated thread `create` is disabled, so this
1407
+ * is the only path to create a thread.
1408
+ *
1409
+ * When a `rootMessageId` is supplied it is bound to `{ id, roomId, tenantId }`
1410
+ * and rejected unless it belongs to the SAME room and tenant — without this a
1411
+ * member of one room could anchor a thread to a message from another
1412
+ * room/tenant. `rootMessageId` is optional (a thread can be opened without a
1413
+ * root message, e.g. an agent-editor thread).
1414
+ */
1415
+ async startThread(params) {
1416
+ await this.#requireActiveMembership(
1417
+ params.roomId,
1418
+ params.actorProfileId,
1419
+ params.tenantId
1420
+ );
1421
+ if (params.rootMessageId) {
1422
+ const rootMessage = await this.#messages.get({
1423
+ id: params.rootMessageId,
1424
+ roomId: params.roomId,
1425
+ tenantId: params.tenantId
1426
+ });
1427
+ if (!rootMessage) {
1428
+ throw new Error(
1429
+ "rootMessageId does not belong to this room/tenant (authorization denied)"
1430
+ );
1431
+ }
1432
+ }
1433
+ const thread = await this.#threads.create({
1434
+ tenantId: params.tenantId,
1435
+ roomId: params.roomId,
1436
+ rootMessageId: params.rootMessageId ?? null,
1437
+ title: params.title ?? "",
1438
+ messageCount: 0
1439
+ });
1440
+ return thread;
1441
+ }
1442
+ /**
1443
+ * Add a participant to a room (S5 #1392).
1444
+ *
1445
+ * Authorization: the acting identity is the server-supplied `actorProfileId`,
1446
+ * which MUST be an owner/admin of the target room. This prevents an arbitrary
1447
+ * tenant member from adding anyone (or themselves) to any room with any role —
1448
+ * a privilege-escalation / IDOR. System-bootstrap enrollment (room creation,
1449
+ * DM/agent-session setup) uses the internal {@link ChatService.enrollParticipant}.
1450
+ */
1451
+ async addParticipant(params) {
1452
+ await this.#requireRoomAdmin(
1453
+ params.roomId,
1454
+ params.actorProfileId,
1455
+ params.tenantId
1456
+ );
1457
+ return this.#enrollParticipant({
1458
+ tenantId: params.tenantId,
1459
+ roomId: params.roomId,
1460
+ profileId: params.profileId,
1461
+ role: params.role
1462
+ });
1463
+ }
1464
+ /**
1465
+ * INTERNAL participant enrollment (S5 #1392). No authorization check — only
1466
+ * the trusted in-class bootstrap paths (room creation, DM/agent-session setup)
1467
+ * and the owner-checked {@link ChatService.addParticipant} call this. Not
1468
+ * exposed publicly so a route cannot enroll an arbitrary profile.
1469
+ */
1470
+ async #enrollParticipant(params) {
1471
+ const existing = await this.#participants.findMembership(
1472
+ params.roomId,
1473
+ params.profileId,
1474
+ params.tenantId
1475
+ );
1476
+ if (existing) {
1477
+ if (existing.status !== "active") {
1478
+ existing.status = "active";
1479
+ existing.joinedAt = /* @__PURE__ */ new Date();
1480
+ await existing.save();
1481
+ }
1482
+ return existing;
1483
+ }
1484
+ const participant = await this.#participants.create({
1485
+ tenantId: params.tenantId,
1486
+ roomId: params.roomId,
1487
+ profileId: params.profileId,
1488
+ role: params.role ?? "member",
1489
+ status: "active",
1490
+ joinedAt: /* @__PURE__ */ new Date()
1491
+ });
1492
+ return participant;
1493
+ }
1494
+ /**
1495
+ * Remove (soft-leave) a participant from a room (S5 #1392).
1496
+ *
1497
+ * Authorization: the server-supplied `actorProfileId` may remove THEMSELVES
1498
+ * (leave) at any time; removing ANOTHER profile requires the actor to be an
1499
+ * owner/admin of the room. An admin who is not an owner cannot remove an owner.
1500
+ */
1501
+ async removeParticipant(params) {
1502
+ const target = await this.#participants.findMembership(
1503
+ params.roomId,
1504
+ params.profileId,
1505
+ params.tenantId
1506
+ );
1507
+ if (!target) return;
1508
+ const isSelf = params.actorProfileId === params.profileId;
1509
+ if (!isSelf) {
1510
+ const actor = await this.#participants.findActiveMembership(
1511
+ params.roomId,
1512
+ params.actorProfileId,
1513
+ params.tenantId
1514
+ );
1515
+ if (!actor || !actor.isAdmin()) {
1516
+ throw new Error(
1517
+ "Only a room owner/admin may remove another participant (authorization denied)"
1518
+ );
1519
+ }
1520
+ if (target.isOwner() && !actor.isOwner()) {
1521
+ throw new Error(
1522
+ "Only a room owner may remove an owner (authorization denied)"
1523
+ );
1524
+ }
1525
+ }
1526
+ target.status = "left";
1527
+ await target.save();
1528
+ }
1529
+ /**
1530
+ * Update mutable room fields, restricted to a room owner/admin (S5 #1392).
1531
+ *
1532
+ * Generated `update` on ChatRoom is disabled, so this owner-checked path is the
1533
+ * only way to mutate room state. The acting identity is the server-supplied
1534
+ * `actorProfileId`.
1535
+ */
1536
+ async updateRoom(params) {
1537
+ await this.#requireRoomAdmin(
1538
+ params.roomId,
1539
+ params.actorProfileId,
1540
+ params.tenantId
1541
+ );
1542
+ const room = await this.#rooms.get({
1543
+ id: params.roomId,
1544
+ tenantId: params.tenantId
1545
+ });
1546
+ if (!room) throw new Error("Room not found");
1547
+ if (params.name !== void 0) room.name = params.name;
1548
+ if (params.description !== void 0) room.description = params.description;
1549
+ if (params.topic !== void 0) room.topic = params.topic;
1550
+ if (params.avatarUrl !== void 0) room.avatarUrl = params.avatarUrl;
1551
+ if (params.status !== void 0) room.status = params.status;
1552
+ await room.save();
1553
+ return room;
1554
+ }
1555
+ /**
1556
+ * Add a reaction to a message as the authenticated caller (S5 #1392).
1557
+ *
1558
+ * Generated `create` on ChatReaction is disabled. The reaction is always
1559
+ * authored as the server-supplied `actorProfileId` (no caller-supplied
1560
+ * `profileId`), and the actor must be an active member of the room that owns
1561
+ * the message. Idempotent: re-reacting with the same emoji returns the
1562
+ * existing row.
1563
+ */
1564
+ async addReaction(params) {
1565
+ const message = await this.#messages.get({
1566
+ id: params.messageId,
1567
+ tenantId: params.tenantId
1568
+ });
1569
+ if (!message) throw new Error("Message not found");
1570
+ await this.#requireActiveMembership(
1571
+ message.roomId,
1572
+ params.actorProfileId,
1573
+ params.tenantId
1574
+ );
1575
+ const existing = await this.#reactions.list({
1576
+ where: {
1577
+ tenantId: params.tenantId,
1578
+ messageId: params.messageId,
1579
+ profileId: params.actorProfileId,
1580
+ emoji: params.emoji
1581
+ },
1582
+ limit: 1
1583
+ });
1584
+ if (existing[0]) return existing[0];
1585
+ return this.#reactions.create({
1586
+ tenantId: params.tenantId,
1587
+ messageId: params.messageId,
1588
+ profileId: params.actorProfileId,
1589
+ emoji: params.emoji
1590
+ });
1591
+ }
1592
+ /**
1593
+ * Remove the caller's own reaction from a message (S5 #1392).
1594
+ *
1595
+ * Generated `delete` on ChatReaction is disabled. A caller may only delete
1596
+ * THEIR OWN reaction (keyed on `actorProfileId`), so the route cannot remove
1597
+ * another member's reaction.
1598
+ */
1599
+ async removeReaction(params) {
1600
+ const existing = await this.#reactions.list({
1601
+ where: {
1602
+ tenantId: params.tenantId,
1603
+ messageId: params.messageId,
1604
+ profileId: params.actorProfileId,
1605
+ emoji: params.emoji
1606
+ },
1607
+ limit: 1
1608
+ });
1609
+ if (existing[0]) {
1610
+ await existing[0].delete();
1611
+ return true;
1612
+ }
1613
+ return false;
1614
+ }
1615
+ /**
1616
+ * Get or create a DM room with auto-participant setup.
1617
+ *
1618
+ * The acting identity is the server-supplied `actorProfileId`, which must be
1619
+ * one of the two DM participants — a caller cannot open a DM between two other
1620
+ * profiles on their behalf (S5 #1392). Enrollment uses the internal system
1621
+ * path (no owner check needed for a DM the actor is part of).
1622
+ */
1623
+ async getOrCreateDM(params) {
1624
+ if (params.actorProfileId !== params.profileId1 && params.actorProfileId !== params.profileId2) {
1625
+ throw new Error(
1626
+ "Caller must be a participant of the DM (authorization denied)"
1627
+ );
1628
+ }
1629
+ const room = await this.#rooms.findOrCreateDM(
1630
+ params.profileId1,
1631
+ params.profileId2,
1632
+ params.tenantId,
1633
+ this.#participants
1634
+ );
1635
+ await this.#enrollParticipant({
1636
+ tenantId: params.tenantId,
1637
+ roomId: room.id,
1638
+ profileId: params.profileId1
1639
+ });
1640
+ await this.#enrollParticipant({
1641
+ tenantId: params.tenantId,
1642
+ roomId: room.id,
1643
+ profileId: params.profileId2
1644
+ });
1645
+ return room;
1646
+ }
1647
+ /**
1648
+ * Create an agent conversation session with a linked chat room (S5 #1392).
1649
+ *
1650
+ * The acting identity is the server-supplied `actorProfileId`; the session is
1651
+ * ALWAYS created for that actor as the owning participant. A caller cannot
1652
+ * supply a `participantProfileId` to open (and own) a session on behalf of
1653
+ * another profile.
1654
+ *
1655
+ * `sessionKey` scopes the session's identity to a conversation subject (e.g. a
1656
+ * content id) (S5 #1392). The reuse lookup keys on `(agentId,
1657
+ * participantProfileId, tenantId)`, which is too coarse for callers that open
1658
+ * separate conversations for distinct subjects under one agent/profile/tenant:
1659
+ * without a key, a session opened for subject A would be reused for a request
1660
+ * about subject B, returning A's room/threads on B's route. When `sessionKey`
1661
+ * is set, an existing session is reused ONLY if its key matches exactly, and a
1662
+ * newly created session records the key; distinct keys therefore get distinct
1663
+ * sessions and rooms. When omitted, behavior is unchanged (single session per
1664
+ * agent/profile/tenant).
1665
+ */
1666
+ async createAgentSession(params) {
1667
+ const participantProfileId = params.actorProfileId;
1668
+ const sessionKey = params.sessionKey ?? null;
1669
+ const existingSession = await this.#agentSessions.findActiveSession(
1670
+ params.agentId,
1671
+ participantProfileId,
1672
+ params.tenantId,
1673
+ sessionKey
1674
+ );
1675
+ if (existingSession && existingSession.tenantId === params.tenantId) {
1676
+ if (existingSession.chatRoomId) {
1677
+ const existingRoom = await this.#rooms.get({
1678
+ id: existingSession.chatRoomId,
1679
+ tenantId: params.tenantId
1680
+ });
1681
+ if (existingRoom) {
1682
+ await this.#enrollParticipant({
1683
+ tenantId: params.tenantId,
1684
+ roomId: existingRoom.id,
1685
+ profileId: participantProfileId,
1686
+ role: "owner"
1687
+ });
1688
+ await this.#enrollParticipant({
1689
+ tenantId: params.tenantId,
1690
+ roomId: existingRoom.id,
1691
+ profileId: params.agentId,
1692
+ role: "member"
1693
+ });
1694
+ return { session: existingSession, room: existingRoom };
1695
+ }
1696
+ }
1697
+ await existingSession.expire();
1698
+ }
1699
+ const room = await this.#rooms.create({
1700
+ tenantId: params.tenantId,
1701
+ name: "",
1702
+ roomType: "agent",
1703
+ createdByProfileId: participantProfileId,
1704
+ status: "active",
1705
+ maxParticipants: 2
1706
+ });
1707
+ await this.#enrollParticipant({
1708
+ tenantId: params.tenantId,
1709
+ roomId: room.id,
1710
+ profileId: participantProfileId,
1711
+ role: "owner"
1712
+ });
1713
+ await this.#enrollParticipant({
1714
+ tenantId: params.tenantId,
1715
+ roomId: room.id,
1716
+ profileId: params.agentId,
1717
+ role: "member"
1718
+ });
1719
+ const session = await this.#agentSessions.findOrCreate({
1720
+ agentId: params.agentId,
1721
+ participantProfileId,
1722
+ tenantId: params.tenantId,
1723
+ allowedTools: params.allowedTools,
1724
+ chatRoomId: room.id,
1725
+ systemPrompt: params.systemPrompt,
1726
+ sessionKey
1727
+ });
1728
+ if (params.maxTokens) session.maxTokens = params.maxTokens;
1729
+ if (params.maxMessages) session.maxMessages = params.maxMessages;
1730
+ await session.save();
1731
+ return { session, room };
1732
+ }
1733
+ /**
1734
+ * Send a USER message within an agent session (S5 #1392).
1735
+ *
1736
+ * The authenticated caller (`actorProfileId`) must be the session's owning
1737
+ * participant. The message is always authored as `session.participantProfileId`
1738
+ * — the caller cannot supply a `senderProfileId`, a `role`, or tool-call data,
1739
+ * so this path can never be used to post as the agent (`assistant`/`tool`) or
1740
+ * to impersonate another profile. Agent replies go through the internal
1741
+ * {@link ChatService.sendAgentReply}.
1742
+ */
1743
+ async sendAgentUserMessage(params) {
1744
+ const session = await this.#loadActiveSession(
1745
+ params.agentSessionId,
1746
+ params.tenantId
1747
+ );
1748
+ if (params.actorProfileId !== session.participantProfileId) {
1749
+ throw new Error(
1750
+ "Only the session participant may post user messages in this agent session (authorization denied)"
1751
+ );
1752
+ }
1753
+ return this.#writeMessage({
1754
+ tenantId: params.tenantId,
1755
+ roomId: session.chatRoomId,
1756
+ senderProfileId: session.participantProfileId,
1757
+ content: params.content,
1758
+ role: "user",
1759
+ messageType: params.messageType ?? "text",
1760
+ agentSessionId: params.agentSessionId
1761
+ });
1762
+ }
1763
+ /**
1764
+ * Emit an ASSISTANT or TOOL message authored by the agent (S5 #1392).
1765
+ *
1766
+ * INTERNAL authority: this is the only path that authors a message as the
1767
+ * agent, and it is not reachable with a caller-supplied `senderProfileId`/
1768
+ * `role`. The author is always `session.agentId`. Tool calls are gated
1769
+ * fail-closed against the session's allow-list. Intended for the trusted
1770
+ * agent-runtime, never a per-tenant request handler driven by client-supplied
1771
+ * role/sender.
1772
+ *
1773
+ * This is a `private` method and is NOT exported from the package index. The
1774
+ * in-process agent runtime reaches it through the {@link sendAgentReply}
1775
+ * bridge in this module, which is deliberately not re-exported from
1776
+ * `src/index.ts`, so a route/consumer can never author a message as the agent.
1777
+ */
1778
+ async #emitAgentReply(params) {
1779
+ const session = await this.#loadActiveSession(
1780
+ params.agentSessionId,
1781
+ params.tenantId
1782
+ );
1783
+ const role = params.kind === "tool" ? "tool" : "assistant";
1784
+ if (params.messageType === "tool_call" || role === "tool" || params.toolCallData) {
1785
+ const toolName = ChatService.#extractToolName(params.toolCallData);
1786
+ if (!toolName) {
1787
+ throw new Error("Tool call is missing a tool name");
1788
+ }
1789
+ if (!session.isToolAllowed(toolName)) {
1790
+ throw new Error(
1791
+ `Tool '${toolName}' is not allowed for this agent session (authorization denied)`
1792
+ );
1793
+ }
1794
+ }
1795
+ return this.#writeMessage({
1796
+ tenantId: params.tenantId,
1797
+ roomId: session.chatRoomId,
1798
+ senderProfileId: session.agentId,
1799
+ content: params.content,
1800
+ role,
1801
+ messageType: params.messageType ?? "text",
1802
+ threadId: params.threadId ?? null,
1803
+ agentSessionId: params.agentSessionId,
1804
+ toolCallData: params.toolCallData ?? null
1805
+ });
1806
+ }
1807
+ /** Load an active agent session by id, tenant-bound, or throw. */
1808
+ async #loadActiveSession(agentSessionId, tenantId2) {
1809
+ const session = await this.#agentSessions.get({
1810
+ id: agentSessionId,
1811
+ tenantId: tenantId2
1812
+ });
1813
+ if (!session) throw new Error("Agent session not found");
1814
+ if (!session.isActive()) throw new Error("Agent session is not active");
1815
+ if (!session.chatRoomId) throw new Error("Agent session has no chat room");
1816
+ return session;
1817
+ }
1818
+ /**
1819
+ * Read messages in a room, gated on the AUTHENTICATED CALLER's active
1820
+ * membership (S5 #1392).
1821
+ *
1822
+ * The acting identity is the server-supplied `actorProfileId` (the
1823
+ * authenticated principal the route injects), NOT a caller-controlled
1824
+ * `profileId`. Authorizing a supplied `profileId` would make a route a
1825
+ * confused deputy: any caller could read a room by smuggling some member's
1826
+ * profile id. Throws if `actorProfileId` is not an active participant of
1827
+ * `roomId`. `tenantId` is required so the membership gate is always
1828
+ * tenant-scoped.
1829
+ */
1830
+ async getRoomMessages(params) {
1831
+ await this.#requireActiveMembership(
1832
+ params.roomId,
1833
+ params.actorProfileId,
1834
+ params.tenantId
1835
+ );
1836
+ return this.#messages.getByRoom(params.roomId, params.tenantId, {
1837
+ limit: params.limit,
1838
+ before: params.before
1839
+ });
1840
+ }
1841
+ /**
1842
+ * Load a room only if the AUTHENTICATED CALLER is an active member (S5 #1392).
1843
+ *
1844
+ * The acting identity is the server-supplied `actorProfileId`, never a
1845
+ * caller-controlled `profileId` (confused-deputy avoidance — see
1846
+ * {@link ChatService.getRoomMessages}). Returns null when the room does not
1847
+ * exist; throws when the actor is not an active participant. `tenantId` is
1848
+ * required so the lookup and the membership gate are always tenant-scoped.
1849
+ */
1850
+ async getRoomForMember(roomId, actorProfileId, tenantId2) {
1851
+ const room = await this.#rooms.get({ id: roomId, tenantId: tenantId2 });
1852
+ if (!room) return null;
1853
+ await this.#requireActiveMembership(roomId, actorProfileId, tenantId2);
1854
+ return room;
1855
+ }
1856
+ /**
1857
+ * Tenant-bound read of a single agent session (S5 #1392).
1858
+ *
1859
+ * The lookup ALWAYS binds `tenantId` (including the `null`/untenanted scope),
1860
+ * so a caller can never resolve a session belonging to another tenant by id.
1861
+ * Returns `null` when no session matches the id within the tenant. Replaces
1862
+ * direct `agentSessions.get(id)` reach-ins in package consumers; consumers
1863
+ * still apply their own ownership/context authorization on the returned row.
1864
+ */
1865
+ async getAgentSession(lookup) {
1866
+ const session = await this.#agentSessions.get({
1867
+ id: lookup.agentSessionId,
1868
+ tenantId: lookup.tenantId
1869
+ });
1870
+ return session ?? null;
1871
+ }
1872
+ /**
1873
+ * Tenant-bound list of ACTIVE agent sessions for an (agent, participant) pair
1874
+ * (S5 #1392).
1875
+ *
1876
+ * Binds `tenantId` into the query so the result can never include a session
1877
+ * from another tenant. Replaces direct `agentSessions.list({ where })`
1878
+ * reach-ins; consumers apply their own per-session context authorization.
1879
+ */
1880
+ async findActiveAgentSessions(params) {
1881
+ const sessions = await this.#agentSessions.list({
1882
+ where: {
1883
+ tenantId: params.tenantId,
1884
+ agentId: params.agentId,
1885
+ participantProfileId: params.participantProfileId,
1886
+ status: "active"
1887
+ }
1888
+ });
1889
+ return sessions.filter((s) => s.isActive());
1890
+ }
1891
+ /**
1892
+ * Tenant-bound read of a single thread (S5 #1392).
1893
+ *
1894
+ * Binds `tenantId` into the lookup so a thread from another tenant can never
1895
+ * be resolved by id. Returns `null` when no thread matches within the tenant.
1896
+ * Replaces direct `threads.get(id)` reach-ins.
1897
+ */
1898
+ async getThread(lookup) {
1899
+ const thread = await this.#threads.get({
1900
+ id: lookup.threadId,
1901
+ tenantId: lookup.tenantId
1902
+ });
1903
+ return thread ?? null;
1904
+ }
1905
+ /**
1906
+ * List a room's threads, gated on the caller's active membership (S5 #1392).
1907
+ *
1908
+ * Tenant- and membership-scoped: throws if `actorProfileId` is not an active
1909
+ * participant of `roomId`. Replaces direct `threads.list({ where: { roomId } })`
1910
+ * reach-ins that returned threads without a membership/tenant gate.
1911
+ */
1912
+ async listRoomThreads(params) {
1913
+ await this.#requireActiveMembership(
1914
+ params.roomId,
1915
+ params.actorProfileId,
1916
+ params.tenantId
1917
+ );
1918
+ return this.#threads.list({
1919
+ where: { tenantId: params.tenantId, roomId: params.roomId },
1920
+ orderBy: "createdAt DESC"
1921
+ });
1922
+ }
1923
+ /**
1924
+ * Read messages within a thread, tenant- and membership-bound (S5 #1392).
1925
+ *
1926
+ * The thread is resolved tenant-bound; the caller must be an active member of
1927
+ * the thread's room. Messages are returned oldest-first (chronological).
1928
+ * Replaces direct `messages.list({ where: { threadId } })` reach-ins that
1929
+ * could read another tenant's/room's thread history by raw id.
1930
+ */
1931
+ async getThreadMessages(params) {
1932
+ const thread = await this.#threads.get({
1933
+ id: params.threadId,
1934
+ tenantId: params.tenantId
1935
+ });
1936
+ if (!thread) {
1937
+ throw new Error("Thread not found");
1938
+ }
1939
+ await this.#requireActiveMembership(
1940
+ thread.roomId,
1941
+ params.actorProfileId,
1942
+ params.tenantId
1943
+ );
1944
+ const messages = await this.#messages.list({
1945
+ where: {
1946
+ tenantId: params.tenantId,
1947
+ threadId: params.threadId,
1948
+ isDeleted: false
1949
+ },
1950
+ orderBy: "created_at DESC",
1951
+ limit: params.limit
1952
+ });
1953
+ return messages.reverse();
1954
+ }
1955
+ /**
1956
+ * Update the per-session agent configuration (allowedTools / systemPrompt),
1957
+ * restricted to the session owner — the room owner participant (S5 #1392).
1958
+ *
1959
+ * Tool whitelist and system prompt govern what the agent may do, so only the
1960
+ * owning participant (not arbitrary tenant members or the agent itself) may
1961
+ * mutate them.
1962
+ */
1963
+ async updateAgentSessionConfig(params) {
1964
+ const session = await this.#agentSessions.get({
1965
+ id: params.agentSessionId,
1966
+ tenantId: params.tenantId
1967
+ });
1968
+ if (!session) throw new Error("Agent session not found");
1969
+ const isOwner = params.actorProfileId === session.participantProfileId;
1970
+ if (!isOwner) {
1971
+ throw new Error(
1972
+ "Only the session owner may update agent session configuration (authorization denied)"
1973
+ );
1974
+ }
1975
+ if (params.allowedTools !== void 0) {
1976
+ session.setAllowedTools(params.allowedTools);
1977
+ }
1978
+ if (params.systemPrompt !== void 0) {
1979
+ session.systemPrompt = params.systemPrompt;
1980
+ }
1981
+ await session.save();
1982
+ return session;
1983
+ }
1984
+ /** Throw unless the profile is an active participant of the room. */
1985
+ async #requireActiveMembership(roomId, profileId, tenantId2) {
1986
+ const isMember = await this.#participants.isActiveMember(
1987
+ roomId,
1988
+ profileId,
1989
+ tenantId2
1990
+ );
1991
+ if (!isMember) {
1992
+ throw new Error(
1993
+ "Caller is not an active member of the room (authorization denied)"
1994
+ );
1995
+ }
1996
+ }
1997
+ /** Throw unless the profile is an active owner/admin of the room. */
1998
+ async #requireRoomAdmin(roomId, profileId, tenantId2) {
1999
+ const actor = await this.#participants.findActiveMembership(
2000
+ roomId,
2001
+ profileId,
2002
+ tenantId2
2003
+ );
2004
+ if (!actor || !actor.isAdmin()) {
2005
+ throw new Error(
2006
+ "Caller must be a room owner/admin (authorization denied)"
2007
+ );
2008
+ }
2009
+ }
2010
+ /** Extract a tool name from tool-call payload data, if present. */
2011
+ static #extractToolName(data) {
2012
+ if (!data || typeof data !== "object") return null;
2013
+ const candidate = data.name ?? data.tool ?? data.toolName;
2014
+ return typeof candidate === "string" && candidate.length > 0 ? candidate : null;
2015
+ }
2016
+ /**
2017
+ * Symbol-keyed bridge to the `private` {@link ChatService.emitAgentReply} for
2018
+ * the module-local {@link sendAgentReply} function (S5 #1392). A static member
2019
+ * may reach a private instance member of its own class, so this is the
2020
+ * sanctioned "friend" access without widening the public instance surface.
2021
+ *
2022
+ * Keyed on the module-private {@link RUN_AGENT_REPLY} symbol — NOT a named
2023
+ * static — so it does not appear on the `ChatService` type, is not enumerable,
2024
+ * and is callable only by code holding the (non-exported) symbol. This is the
2025
+ * sole path the agent-runtime bridge uses to author a message as the agent.
2026
+ */
2027
+ static [RUN_AGENT_REPLY](service, params) {
2028
+ return service.#emitAgentReply(params);
2029
+ }
2030
+ }
2031
+ function sendAgentReply(service, params) {
2032
+ return ChatService[RUN_AGENT_REPLY](service, params);
2033
+ }
2034
+ export {
2035
+ AgentSession as A,
2036
+ ChatMessage as C,
2037
+ ChatParticipant as a,
2038
+ ChatReaction as b,
2039
+ ChatRoom as c,
2040
+ ChatService as d,
2041
+ ChatThread as e,
2042
+ sendAgentReply as s
2043
+ };
2044
+ //# sourceMappingURL=ChatService-Dpzc1Pa5.js.map