@things-factory/ai-assistant 10.1.26 → 10.1.28

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 (194) hide show
  1. package/client/components/assistant-chat.ts +653 -59
  2. package/client/components/assistant-mode.test.ts +86 -55
  3. package/client/components/assistant-mode.ts +124 -82
  4. package/client/components/chat-echo-dedup.test.ts +23 -0
  5. package/client/components/chat-echo-dedup.ts +29 -3
  6. package/client/components/chat-input-builder.test.ts +20 -20
  7. package/client/components/chat-input-builder.ts +16 -14
  8. package/client/components/chat-proposal-survival.test.ts +73 -0
  9. package/client/components/mention-popup.ts +31 -8
  10. package/client/components/panel-aria.test.ts +34 -0
  11. package/client/components/proposal-outcome-line.ts +33 -0
  12. package/client/utils/assistant-session-controller.test.ts +75 -0
  13. package/client/utils/assistant-session-controller.ts +23 -0
  14. package/client/utils/assistant-session-transport.ts +11 -3
  15. package/dist-client/components/assistant-chat.d.ts +128 -13
  16. package/dist-client/components/assistant-chat.js +613 -67
  17. package/dist-client/components/assistant-chat.js.map +1 -1
  18. package/dist-client/components/assistant-mode.d.ts +80 -68
  19. package/dist-client/components/assistant-mode.js +63 -19
  20. package/dist-client/components/assistant-mode.js.map +1 -1
  21. package/dist-client/components/assistant-mode.test.js +74 -53
  22. package/dist-client/components/assistant-mode.test.js.map +1 -1
  23. package/dist-client/components/chat-echo-dedup.d.ts +2 -0
  24. package/dist-client/components/chat-echo-dedup.js +27 -4
  25. package/dist-client/components/chat-echo-dedup.js.map +1 -1
  26. package/dist-client/components/chat-echo-dedup.test.js +20 -0
  27. package/dist-client/components/chat-echo-dedup.test.js.map +1 -1
  28. package/dist-client/components/chat-input-builder.d.ts +12 -7
  29. package/dist-client/components/chat-input-builder.js +8 -10
  30. package/dist-client/components/chat-input-builder.js.map +1 -1
  31. package/dist-client/components/chat-input-builder.test.js +17 -18
  32. package/dist-client/components/chat-input-builder.test.js.map +1 -1
  33. package/dist-client/components/chat-proposal-survival.test.d.ts +1 -0
  34. package/dist-client/components/chat-proposal-survival.test.js +61 -0
  35. package/dist-client/components/chat-proposal-survival.test.js.map +1 -0
  36. package/dist-client/components/mention-popup.js +28 -8
  37. package/dist-client/components/mention-popup.js.map +1 -1
  38. package/dist-client/components/panel-aria.test.d.ts +1 -0
  39. package/dist-client/components/panel-aria.test.js +31 -0
  40. package/dist-client/components/panel-aria.test.js.map +1 -0
  41. package/dist-client/components/proposal-outcome-line.d.ts +9 -0
  42. package/dist-client/components/proposal-outcome-line.js +15 -0
  43. package/dist-client/components/proposal-outcome-line.js.map +1 -0
  44. package/dist-client/tsconfig.tsbuildinfo +1 -1
  45. package/dist-client/utils/assistant-session-controller.d.ts +10 -0
  46. package/dist-client/utils/assistant-session-controller.js +16 -0
  47. package/dist-client/utils/assistant-session-controller.js.map +1 -1
  48. package/dist-client/utils/assistant-session-controller.test.d.ts +1 -0
  49. package/dist-client/utils/assistant-session-controller.test.js +56 -0
  50. package/dist-client/utils/assistant-session-controller.test.js.map +1 -0
  51. package/dist-client/utils/assistant-session-transport.js +11 -3
  52. package/dist-client/utils/assistant-session-transport.js.map +1 -1
  53. package/dist-server/service/assistant-capability-resolver.d.ts +17 -0
  54. package/dist-server/service/assistant-capability-resolver.js +88 -0
  55. package/dist-server/service/assistant-capability-resolver.js.map +1 -0
  56. package/dist-server/service/assistant-chat-resolver.d.ts +6 -6
  57. package/dist-server/service/assistant-chat-resolver.js +166 -44
  58. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  59. package/dist-server/service/assistant-mode/assistant-mode-resolver.d.ts +25 -0
  60. package/dist-server/service/assistant-mode/assistant-mode-resolver.js +140 -0
  61. package/dist-server/service/assistant-mode/assistant-mode-resolver.js.map +1 -0
  62. package/dist-server/service/assistant-mode/index.d.ts +9 -0
  63. package/dist-server/service/assistant-mode/index.js +19 -0
  64. package/dist-server/service/assistant-mode/index.js.map +1 -0
  65. package/dist-server/service/assistant-mode/registry.d.ts +18 -0
  66. package/dist-server/service/assistant-mode/registry.js +136 -0
  67. package/dist-server/service/assistant-mode/registry.js.map +1 -0
  68. package/dist-server/service/assistant-mode/resolve.d.ts +33 -0
  69. package/dist-server/service/assistant-mode/resolve.js +60 -0
  70. package/dist-server/service/assistant-mode/resolve.js.map +1 -0
  71. package/dist-server/service/assistant-mode/types.d.ts +99 -0
  72. package/dist-server/service/assistant-mode/types.js +12 -0
  73. package/dist-server/service/assistant-mode/types.js.map +1 -0
  74. package/dist-server/service/chat-message/carried-turns.d.ts +48 -0
  75. package/dist-server/service/chat-message/carried-turns.js +80 -0
  76. package/dist-server/service/chat-message/carried-turns.js.map +1 -0
  77. package/dist-server/service/chat-message/chat-message-subscription.d.ts +1 -1
  78. package/dist-server/service/chat-message/chat-message-subscription.js +24 -6
  79. package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -1
  80. package/dist-server/service/chat-message/chat-message.d.ts +32 -0
  81. package/dist-server/service/chat-message/chat-message.js +17 -0
  82. package/dist-server/service/chat-message/chat-message.js.map +1 -1
  83. package/dist-server/service/chat-message/door-access.d.ts +15 -0
  84. package/dist-server/service/chat-message/door-access.js +26 -0
  85. package/dist-server/service/chat-message/door-access.js.map +1 -0
  86. package/dist-server/service/chat-message/fold-session-history.d.ts +30 -0
  87. package/dist-server/service/chat-message/fold-session-history.js +133 -0
  88. package/dist-server/service/chat-message/fold-session-history.js.map +1 -0
  89. package/dist-server/service/chat-message/llm-history.d.ts +7 -0
  90. package/dist-server/service/chat-message/llm-history.js +23 -11
  91. package/dist-server/service/chat-message/llm-history.js.map +1 -1
  92. package/dist-server/service/chat-message/message-doors.d.ts +29 -0
  93. package/dist-server/service/chat-message/message-doors.js +107 -0
  94. package/dist-server/service/chat-message/message-doors.js.map +1 -0
  95. package/dist-server/service/chat-message/proposal-outcome.d.ts +18 -0
  96. package/dist-server/service/chat-message/proposal-outcome.js +31 -0
  97. package/dist-server/service/chat-message/proposal-outcome.js.map +1 -0
  98. package/dist-server/service/chat-message/reader-gate.d.ts +3 -0
  99. package/dist-server/service/chat-message/reader-gate.js +37 -0
  100. package/dist-server/service/chat-message/reader-gate.js.map +1 -0
  101. package/dist-server/service/chat-message/room-message.d.ts +4 -0
  102. package/dist-server/service/chat-message/room-message.js +25 -0
  103. package/dist-server/service/chat-message/room-message.js.map +1 -0
  104. package/dist-server/service/chat-session/chat-session.d.ts +24 -0
  105. package/dist-server/service/chat-session/chat-session.js +26 -2
  106. package/dist-server/service/chat-session/chat-session.js.map +1 -1
  107. package/dist-server/service/chat-session/session-inbox.d.ts +4 -1
  108. package/dist-server/service/chat-session/session-inbox.js +3 -2
  109. package/dist-server/service/chat-session/session-inbox.js.map +1 -1
  110. package/dist-server/service/chat-session/session-visibility.d.ts +15 -0
  111. package/dist-server/service/chat-session/session-visibility.js +28 -0
  112. package/dist-server/service/chat-session/session-visibility.js.map +1 -0
  113. package/dist-server/service/chat-session/station-session-guard.d.ts +1 -5
  114. package/dist-server/service/chat-session/station-session-guard.js +7 -2
  115. package/dist-server/service/chat-session/station-session-guard.js.map +1 -1
  116. package/dist-server/service/chat-session-participant/mention-invite.d.ts +29 -0
  117. package/dist-server/service/chat-session-participant/mention-invite.js +103 -0
  118. package/dist-server/service/chat-session-participant/mention-invite.js.map +1 -0
  119. package/dist-server/service/chat-session-resolver.d.ts +15 -0
  120. package/dist-server/service/chat-session-resolver.js +267 -15
  121. package/dist-server/service/chat-session-resolver.js.map +1 -1
  122. package/dist-server/service/grounding-sources.d.ts +14 -0
  123. package/dist-server/service/grounding-sources.js +26 -0
  124. package/dist-server/service/grounding-sources.js.map +1 -0
  125. package/dist-server/service/index.d.ts +8 -1
  126. package/dist-server/service/index.js +19 -0
  127. package/dist-server/service/index.js.map +1 -1
  128. package/dist-server/service/request-translator.d.ts +2 -0
  129. package/dist-server/service/request-translator.js +20 -0
  130. package/dist-server/service/request-translator.js.map +1 -0
  131. package/dist-server/service/testing/conversation-script.d.ts +123 -0
  132. package/dist-server/service/testing/conversation-script.js +202 -0
  133. package/dist-server/service/testing/conversation-script.js.map +1 -0
  134. package/dist-server/service/testing/mode-conformance.d.ts +40 -0
  135. package/dist-server/service/testing/mode-conformance.js +90 -0
  136. package/dist-server/service/testing/mode-conformance.js.map +1 -0
  137. package/dist-server/tool-registry-boot-report.d.ts +3 -0
  138. package/dist-server/tool-registry-boot-report.js +25 -2
  139. package/dist-server/tool-registry-boot-report.js.map +1 -1
  140. package/dist-server/tsconfig.tsbuildinfo +1 -1
  141. package/package.json +5 -5
  142. package/server/explicit-graphql-types.test.ts +56 -0
  143. package/server/service/assistant-capability-resolver.ts +66 -0
  144. package/server/service/assistant-chat-resolver.ts +184 -42
  145. package/server/service/assistant-mode/assistant-mode-resolver.ts +102 -0
  146. package/server/service/assistant-mode/index.ts +16 -0
  147. package/server/service/assistant-mode/registry.test.ts +105 -0
  148. package/server/service/assistant-mode/registry.ts +147 -0
  149. package/server/service/assistant-mode/resolve.test.ts +78 -0
  150. package/server/service/assistant-mode/resolve.ts +96 -0
  151. package/server/service/assistant-mode/subject-defaults.test.ts +86 -0
  152. package/server/service/assistant-mode/types.ts +103 -0
  153. package/server/service/chat-message/carried-turns.test.ts +69 -0
  154. package/server/service/chat-message/carried-turns.ts +99 -0
  155. package/server/service/chat-message/chat-message-subscription.ts +20 -4
  156. package/server/service/chat-message/chat-message.ts +44 -0
  157. package/server/service/chat-message/door-access.ts +45 -0
  158. package/server/service/chat-message/fold-session-history.test.ts +98 -0
  159. package/server/service/chat-message/fold-session-history.ts +115 -0
  160. package/server/service/chat-message/llm-history.test.ts +58 -7
  161. package/server/service/chat-message/llm-history.ts +30 -11
  162. package/server/service/chat-message/message-doors.test.ts +87 -0
  163. package/server/service/chat-message/message-doors.ts +117 -0
  164. package/server/service/chat-message/proposal-outcome.test.ts +48 -0
  165. package/server/service/chat-message/proposal-outcome.ts +42 -0
  166. package/server/service/chat-message/reader-gate.ts +42 -0
  167. package/server/service/chat-message/room-message.test.ts +21 -0
  168. package/server/service/chat-message/room-message.ts +22 -0
  169. package/server/service/chat-session/chat-session.ts +31 -1
  170. package/server/service/chat-session/session-inbox.test.ts +18 -0
  171. package/server/service/chat-session/session-inbox.ts +9 -2
  172. package/server/service/chat-session/session-visibility.test.ts +36 -0
  173. package/server/service/chat-session/session-visibility.ts +26 -0
  174. package/server/service/chat-session/station-session-guard.ts +10 -2
  175. package/server/service/chat-session-participant/mention-invite.test.ts +125 -0
  176. package/server/service/chat-session-participant/mention-invite.ts +121 -0
  177. package/server/service/chat-session-resolver.ts +272 -15
  178. package/server/service/grounding-sources.test.ts +55 -0
  179. package/server/service/grounding-sources.ts +46 -0
  180. package/server/service/index.ts +19 -0
  181. package/server/service/request-translator.test.ts +45 -0
  182. package/server/service/request-translator.ts +33 -0
  183. package/server/service/station-session-guard.test.ts +12 -5
  184. package/server/service/testing/conversation-scenario.test.ts +163 -0
  185. package/server/service/testing/conversation-script.test.ts +131 -0
  186. package/server/service/testing/conversation-script.ts +288 -0
  187. package/server/service/testing/mode-conformance.ts +107 -0
  188. package/server/tool-registry-boot-report.ts +25 -2
  189. package/test/assistant-chat-call-sites.test.ts +82 -0
  190. package/translations/en.json +44 -2
  191. package/translations/ja.json +44 -2
  192. package/translations/ko.json +44 -2
  193. package/translations/ms.json +44 -2
  194. package/translations/zh.json +44 -2
