@things-factory/ai-assistant 10.1.207 → 10.1.210

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 (181) hide show
  1. package/client/components/assistant-chat.ts +238 -9
  2. package/client/components/assistant-dock.ts +9 -4
  3. package/client/components/assistant-invite.ts +138 -0
  4. package/client/components/assistant-session-manager.ts +53 -12
  5. package/client/components/proposal-request.test.ts +10 -0
  6. package/client/components/roster-identity.test.ts +2 -12
  7. package/client/components/roster-identity.ts +0 -6
  8. package/client/setup-assistant-dock.ts +1 -0
  9. package/client/utils/assistant-session-actions.ts +3 -1
  10. package/client/utils/assistant-session-admin.ts +9 -2
  11. package/client/utils/assistant-session-controller.ts +1 -1
  12. package/client/utils/assistant-typing.test.ts +33 -0
  13. package/client/utils/assistant-typing.ts +53 -0
  14. package/dist-client/components/assistant-chat.d.ts +28 -0
  15. package/dist-client/components/assistant-chat.js +259 -8
  16. package/dist-client/components/assistant-chat.js.map +1 -1
  17. package/dist-client/components/assistant-dock.js +12 -4
  18. package/dist-client/components/assistant-dock.js.map +1 -1
  19. package/dist-client/components/assistant-invite.d.ts +39 -0
  20. package/dist-client/components/assistant-invite.js +150 -0
  21. package/dist-client/components/assistant-invite.js.map +1 -0
  22. package/dist-client/components/assistant-session-manager.d.ts +5 -1
  23. package/dist-client/components/assistant-session-manager.js +40 -10
  24. package/dist-client/components/assistant-session-manager.js.map +1 -1
  25. package/dist-client/components/proposal-request.test.js +8 -0
  26. package/dist-client/components/proposal-request.test.js.map +1 -1
  27. package/dist-client/components/roster-identity.d.ts +0 -2
  28. package/dist-client/components/roster-identity.js +0 -7
  29. package/dist-client/components/roster-identity.js.map +1 -1
  30. package/dist-client/components/roster-identity.test.js +2 -13
  31. package/dist-client/components/roster-identity.test.js.map +1 -1
  32. package/dist-client/setup-assistant-dock.js +1 -0
  33. package/dist-client/setup-assistant-dock.js.map +1 -1
  34. package/dist-client/tsconfig.tsbuildinfo +1 -1
  35. package/dist-client/utils/assistant-session-actions.d.ts +1 -0
  36. package/dist-client/utils/assistant-session-actions.js +3 -1
  37. package/dist-client/utils/assistant-session-actions.js.map +1 -1
  38. package/dist-client/utils/assistant-session-admin.d.ts +5 -2
  39. package/dist-client/utils/assistant-session-admin.js +3 -0
  40. package/dist-client/utils/assistant-session-admin.js.map +1 -1
  41. package/dist-client/utils/assistant-session-controller.d.ts +1 -1
  42. package/dist-client/utils/assistant-session-controller.js.map +1 -1
  43. package/dist-client/utils/assistant-typing.d.ts +20 -0
  44. package/dist-client/utils/assistant-typing.js +48 -0
  45. package/dist-client/utils/assistant-typing.js.map +1 -0
  46. package/dist-client/utils/assistant-typing.test.d.ts +1 -0
  47. package/dist-client/utils/assistant-typing.test.js +30 -0
  48. package/dist-client/utils/assistant-typing.test.js.map +1 -0
  49. package/dist-server/service/assistant-chat-resolver.js +5 -12
  50. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  51. package/dist-server/service/assistant-mode/index.d.ts +1 -1
  52. package/dist-server/service/assistant-mode/index.js +4 -1
  53. package/dist-server/service/assistant-mode/index.js.map +1 -1
  54. package/dist-server/service/assistant-mode/mode-categories.js +9 -2
  55. package/dist-server/service/assistant-mode/mode-categories.js.map +1 -1
  56. package/dist-server/service/assistant-mode/screen-rules.d.ts +19 -0
  57. package/dist-server/service/assistant-mode/screen-rules.js +40 -6
  58. package/dist-server/service/assistant-mode/screen-rules.js.map +1 -1
  59. package/dist-server/service/chat-message/chat-message-room.js +3 -1
  60. package/dist-server/service/chat-message/chat-message-room.js.map +1 -1
  61. package/dist-server/service/chat-message/chat-typing-subscription.d.ts +10 -0
  62. package/dist-server/service/chat-message/chat-typing-subscription.js +68 -0
  63. package/dist-server/service/chat-message/chat-typing-subscription.js.map +1 -0
  64. package/dist-server/service/chat-message/chat-typing.d.ts +28 -0
  65. package/dist-server/service/chat-message/chat-typing.js +40 -0
  66. package/dist-server/service/chat-message/chat-typing.js.map +1 -0
  67. package/dist-server/service/chat-session/attachment-tool-result.d.ts +1 -0
  68. package/dist-server/service/chat-session/attachment-tool-result.js +6 -0
  69. package/dist-server/service/chat-session/attachment-tool-result.js.map +1 -1
  70. package/dist-server/service/chat-session/attachment-tools.js +2 -2
  71. package/dist-server/service/chat-session/attachment-tools.js.map +1 -1
  72. package/dist-server/service/chat-session/chat-session.d.ts +2 -1
  73. package/dist-server/service/chat-session/chat-session.js +1 -1
  74. package/dist-server/service/chat-session/chat-session.js.map +1 -1
  75. package/dist-server/service/chat-session/session-activity-broadcast.d.ts +12 -0
  76. package/dist-server/service/chat-session/session-activity-broadcast.js +15 -0
  77. package/dist-server/service/chat-session/session-activity-broadcast.js.map +1 -1
  78. package/dist-server/service/chat-session/session-activity-publish.d.ts +2 -0
  79. package/dist-server/service/chat-session/session-activity-publish.js +5 -2
  80. package/dist-server/service/chat-session/session-activity-publish.js.map +1 -1
  81. package/dist-server/service/chat-session/session-visibility.d.ts +17 -2
  82. package/dist-server/service/chat-session/session-visibility.js +28 -3
  83. package/dist-server/service/chat-session/session-visibility.js.map +1 -1
  84. package/dist-server/service/chat-session/session-write-gate.d.ts +1 -1
  85. package/dist-server/service/chat-session/session-write-gate.js +1 -1
  86. package/dist-server/service/chat-session/session-write-gate.js.map +1 -1
  87. package/dist-server/service/chat-session-participant/index.d.ts +1 -0
  88. package/dist-server/service/chat-session-participant/index.js +4 -1
  89. package/dist-server/service/chat-session-participant/index.js.map +1 -1
  90. package/dist-server/service/chat-session-participant/invite-notification.d.ts +16 -1
  91. package/dist-server/service/chat-session-participant/invite-notification.js +34 -5
  92. package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -1
  93. package/dist-server/service/chat-session-participant/mention-invite.d.ts +30 -4
  94. package/dist-server/service/chat-session-participant/mention-invite.js +78 -31
  95. package/dist-server/service/chat-session-participant/mention-invite.js.map +1 -1
  96. package/dist-server/service/chat-session-participant/read-roster.js +5 -2
  97. package/dist-server/service/chat-session-participant/read-roster.js.map +1 -1
  98. package/dist-server/service/chat-session-resolver.d.ts +27 -0
  99. package/dist-server/service/chat-session-resolver.js +226 -25
  100. package/dist-server/service/chat-session-resolver.js.map +1 -1
  101. package/dist-server/service/data-entry/entry-target.d.ts +19 -0
  102. package/dist-server/service/data-entry/entry-target.js +20 -5
  103. package/dist-server/service/data-entry/entry-target.js.map +1 -1
  104. package/dist-server/service/data-entry/entry-tools.js +2 -2
  105. package/dist-server/service/data-entry/entry-tools.js.map +1 -1
  106. package/dist-server/service/data-entry/entry-update.d.ts +2 -1
  107. package/dist-server/service/data-entry/entry-update.js +5 -3
  108. package/dist-server/service/data-entry/entry-update.js.map +1 -1
  109. package/dist-server/service/data-read/list-read.d.ts +10 -0
  110. package/dist-server/service/data-read/list-read.js +12 -0
  111. package/dist-server/service/data-read/list-read.js.map +1 -1
  112. package/dist-server/service/data-read/read-target.d.ts +7 -0
  113. package/dist-server/service/data-read/read-target.js +2 -2
  114. package/dist-server/service/data-read/read-target.js.map +1 -1
  115. package/dist-server/service/data-read/read-tools.js +1 -1
  116. package/dist-server/service/data-read/read-tools.js.map +1 -1
  117. package/dist-server/service/index.d.ts +4 -2
  118. package/dist-server/service/index.js +9 -1
  119. package/dist-server/service/index.js.map +1 -1
  120. package/dist-server/service/testing/conversation-script.d.ts +1 -1
  121. package/dist-server/service/testing/conversation-script.js.map +1 -1
  122. package/dist-server/service/testing/mode-conformance.js +4 -0
  123. package/dist-server/service/testing/mode-conformance.js.map +1 -1
  124. package/dist-server/tsconfig.tsbuildinfo +1 -1
  125. package/package.json +9 -9
  126. package/server/explicit-graphql-types.test.ts +1 -1
  127. package/server/service/assistant-chat-resolver.ts +7 -12
  128. package/server/service/assistant-mode/index.ts +1 -1
  129. package/server/service/assistant-mode/mode-categories.ts +11 -3
  130. package/server/service/assistant-mode/mode-front.test.ts +53 -1
  131. package/server/service/assistant-mode/resolve.test.ts +2 -1
  132. package/server/service/assistant-mode/screen-rules.test.ts +41 -3
  133. package/server/service/assistant-mode/screen-rules.ts +44 -5
  134. package/server/service/chat-message/chat-message-room.test.ts +7 -2
  135. package/server/service/chat-message/chat-message-room.ts +3 -1
  136. package/server/service/chat-message/chat-typing-subscription.ts +52 -0
  137. package/server/service/chat-message/chat-typing.test.ts +45 -0
  138. package/server/service/chat-message/chat-typing.ts +48 -0
  139. package/server/service/chat-message/fold-history-window.test.ts +1 -1
  140. package/server/service/chat-session/attachment-tool-result.test.ts +9 -1
  141. package/server/service/chat-session/attachment-tool-result.ts +7 -0
  142. package/server/service/chat-session/attachment-tools.ts +3 -3
  143. package/server/service/chat-session/chat-session.ts +3 -2
  144. package/server/service/chat-session/group-room.test.ts +233 -0
  145. package/server/service/chat-session/session-activity-broadcast.ts +20 -0
  146. package/server/service/chat-session/session-activity-publish.ts +6 -1
  147. package/server/service/chat-session/session-activity-receivers.test.ts +26 -0
  148. package/server/service/chat-session/session-visibility.test.ts +32 -1
  149. package/server/service/chat-session/session-visibility.ts +33 -4
  150. package/server/service/chat-session/session-write-gate.test.ts +7 -1
  151. package/server/service/chat-session/session-write-gate.ts +2 -2
  152. package/server/service/chat-session-participant/index.ts +2 -0
  153. package/server/service/chat-session-participant/invite-notification.test.ts +36 -1
  154. package/server/service/chat-session-participant/invite-notification.ts +43 -5
  155. package/server/service/chat-session-participant/mention-invite.test.ts +49 -1
  156. package/server/service/chat-session-participant/mention-invite.ts +82 -28
  157. package/server/service/chat-session-participant/read-roster.test.ts +13 -4
  158. package/server/service/chat-session-participant/read-roster.ts +6 -3
  159. package/server/service/chat-session-resolver.ts +204 -27
  160. package/server/service/data-entry/data-entry-update.test.ts +19 -0
  161. package/server/service/data-entry/data-entry.test.ts +65 -1
  162. package/server/service/data-entry/entry-target.ts +36 -5
  163. package/server/service/data-entry/entry-tools.ts +2 -2
  164. package/server/service/data-entry/entry-update.ts +6 -3
  165. package/server/service/data-read/list-missing.test.ts +49 -0
  166. package/server/service/data-read/list-read.ts +18 -0
  167. package/server/service/data-read/read-target.ts +10 -3
  168. package/server/service/data-read/read-tools.ts +1 -1
  169. package/server/service/data-read/record-keys.test.ts +36 -0
  170. package/server/service/index.ts +6 -1
  171. package/server/service/testing/conversation-script.ts +1 -1
  172. package/server/service/testing/mode-conformance.ts +2 -0
  173. package/test/assistant-dock.test.ts +5 -1
  174. package/test/assistant-session-manager.test.ts +4 -2
  175. package/test/open-doors.test.ts +10 -5
  176. package/translations/en.json +29 -4
  177. package/translations/es.json +29 -4
  178. package/translations/ja.json +29 -4
  179. package/translations/ko.json +30 -5
  180. package/translations/ms.json +29 -4
  181. package/translations/zh.json +29 -4