@@ -17,6 +17,32 @@ const typeorm_1 = require("typeorm");
17
17
  const chat_session_js_1 = require("./chat-session/chat-session.js");
18
18
  const session_anchor_js_1 = require("./chat-session/session-anchor.js");
19
19
  const session_inbox_js_1 = require("./chat-session/session-inbox.js");
20
+ const carried_turns_js_1 = require("./chat-message/carried-turns.js");
21
+ const room_message_js_1 = require("./chat-message/room-message.js");
22
+ const reader_gate_js_1 = require("./chat-message/reader-gate.js");
23
+ const session_visibility_js_1 = require("./chat-session/session-visibility.js");
24
+ const mention_invite_js_1 = require("./chat-session-participant/mention-invite.js");
25
+ const invite_notification_js_1 = require("./chat-session-participant/invite-notification.js");
26
+ /**
27
+ * 초대 알림 — 발송은 호스트의 기전을 그대로 쓴다(중복 구현 금지).
28
+ *
29
+ * 알림이 안 가도 초대 자체는 이미 일어났다. 그래서 실패를 삼키되, 초대를 되돌리지는 않는다 —
30
+ * 참여자 목록이 정본이고 알림은 그것을 알리는 편지다.
31
+ */
32
+ async function notifyInvited(context, invited, inviter) {
33
+ if (invited.length === 0)
34
+ return;
35
+ const payload = (0, invite_notification_js_1.buildInviteNotification)(invited, undefined, inviter);
36
+ if (!payload)
37
+ return;
38
+ try {
39
+ await context.state?.notify?.(payload);
40
+ }
41
+ catch {
42
+ /* 편지가 못 갔다고 방에 들어온 사실이 취소되지는 않는다. */
43
+ }
44
+ }
45
+ const proposal_outcome_js_1 = require("./chat-message/proposal-outcome.js");
20
46
  const session_activity_publish_js_1 = require("./chat-session/session-activity-publish.js");
21
47
  const chat_message_js_1 = require("./chat-message/chat-message.js");
22
48
  const chat_session_participant_js_1 = require("./chat-session-participant/chat-session-participant.js");
@@ -51,6 +77,10 @@ tslib_1.__decorate([
51
77
  (0, type_graphql_1.Field)({ nullable: true, description: 'Short preview of the last message.' }),
52
78
  tslib_1.__metadata("design:type", String)
53
79
  ], ChatSessionInboxEntry.prototype, "lastMessagePreview", void 0);
80
+ tslib_1.__decorate([
81
+ (0, type_graphql_1.Field)({ nullable: true, description: 'True when the preview was replaced because this reader does not pass the doors of the message it came from.' }),
82
+ tslib_1.__metadata("design:type", Boolean)
83
+ ], ChatSessionInboxEntry.prototype, "previewRedacted", void 0);
54
84
  tslib_1.__decorate([
55
85
  (0, type_graphql_1.Field)({ nullable: true, description: 'Session creation timestamp.' }),
56
86
  tslib_1.__metadata("design:type", Date)
@@ -152,7 +182,19 @@ let ChatSessionResolver = class ChatSessionResolver {
152
182
  mine: createdByMe.has(entry.id)
153
183
  }));
154
184
  /* showHidden=true 로 불러 **전체**를 얻는다 — 숨김 여부 판정을 두 곳에 복제하지 않기 위함. */