@@ -20,30 +20,28 @@ const session_inbox_js_1 = require("./chat-session/session-inbox.js");
20
20
  const carried_turns_js_1 = require("./chat-message/carried-turns.js");
21
21
  const room_message_js_1 = require("./chat-message/room-message.js");
22
22
  const reader_gate_js_1 = require("./chat-message/reader-gate.js");
23
+ const request_translator_js_1 = require("./request-translator.js");
23
24
  const session_visibility_js_1 = require("./chat-session/session-visibility.js");
24
25
  const mention_invite_js_1 = require("./chat-session-participant/mention-invite.js");
26
+ const chat_typing_js_1 = require("./chat-message/chat-typing.js");
25
27
  const invite_notification_js_1 = require("./chat-session-participant/invite-notification.js");
26
28
  const read_roster_js_1 = require("./chat-session-participant/read-roster.js");
27
29
  const participant_lookup_js_1 = require("./chat-session-participant/participant-lookup.js");
28
30
  const session_write_gate_js_1 = require("./chat-session/session-write-gate.js");
31
+ /** 초대 알림 — 발송은 호스트의 기전을 그대로 쓴다(`sendInviteNotification`, 문 셋이 같은 한 곳). */
32
+ async function notifyInvited(context, invited, inviter, sessionName) {
33
+ await (0, invite_notification_js_1.sendInviteNotification)(context.state?.notify, invited, inviter, sessionName, undefined, (0, request_translator_js_1.requestTranslator)(context));
34
+ }
29
35
  /**
30
- * 초대 알림 — 발송은 호스트의 기전을 그대로 쓴다(중복 구현 금지).
31
- *
32
- * 알림이 안 가도 초대 자체는 이미 일어났다. 그래서 실패를 삼키되, 초대를 되돌리지는 않는다 —
33
- * 참여자 목록이 정본이고 알림은 그것을 알리는 편지다.
36
+ * A private room is the owner's alone (ADR-0073 decision 3) - closing a room to private sends its members out.
37
+ * Returns who left, so their lists can be told once.
34
38
  */