155
- return (0, session_inbox_js_1.applyHidden)(entries, hiddenAt, true);
185
+ const shown = (0, session_inbox_js_1.applyHidden)(entries, hiddenAt, true);
186
+ /*
187
+ * 목록도 같은 관문을 지난다 — ADR-0073 판정 1. 미리보기는 그 답을 **다시 읽는 두 번째 길**이고,
188
+ * 한 길만 막으면 같은 문장이 다른 길로 그대로 나간다. 미리보기 자리를 `content` 로 바꿔 넣고
189
+ * 가려진 것만 되돌려 담는다.
190
+ */
191
+ const gate = (0, reader_gate_js_1.readerGate)(context);
192
+ const judged = await gate(shown.map(entry => ({ doors: entry.lastMessageDoors, content: entry.lastMessagePreview ?? '' })));
193
+ return shown.map((entry, index) => ({
194
+ ...entry,
195
+ lastMessagePreview: judged[index].content,
196
+ previewRedacted: judged[index].redacted === true
197
+ }));
156
198
  }
157
199
  /**
158
200
  * 세션 삭제 — **만든 사람만.**
@@ -231,6 +273,41 @@ let ChatSessionResolver = class ChatSessionResolver {
231
273
  * 변경은 **meta 활동**으로 방송한다 — 모든 참여자 목록에 즉시 반영되지만 안 읽음 배지는 올리지 않는다
232
274
  * (이름 변경을 "새 소식" 으로 알리면 소음이다).
233
275
  */