35
- async function notifyInvited(context, invited, inviter, sessionName) {
36
- if (invited.length === 0)
37
- return;
38
- const payload = (0, invite_notification_js_1.buildInviteNotification)(invited, undefined, inviter, sessionName);
39
- if (!payload)
40
- return;
41
- try {
42
- await context.state?.notify?.(payload);
43
- }
44
- catch {
45
- /* 편지가 못 갔다고 방에 들어온 사실이 취소되지는 않는다. */
46
- }
39
+ async function sendOutMembers(domainId, sessionId, tx) {
40
+ const repo = (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant, tx);
41
+ const seats = await repo.find({ where: { domain: { id: domainId }, session: { id: sessionId }, role: 'member' } });
42
+ for (const seat of seats)
43
+ await repo.softDelete(seat.id);
44
+ return seats.map(seat => seat.userId).filter(Boolean);
47
45
  }
48
46
  /** Every door that writes into a conversation passes this first (`session-write-gate.ts`). */
49
47
  async function writeGate(session, context, tx) {
@@ -123,7 +121,7 @@ tslib_1.__decorate([
123
121
  tslib_1.__metadata("design:type", Boolean)
124
122
  ], ChatSessionInboxEntry.prototype, "hidden", void 0);
125
123
  tslib_1.__decorate([
126
- (0, type_graphql_1.Field)({ description: "Who may read the session: 'open' (everyone in the domain) or 'private' (participants only)." }),
124
+ (0, type_graphql_1.Field)({ description: "Who may read the session: 'open' (everyone in the domain), 'group' (the owner and the people brought in) or 'private' (the owner alone)." }),
127
125
  tslib_1.__metadata("design:type", String)
128
126
  ], ChatSessionInboxEntry.prototype, "visibility", void 0);
129
127
  tslib_1.__decorate([
@@ -181,8 +179,17 @@ function attachmentRefusalMessage(refusal, context) {
181
179
  }
182
180
  let ChatSessionResolver = class ChatSessionResolver {
183
181
  async chatSession(id, context) {
184
- const { domain } = context.state;
185
- return (await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).findOneBy({ id, domain: { id: domain.id } })) ?? null;
182
+ const { domain, user } = context.state;
183
+ const session = await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).findOneBy({ id, domain: { id: domain.id } });
184
+ if (!session)
185
+ return null;
186
+ /*
187
+ * The row carries the name, the last preview and the summary of older messages - the conversation in
188
+ * short. A room the caller cannot enter answers as a missing one (ADR-0073: every reading path passes the gate).
189
+ */
190
+ if (!(0, session_visibility_js_1.isOpenRoom)(session.visibility) && !(0, session_visibility_js_1.mayReadSession)(session.visibility, await (0, participant_lookup_js_1.isSessionParticipant)(domain.id, id, user?.id)))
191
+ return null;
192
+ return session;
186
193
  }
187
194
  async chatSessionsByAnchor(anchorType, anchorId, context) {
188
195
  const { domain } = context.state;
@@ -386,7 +393,54 @@ let ChatSessionResolver = class ChatSessionResolver {
386
393
  return false;
387
394
  if (session.visibility === visibility)
388
395
  return true;
396
+ /*
397
+ * Private is the owner's alone (ADR-0073 decision 3). The members leave with the change - a private room
398
+ * with people still seated in it would be a group under another name. Bringing them back is inviting
399
+ * them again, in a group.
400
+ */
401
+ const sentOut = visibility === 'private' ? await sendOutMembers(domain.id, sessionId) : [];
389
402
  await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).update(sessionId, { visibility, updater: user });
403
+ (0, session_activity_publish_js_1.publishSessionActivity)({
404
+ domainId: domain.id,
405
+ sessionId,
406
+ kind: 'meta',
407
+ anchorType: session.anchorType,
408
+ anchorId: session.anchorId,
409
+ actorId: user?.id,
410
+ alsoTo: sentOut
411
+ });
412
+ return true;
413
+ }
414
+ /**
415
+ * Bring people into this conversation - the same seat a mention gives, by the same rule (`maySeat`): anyone
416
+ * in an open room, the owner in a group, nobody in a private one. Each person seated is told
417
+ * (`notifyInvited`). People outside the domain are not seated.
418
+ */
419
+ async inviteToAISession(sessionId, userIds, context) {
420
+ const { domain, user } = context.state;
421
+ const session = await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).findOneBy({ id: sessionId, domain: { id: domain.id } });
422
+ if (!session)
423
+ throw new Error('chat session not found');
424
+ /* Only people of this domain - an id from elsewhere is not a colleague here. */
425
+ const wanted = [...new Set((userIds ?? []).filter(id => typeof id === 'string' && id))].slice(0, 50);
426
+ if (wanted.length === 0)
427
+ return [];
428
+ const members = await (0, get_domain_users_js_1.buildDomainUsersQueryBuilder)(domain.id, 'USER').andWhere('USER.id IN (:...ids)', { ids: wanted }).getMany();
429
+ const known = new Set(members.map(member => member.id));
430
+ const seat = await (0, mention_invite_js_1.seatParticipants)({
431
+ sessionId,
432
+ domainId: domain.id,
433
+ inviter: user,
434
+ userIds: wanted.filter(id => known.has(id)),
435
+ participants: (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant),
436
+ visibility: session.visibility
437
+ });
438
+ if (seat.notSeated?.length) {
439
+ throw new Error(session.visibility === 'private'
440
+ ? (0, request_translator_js_1.requestTranslator)(context)('ai-assistant.text.invite-private-refused', { defaultValue: 'a private conversation is yours alone. make it a group to bring people in.' })
441
+ : (0, request_translator_js_1.requestTranslator)(context)('ai-assistant.text.invite-owner-only', { defaultValue: 'only the person who started this group conversation can bring people in.' }));
442
+ }
443
+ await notifyInvited(context, seat.invited, user, session.name);
390
444
  (0, session_activity_publish_js_1.publishSessionActivity)({
391
445
  domainId: domain.id,
392
446
  sessionId,
@@ -395,6 +449,77 @@ let ChatSessionResolver = class ChatSessionResolver {
395
449
  anchorId: session.anchorId,
396
450
  actorId: user?.id
397
451
  });
452
+ return seat.invited;
453
+ }
454
+ /**
455
+ * Send a member out - the owner only, and never the owner themself. In a group the seat was the key, so the
456
+ * person no longer reads or writes here (every gate asks the seat). Their list is told once and drops it.
457
+ */
458
+ async removeFromAISession(sessionId, userId, context) {
459
+ const { domain, user } = context.state;
460
+ const repo = (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant);
461
+ const seats = await repo.find({ where: { domain: { id: domain.id }, session: { id: sessionId } } });
462
+ const mine = seats.find(seat => seat.userId === user?.id);
463
+ if (mine?.role !== 'owner') {
464
+ throw new Error((0, request_translator_js_1.requestTranslator)(context)('ai-assistant.text.remove-owner-only', { defaultValue: 'only the person who started this conversation can send someone out.' }));
465
+ }
466
+ const theirs = seats.find(seat => seat.userId === userId && seat.role === 'member');
467
+ if (!theirs)
468
+ return false;
469
+ return await this._leave(sessionId, theirs, context);
470
+ }
471
+ /**
472
+ * Leave this conversation - a member steps out of their own seat. The owner does not leave: the room would be
473
+ * nobody's. They delete it, or make it private.
474
+ */
475
+ async leaveAISession(sessionId, context) {
476
+ const { domain, user } = context.state;
477
+ if (!user?.id)
478
+ return false;
479
+ const mine = await (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant).findOne({
480
+ where: { domain: { id: domain.id }, session: { id: sessionId }, user: { id: user.id } }
481
+ });
482
+ if (mine?.role === 'owner') {
483
+ throw new Error((0, request_translator_js_1.requestTranslator)(context)('ai-assistant.text.owner-cannot-leave', { defaultValue: 'you started this conversation, so you cannot leave it. delete it, or make it private.' }));
484
+ }
485
+ if (mine?.role !== 'member')
486
+ return false;
487
+ return await this._leave(sessionId, mine, context);
488
+ }
489
+ async _leave(sessionId, seat, context) {
490
+ const { domain, user } = context.state;
491
+ const session = await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).findOneBy({ id: sessionId, domain: { id: domain.id } });
492
+ await (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant).softDelete(seat.id);
493
+ (0, session_activity_publish_js_1.publishSessionActivity)({
494
+ domainId: domain.id,
495
+ sessionId,
496
+ kind: 'meta',
497
+ anchorType: session?.anchorType,
498
+ anchorId: session?.anchorId,
499
+ actorId: user?.id,
500
+ alsoTo: [seat.userId].filter(Boolean)
501
+ });
502
+ return true;
503
+ }
504
+ /**
505
+ * "I am typing" - a notice to the others in the room, stored nowhere. Only a person who may write here is
506
+ * typing here (the write gate), and the listeners pass the room's read gate (`ChatTypingSubscription`).
507
+ */
508
+ async sayTyping(sessionId, context) {
509
+ const { domain, user } = context.state;
510
+ const session = await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).findOneBy({ id: sessionId, domain: { id: domain.id } });
511
+ if (!session)
512
+ return false;
513
+ await writeGate(session, context);
514
+ const notice = (0, chat_typing_js_1.typingNotice)({ domainId: domain.id, sessionId, user });
515
+ if (!notice)
516
+ return false;
517
+ try {
518
+ shell_1.pubsub.publish(chat_typing_js_1.CHAT_TYPING_TOPIC, notice);
519
+ }
520
+ catch {
521
+ /* a missed notice is a line that does not show - nothing else rests on it */
522
+ }
398
523
  return true;
399
524
  }
400
525
  async renameAISession(sessionId, name, context) {
@@ -460,6 +585,20 @@ let ChatSessionResolver = class ChatSessionResolver {
460
585
  find: options => (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant).find(options)
461
586
  });
462
587
  }
588
+ /**
589
+ * May the caller seat someone here - the server's answer to the screen, so the screen keeps no copy of the
590
+ * rule (`maySeat`). The invite button and the line under a mention read it; the doors decide again.
591
+ */
592
+ async mayInviteToAISession(sessionId, context) {
593
+ const { domain, user } = context.state;
594
+ const session = await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).findOneBy({ id: sessionId, domain: { id: domain.id } });
595
+ if (!session || !user?.id)
596
+ return false;
597
+ const seat = await (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant).findOne({
598
+ where: { domain: { id: domain.id }, session: { id: sessionId }, user: { id: user.id } }
599
+ });
600
+ return (0, session_visibility_js_1.maySeat)(session.visibility, seat?.role);
601
+ }
463
602
  async touchPresence(sessionId, context) {
464
603
  const { domain, user } = context.state;
465
604
  const repo = (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant);
@@ -483,7 +622,7 @@ let ChatSessionResolver = class ChatSessionResolver {
483
622
  * 공개 대화는 도메인 안이면 누구나 읽는다 — 초대 절차가 없다(ADR-0073 판정 2). 비공개 대화는
484
623
  * 참여자만. 여기서 막히는 것은 **방**이고, 방에 들어온 뒤 값을 읽는 일은 아래 문 판정이 따로 본다.
485
624
  */
486
- if (session.visibility === 'private') {
625
+ if (!(0, session_visibility_js_1.isOpenRoom)(session.visibility)) {
487
626
  /* A hide-only `viewer` row is not being in the room - it must not open a private conversation. */
488
627
  const part = await (0, participant_lookup_js_1.isSessionParticipant)(domain.id, sessionId, user?.id);
489
628
  if (!(0, session_visibility_js_1.mayReadSession)(session.visibility, part))
@@ -678,9 +817,9 @@ let ChatSessionResolver = class ChatSessionResolver {
678
817
  });
679
818
  /*
680
819
  * 멘션은 사람을 불러들인다 — **사람의 말을 저장하는 문**만 이것을 한다(ADR-0071 결정 1 ①).
681
- * 초대는 방에 있는 사람(owner · member)만 할 수 있고, 거절되면 그 사실이 화면으로 돌아간다.
820
+ * 누가 자리를 주는지는 `maySeat` 가 정한다. 공개 방에서는 말한 사람도 앉는다(`seatTurn`).
682
821
  */
683
- const invite = await (0, mention_invite_js_1.inviteMentionedUsers)({
822
+ const invite = await (0, mention_invite_js_1.seatTurn)({
684
823
  content: body,
685
824
  sessionId: session.id,
686
825
  domainId: domain.id,
@@ -689,7 +828,7 @@ let ChatSessionResolver = class ChatSessionResolver {
689
828
  /* 비공개 대화에서는 이름만 닿는다 — ADR-0073 판정 2. */
690
829
  visibility: session.visibility
691
830
  });
692
- await notifyInvited(context, invite.invited, user);
831
+ await notifyInvited(context, invite.invited, user, session.name);
693
832
  /* 사람의 말이다 — 목록의 최근 활동과 다른 참여자의 안 읽음이 이것으로 움직인다. */
694
833
  try {
695
834
  await (0, shell_1.getRepository)(chat_session_js_1.ChatSession, tx).update(session.id, (0, session_inbox_js_1.activityPatch)(body));
@@ -809,7 +948,12 @@ let ChatSessionResolver = class ChatSessionResolver {
809
948
  });
810
949
  saved.push(previous);
811
950
  }
812
- if (carried && session.visibility !== 'private') {
951
+ /*
952
+ * Carried words do not land in front of the whole domain. An open room closes to its owner; a group stays
953
+ * a group - its people were chosen by the owner, and carrying into it is writing to them.
954
+ */
955
+ if (carried && (0, session_visibility_js_1.isOpenRoom)(session.visibility)) {
956
+ await sendOutMembers(domain.id, session.id, tx);
813
957
  await (0, shell_1.getRepository)(chat_session_js_1.ChatSession, tx).update(session.id, { visibility: 'private', updater: user });
814
958
  }
815
959
  /* 활동은 마지막 발언 기준 한 번만 — 옮긴 줄마다 목록을 흔들 이유가 없다. */
@@ -934,7 +1078,7 @@ tslib_1.__decorate([
934
1078
  ], ChatSessionResolver.prototype, "hideAISession", null);
935
1079
  tslib_1.__decorate([
936
1080
  (0, type_graphql_1.Mutation)(() => Boolean, {
937
- 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)."
1081
+ description: "Set who reads this conversation: 'open' (everyone in the domain), 'group' (the owner and the people the owner brings in) or 'private' (the owner alone - the members are sent out). 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)."
938
1082
  }),
939
1083
  (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)'),
940
1084
  tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
@@ -944,6 +1088,52 @@ tslib_1.__decorate([
944
1088
  tslib_1.__metadata("design:paramtypes", [String, String, Object]),
945
1089
  tslib_1.__metadata("design:returntype", Promise)
946
1090
  ], ChatSessionResolver.prototype, "setSessionVisibility", null);
1091
+ tslib_1.__decorate([
1092
+ (0, type_graphql_1.Mutation)(() => [String], {
1093
+ description: "Bring people into this conversation and tell them. In a group only the owner may; a private conversation takes nobody (make it a group first). Returns the ids of the people seated now - people already in it are left out."
1094
+ }),
1095
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)'),
1096
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
1097
+ tslib_1.__param(1, (0, type_graphql_1.Arg)('userIds', () => [String])),
1098
+ tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
1099
+ tslib_1.__metadata("design:type", Function),
1100
+ tslib_1.__metadata("design:paramtypes", [String, Array, Object]),
1101
+ tslib_1.__metadata("design:returntype", Promise)
1102
+ ], ChatSessionResolver.prototype, "inviteToAISession", null);
1103
+ tslib_1.__decorate([
1104
+ (0, type_graphql_1.Mutation)(() => Boolean, {
1105
+ description: 'Send a member out of this conversation. Only the owner may, and the owner cannot be sent out. In a group the person no longer reads or writes here.'
1106
+ }),
1107
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)'),
1108
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
1109
+ tslib_1.__param(1, (0, type_graphql_1.Arg)('userId')),
1110
+ tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
1111
+ tslib_1.__metadata("design:type", Function),
1112
+ tslib_1.__metadata("design:paramtypes", [String, String, Object]),
1113
+ tslib_1.__metadata("design:returntype", Promise)
1114
+ ], ChatSessionResolver.prototype, "removeFromAISession", null);
1115
+ tslib_1.__decorate([
1116
+ (0, type_graphql_1.Mutation)(() => Boolean, {
1117
+ description: 'Leave this conversation. A member may; the owner cannot (delete it or make it private instead). In a group, leaving ends reading and writing here.'
1118
+ }),
1119
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)'),
1120
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
1121
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
1122
+ tslib_1.__metadata("design:type", Function),
1123
+ tslib_1.__metadata("design:paramtypes", [String, Object]),
1124
+ tslib_1.__metadata("design:returntype", Promise)
1125
+ ], ChatSessionResolver.prototype, "leaveAISession", null);
1126
+ tslib_1.__decorate([
1127
+ (0, type_graphql_1.Mutation)(() => Boolean, {
1128
+ description: 'Tell the others in this conversation that the caller is typing. Nothing is stored; send it every few seconds while typing. Only a person who may write here may send it.'
1129
+ }),
1130
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)'),
1131
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
1132
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
1133
+ tslib_1.__metadata("design:type", Function),
1134
+ tslib_1.__metadata("design:paramtypes", [String, Object]),
1135
+ tslib_1.__metadata("design:returntype", Promise)
1136
+ ], ChatSessionResolver.prototype, "sayTyping", null);
947
1137
  tslib_1.__decorate([
948
1138
  (0, type_graphql_1.Mutation)(() => Boolean, {
949
1139
  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.'
@@ -990,6 +1180,17 @@ tslib_1.__decorate([
990
1180
  tslib_1.__metadata("design:paramtypes", [String, Object]),
991
1181
  tslib_1.__metadata("design:returntype", Promise)
992
1182
  ], ChatSessionResolver.prototype, "chatSessionParticipants", null);
1183
+ tslib_1.__decorate([
1184
+ (0, type_graphql_1.Query)(() => Boolean, {
1185
+ description: "Whether the caller may bring people into this conversation now - anyone in an open one, the owner of a group, nobody in a private one. A hint for the screen; the invite and the mention decide again."
1186
+ }),
1187
+ (0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "query", domainOwnerGranted: true)'),
1188
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
1189
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
1190
+ tslib_1.__metadata("design:type", Function),
1191
+ tslib_1.__metadata("design:paramtypes", [String, Object]),
1192
+ tslib_1.__metadata("design:returntype", Promise)
1193
+ ], ChatSessionResolver.prototype, "mayInviteToAISession", null);
993
1194
  tslib_1.__decorate([
994
1195
  (0, type_graphql_1.Mutation)(() => Boolean, {
995
1196
  description: 'Presence heartbeat — bump current user\'s lastSeenAt in the session (online indicator).'