276
+ /**
277
+ * 이 대화를 도메인 안 누구나 읽게 할 것인가 — ADR-0073 판정 2.
278
+ *
279
+ * owner 만 바꾼다. 이름 바꾸기는 어느 참여자나 하지만(되돌릴 수 있고 값을 드러내지 않는다), 공개로
280
+ * 바꾸는 일은 이미 쓰인 글을 도메인 전체에 여는 일이다 — 되돌려도 읽힌 것은 읽힌 것이다.
281
+ *
282
+ * 화면은 공개로 바꾸기 전에 한 줄을 먼저 보인다(문구는 클라이언트가 가진다). 그 한 줄은 이 문이
283
+ * 강제하지 않는다 — 문이 막을 수 있는 것은 누가 바꾸느냐까지다.
284
+ */
285
+ async setSessionVisibility(sessionId, visibility, context) {
286
+ const { domain, user } = context.state;
287
+ if (!session_visibility_js_1.SESSION_VISIBILITIES.includes(visibility)) {
288
+ throw new Error(`unknown visibility '${visibility}' — use ${session_visibility_js_1.SESSION_VISIBILITIES.join(' or ')}`);
289
+ }
290
+ const session = await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).findOneBy({ id: sessionId, domain: { id: domain.id } });
291
+ if (!session)
292
+ return false;
293
+ const mine = await (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant).findOne({
294
+ where: { domain: { id: domain.id }, session: { id: sessionId }, user: { id: user.id } }
295
+ });
296
+ if (mine?.role !== 'owner')
297
+ return false;
298
+ if (session.visibility === visibility)
299
+ return true;
300
+ await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).update(sessionId, { visibility, updater: user });
301
+ (0, session_activity_publish_js_1.publishSessionActivity)({
302
+ domainId: domain.id,
303
+ sessionId,
304
+ kind: 'meta',
305
+ anchorType: session.anchorType,
306
+ anchorId: session.anchorId,
307
+ actorId: user?.id
308
+ });
309
+ return true;
310
+ }
234
311
  async renameAISession(sessionId, name, context) {
235
312
  const { domain, user } = context.state;
236
313
  const trimmed = (name ?? '').trim().slice(0, 200);
@@ -306,14 +383,25 @@ let ChatSessionResolver = class ChatSessionResolver {
306
383
  return true;
307
384
  }
308
385
  async chatMessages(sessionId, limit, offset, context) {
309
- const { domain } = context.state;
386
+ const { domain, user } = context.state;
310
387
  const session = await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).findOneBy({
311
388
  id: sessionId,
312
389
  domain: { id: domain.id }
313
390
  });
314
391
  if (!session)
315
392
  return [];
316
- return await (0, shell_1.getRepository)(chat_message_js_1.ChatMessage).find({
393
+ /*
394
+ * 공개 대화는 도메인 안이면 누구나 읽는다 — 초대 절차가 없다(ADR-0073 판정 2). 비공개 대화는
395
+ * 참여자만. 여기서 막히는 것은 **방**이고, 방에 들어온 뒤 값을 읽는 일은 아래 문 판정이 따로 본다.
396
+ */
397
+ if (session.visibility === 'private') {
398
+ const part = await (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant).findOne({
399
+ where: { session: { id: sessionId }, user: { id: user.id }, domain: { id: domain.id } }
400
+ });
401
+ if (!(0, session_visibility_js_1.mayReadSession)(session.visibility, !!part))
402
+ return [];
403
+ }
404
+ const rows = await (0, shell_1.getRepository)(chat_message_js_1.ChatMessage).find({
317
405
  where: { session: { id: sessionId } },
318
406
  // creator: 그룹챗 발신자 표시(name) + 자기 여부 판정(email). 내부 관리키(id)에 의존하지 않는다.
319
407
  relations: { creator: true },
@@ -321,6 +409,11 @@ let ChatSessionResolver = class ChatSessionResolver {
321
409
  take: limit,
322
410
  skip: offset
323
411
  });
412
+ /*
413
+ * 어시스턴트의 답은 도구 결과를 평문으로 옮겨 적은 것이다. 읽는 사람이 그 도구가 지난 문을 못 지나면
414
+ * 본문 대신 한 줄이 나간다 — 행이 있다는 사실은 보이고 값만 안 보인다(ADR-0073 판정 1).
415
+ */
416
+ return await (0, reader_gate_js_1.readerGate)(context)(rows);
324
417
  }
325
418
  async startAISession(anchorType, anchorId, context) {
326
419
  const { domain, user, tx } = context.state;
@@ -331,6 +424,8 @@ let ChatSessionResolver = class ChatSessionResolver {
331
424
  const session = await repo.save({
332
425
  domain,
333
426
  ...(0, session_anchor_js_1.anchorColumns)(anchorType, anchorId),
427
+ /* 앵커가 있는 대화는 공개로 태어난다 — ADR-0073 판정 2. */
428
+ visibility: (0, session_visibility_js_1.initialVisibility)({ anchorType }),
334
429
  creator: user,
335
430
  updater: user
336
431
  });
@@ -352,11 +447,13 @@ let ChatSessionResolver = class ChatSessionResolver {
352
447
  let finalName = name?.trim();
353
448
  if (!finalName) {
354
449
  const count = await repo.count({ where: (0, session_anchor_js_1.anchorWhere)(domain.id, anchorType, anchorId) });
355
- finalName = `세션 ${count + 1}`;
450
+ /* The name is stored, so it is written in the language of whoever created it (context.t), and read back as is. */
451
+ finalName = context.t('ai-assistant.session.default-name', { n: count + 1, defaultValue: 'conversation {n}' });
356
452
  }
357
453
  const session = await repo.save({
358
454
  domain,
359
455
  ...(0, session_anchor_js_1.anchorColumns)(anchorType, anchorId),
456
+ visibility: (0, session_visibility_js_1.initialVisibility)({ anchorType }),
360
457
  creator: user,
361
458
  updater: user,
362
459
  name: finalName
@@ -381,6 +478,93 @@ let ChatSessionResolver = class ChatSessionResolver {
381
478
  await repo.update(sessionId, { name: name.trim() || null, updater: user });
382
479
  return true;
383
480
  }
481
+ async reportProposalOutcome(sessionId, proposalKey, outcome, context) {
482
+ const { domain, user, tx } = context.state;
483
+ const session = await (0, shell_1.getRepository)(chat_session_js_1.ChatSession, tx).findOneBy({ id: sessionId, domain: { id: domain.id } });
484
+ if (!session)
485
+ throw new Error(`ChatSession ${sessionId} not found`);
486
+ const key = (proposalKey ?? '').trim();
487
+ if (!key)
488
+ return false;
489
+ /*
490
+ * 낱말은 ADR-0042 의 것에 `declined` 하나다. 모르는 낱말을 받으면 그대로 적지 않는다 — 카드가
491
+ * 읽지 못하는 상태가 되고, 그때 화면은 아무 말도 못 한다.
492
+ */
493
+ if (!proposal_outcome_js_1.PROPOSAL_VERDICTS.includes(outcome?.verdict)) {
494
+ throw new Error(`[ai-assistant] '${outcome?.verdict}' is not a proposal verdict. Use one of ${proposal_outcome_js_1.PROPOSAL_VERDICTS.join(', ')}.`);
495
+ }
496
+ const body = (0, proposal_outcome_js_1.formatProposalOutcomeLine)(key, outcome);
497
+ const previousLast = await (0, shell_1.getRepository)(chat_message_js_1.ChatMessage, tx).findOne({
498
+ where: { session: { id: session.id } },
499
+ order: { createdAt: 'DESC' }
500
+ });
501
+ const sysMsg = await (0, shell_1.getRepository)(chat_message_js_1.ChatMessage, tx).save({
502
+ session: { id: session.id },
503
+ role: 'system',
504
+ content: body,
505
+ creator: user,
506
+ updater: user,
507
+ parentMessage: previousLast ? { id: previousLast.id } : undefined
508
+ });
509
+ /* 목록의 미리보기는 사람의 말로 두는 편이 낫다 — 이 줄은 기계의 기록이다. 방송만 한다. */
510
+ (0, chat_message_publish_js_1.publishChatMessage)({ domainId: domain.id, sessionId: session.id, message: sysMsg });
511
+ return true;
512
+ }
513
+ async sayToRoom(sessionId, content, context) {
514
+ const { domain, user, tx } = context.state;
515
+ const session = await (0, shell_1.getRepository)(chat_session_js_1.ChatSession, tx).findOneBy({ id: sessionId, domain: { id: domain.id } });
516
+ if (!session)
517
+ throw new Error(`ChatSession ${sessionId} not found`);
518
+ const body = (0, room_message_js_1.roomMessageBody)(content);
519
+ if (!body)
520
+ return null;
521
+ const previousLast = await (0, shell_1.getRepository)(chat_message_js_1.ChatMessage, tx).findOne({
522
+ where: { session: { id: session.id } },
523
+ order: { createdAt: 'DESC' }
524
+ });
525
+ const saved = await (0, shell_1.getRepository)(chat_message_js_1.ChatMessage, tx).save({
526
+ session: { id: session.id },
527
+ role: 'user',
528
+ content: body,
529
+ /* 대상은 문이 정한다 — 이 문은 사람들에게 하는 말이다(ADR-0070 A15 결정 ①). */
530
+ addressee: 'room',
531
+ creator: user,
532
+ updater: user,
533
+ parentMessage: previousLast ? { id: previousLast.id } : undefined
534
+ });
535
+ /*
536
+ * 멘션은 사람을 불러들인다 — **사람의 말을 저장하는 문**만 이것을 한다(ADR-0071 결정 1 ①).
537
+ * 초대는 방에 있는 사람(owner · member)만 할 수 있고, 거절되면 그 사실이 화면으로 돌아간다.
538
+ */
539
+ const invite = await (0, mention_invite_js_1.inviteMentionedUsers)({
540
+ content: body,
541
+ sessionId: session.id,
542
+ domainId: domain.id,
543
+ inviter: user,
544
+ participants: (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant, tx),
545
+ /* 비공개 대화에서는 이름만 닿는다 — ADR-0073 판정 2. */
546
+ visibility: session.visibility
547
+ });
548
+ await notifyInvited(context, invite.invited, user);
549
+ /* 사람의 말이다 — 목록의 최근 활동과 다른 참여자의 안 읽음이 이것으로 움직인다. */
550
+ try {
551
+ await (0, shell_1.getRepository)(chat_session_js_1.ChatSession, tx).update(session.id, (0, session_inbox_js_1.activityPatch)(body));
552
+ }
553
+ catch {
554
+ /* 목록이 한 박자 늦는 것이 말을 못 하게 할 이유는 아니다. */
555
+ }
556
+ (0, session_activity_publish_js_1.publishSessionActivity)({
557
+ domainId: domain.id,
558
+ sessionId: session.id,
559
+ kind: 'message',
560
+ anchorType: session.anchorType,
561
+ anchorId: session.anchorId,
562
+ preview: body,
563
+ actorId: user?.id
564
+ });
565
+ (0, chat_message_publish_js_1.publishChatMessage)({ domainId: domain.id, sessionId: session.id, message: saved });
566
+ return saved.id ?? null;
567
+ }
384
568
  async appendSessionNote(sessionId, content, context) {
385
569
  const { domain, user, tx } = context.state;
386
570
  const session = await (0, shell_1.getRepository)(chat_session_js_1.ChatSession, tx).findOneBy({ id: sessionId, domain: { id: domain.id } });
@@ -429,13 +613,35 @@ let ChatSessionResolver = class ChatSessionResolver {
429
613
  /* 옮겨온 대화는 **그 대화의 발언 그대로** 들어간다 — 한 덩어리 기록으로 뭉치면 사람이 읽을 수 없고
430
614
  * (화면 하나를 잡아먹는 이탤릭 덩어리), 마크다운도 죽는다. 대신 개수·길이는 제한한다: 맥락 전달이
431
615
  * 목적이고 전문 보존이 아니다. */
432
- const rows = (Array.isArray(turns) ? turns : [])
433
- .map(t => ({
434
- role: t?.role === 'ai' || t?.role === 'assistant' ? 'assistant' : t?.role === 'user' ? 'user' : 'system',
435
- content: (t?.content ?? '').trim().slice(0, 4000)
436
- }))
437
- .filter(t => t.content)
438
- .slice(-40);
616
+ /* **역할은 서버가 쓴다**(ADR-0070 A15 결정 ③). 어시스턴트의 답은 원본을 읽어 복사하고, 원본이 없으면
617
+ * 인용문으로 `system` 행에 남긴다 — 클라이언트가 보낸 글자를 어시스턴트의 말로 저장하지 않는다. */
618
+ const planned = (0, carried_turns_js_1.planCarriedTurns)(turns);
619
+ const rows = [];
620
+ /* 옮겨 적힌 원문이 소급해서 공개되지 않게, 받는 대화는 이 순간부터 비공개다 — ADR-0073 판정 2. */
621
+ let carried = false;
622
+ for (const row of planned) {
623
+ if (!row.copyOf) {
624
+ rows.push({ role: row.role, content: row.content, addressee: row.addressee });
625
+ continue;
626
+ }
627
+ /* 원본은 이 테넌트 안에서만 찾는다 — 남의 도메인 메시지를 옮겨 오는 길이 되지 않게. */
628
+ const original = await (0, shell_1.getRepository)(chat_message_js_1.ChatMessage, tx).findOne({
629
+ where: (0, carried_turns_js_1.carriedOriginalWhere)(domain.id, row.copyOf),
630
+ relations: ['session']
631
+ });
632
+ carried = true;
633
+ if (original && original.role === 'assistant') {
634
+ /*
635
+ * 답과 함께 **그 답이 지나온 문**을 옮긴다 — ADR-0073 판정 1. 문을 두고 글만 옮기면, 문 하나를
636
+ * 지나 얻은 값이 문 없는 행이 되어 아무나 읽는다. 옮겨 적기가 권한을 씻는 길이 된다.
637
+ */
638
+ rows.push({ role: 'assistant', content: original.content, doors: original.doors ?? [] });
639
+ }
640
+ else {
641
+ /* 인용문에도 원문의 문을 실어 둔다 — 인용이라고 값이 달라지지 않는다. */
642
+ rows.push({ role: 'system', content: (0, carried_turns_js_1.quoteCarriedAnswer)(row.content), doors: original?.doors ?? [] });
643
+ }
644
+ }
439
645
  if (!rows.length)
440
646
  return false;
441
647
  let previous = await (0, shell_1.getRepository)(chat_message_js_1.ChatMessage, tx).findOne({
@@ -448,12 +654,18 @@ let ChatSessionResolver = class ChatSessionResolver {
448
654
  session: { id: session.id },
449
655
  role: row.role,
450
656
  content: row.content,
657
+ /* 사람의 말은 방을 향한 것이다 — 이 문은 어시스턴트를 부르는 문이 아니다. */
658
+ addressee: row.addressee,
659
+ doors: row.doors,
451
660
  creator: user,
452
661
  updater: user,
453
662
  parentMessage: previous ? { id: previous.id } : undefined
454
663
  });
455
664
  saved.push(previous);
456
665
  }
666
+ if (carried && session.visibility !== 'private') {
667
+ await (0, shell_1.getRepository)(chat_session_js_1.ChatSession, tx).update(session.id, { visibility: 'private', updater: user });
668
+ }
457
669
  /* 활동은 마지막 발언 기준 한 번만 — 옮긴 줄마다 목록을 흔들 이유가 없다. */
458
670
  const last = rows[rows.length - 1];
459
671
  try {
@@ -566,6 +778,18 @@ tslib_1.__decorate([
566
778
  tslib_1.__metadata("design:paramtypes", [String, Object, Boolean]),
567
779
  tslib_1.__metadata("design:returntype", Promise)
568
780
  ], ChatSessionResolver.prototype, "hideAISession", null);
781
+ tslib_1.__decorate([
782
+ (0, type_graphql_1.Mutation)(() => Boolean, {
783
+ description: "Open this conversation to everyone in the domain, or close it to its participants. Only the owner may change it. Opening exposes what is already written — the screen says so before asking. Door checks on assistant answers are unaffected (ADR-0073)."
784
+ }),
785
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation")'),
786
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
787
+ tslib_1.__param(1, (0, type_graphql_1.Arg)('visibility')),
788
+ tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
789
+ tslib_1.__metadata("design:type", Function),
790
+ tslib_1.__metadata("design:paramtypes", [String, String, Object]),
791
+ tslib_1.__metadata("design:returntype", Promise)
792
+ ], ChatSessionResolver.prototype, "setSessionVisibility", null);
569
793
  tslib_1.__decorate([
570
794
  (0, type_graphql_1.Mutation)(() => Boolean, {
571
795
  description: 'Rename a chat session. Any participant may rename it — the name is the topic label of a shared deliberation and the change is reversible. Broadcast as meta activity so every participant sees it without raising unread badges.'
@@ -591,7 +815,7 @@ tslib_1.__decorate([
591
815
  ], ChatSessionResolver.prototype, "markSessionRead", null);
592
816
  tslib_1.__decorate([
593
817
  (0, type_graphql_1.Query)(() => [auth_base_1.User], {
594
- description: 'List users in the current domain for `@` mention popup. board-ai 권한이면 누구나 멘션용 검색 가능 (관리자 전용 users() 와 별도).'
818
+ description: 'List users in the current domain for the `@` mention popup. Anyone with the chat privilege may search here; this is separate from the admin-only users().'
595
819
  }),
596
820
  (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "query")'),
597
821
  tslib_1.__param(0, (0, type_graphql_1.Arg)('query', { nullable: true, description: 'Substring to match against name/email (case-insensitive). Empty → all.' })),
@@ -650,12 +874,12 @@ tslib_1.__decorate([
650
874
  tslib_1.__decorate([
651
875
  (0, type_graphql_1.Directive)('@transaction'),
652
876
  (0, type_graphql_1.Mutation)(() => chat_session_js_1.ChatSession, {
653
- description: 'Always create a new AI chat session anchored to a target (no idempotent reuse). For multi-session UX — 열기.'
877
+ description: 'Always create a new AI chat session anchored to a target (no idempotent reuse). For multi-session UX — opening another tab.'
654
878
  }),
655
879
  (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation")'),
656
880
  tslib_1.__param(0, (0, type_graphql_1.Arg)('anchorType')),
657
881
  tslib_1.__param(1, (0, type_graphql_1.Arg)('anchorId')),
658
- tslib_1.__param(2, (0, type_graphql_1.Arg)('name', { nullable: true, description: 'Optional name (defaults to auto-generated `세션 N`).' })),
882
+ tslib_1.__param(2, (0, type_graphql_1.Arg)('name', { nullable: true, description: 'Optional name. Defaults to a generated one, numbered within the anchor.' })),
659
883
  tslib_1.__param(3, (0, type_graphql_1.Ctx)()),
660
884
  tslib_1.__metadata("design:type", Function),
661
885
  tslib_1.__metadata("design:paramtypes", [String, String, String, Object]),
@@ -674,6 +898,34 @@ tslib_1.__decorate([
674
898
  tslib_1.__metadata("design:paramtypes", [String, String, Object]),
675
899
  tslib_1.__metadata("design:returntype", Promise)
676
900
  ], ChatSessionResolver.prototype, "renameChatSession", null);
901
+ tslib_1.__decorate([
902
+ (0, type_graphql_1.Directive)('@transaction'),
903
+ (0, type_graphql_1.Mutation)(() => Boolean, {
904
+ description: "What the host did with a proposal the assistant made. The host calls this once per card it was asked to run; the server records it as a system row, so the panel shows it, a refreshed panel still shows it, and the model reads on its next turn that its proposal was refused instead of assuming it landed (ADR-0070)."
905
+ }),
906
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation")'),
907
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
908
+ tslib_1.__param(1, (0, type_graphql_1.Arg)('proposalKey')),
909
+ tslib_1.__param(2, (0, type_graphql_1.Arg)('outcome', () => graphql_scalars_1.GraphQLJSON)),
910
+ tslib_1.__param(3, (0, type_graphql_1.Ctx)()),
911
+ tslib_1.__metadata("design:type", Function),
912
+ tslib_1.__metadata("design:paramtypes", [String, String, Object, Object]),
913
+ tslib_1.__metadata("design:returntype", Promise)
914
+ ], ChatSessionResolver.prototype, "reportProposalOutcome", null);
915
+ tslib_1.__decorate([
916
+ (0, type_graphql_1.Directive)('@transaction'),
917
+ (0, type_graphql_1.Mutation)(() => String, {
918
+ nullable: true,
919
+ description: "Say something to the people in this session. The turn is stored as the speaker's, addressed to the room, and the assistant is not called — it reads the line as context the next time someone asks it something. Returns the stored message id, or null when there was nothing to say (ADR-0070)."
920
+ }),
921
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation")'),
922
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
923
+ tslib_1.__param(1, (0, type_graphql_1.Arg)('content')),
924
+ tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
925
+ tslib_1.__metadata("design:type", Function),
926
+ tslib_1.__metadata("design:paramtypes", [String, String, Object]),
927
+ tslib_1.__metadata("design:returntype", Promise)
928
+ ], ChatSessionResolver.prototype, "sayToRoom", null);
677
929
  tslib_1.__decorate([
678
930
  (0, type_graphql_1.Directive)('@transaction'),
679
931
  (0, type_graphql_1.Mutation)(() => Boolean, {
@@ -690,7 +942,7 @@ tslib_1.__decorate([
690
942
  tslib_1.__decorate([
691
943
  (0, type_graphql_1.Directive)('@transaction'),
692
944
  (0, type_graphql_1.Mutation)(() => Boolean, {
693
- description: 'Carry a conversation from another surface into this session as its own turns (user / assistant), preserving order. Use when a lightweight exchange is promoted into a persistent discussion the participants should read it as the conversation continuing, not as a machine dump.'
945
+ description: "Carry a conversation from another surface into this session as its own turns, preserving order. A person's turns are stored as theirs, addressed to the room. An assistant turn is stored as the assistant's only when it names its original (sourceSessionId, sourceMessageId) and the server can read it; otherwise it is kept as a quotation on a system row — roles are the server's to write (ADR-0070)."
694
946
  }),
695
947
  (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation")'),
696
948
  tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),