@things-factory/ai-assistant 10.1.27 → 10.1.29

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 (210) hide show
  1. package/client/components/assistant-chat.ts +701 -63
  2. package/client/components/assistant-mode.test.ts +105 -55
  3. package/client/components/assistant-mode.ts +144 -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 +54 -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 +37 -1
  14. package/client/utils/assistant-session-transport.ts +13 -5
  15. package/dist-client/components/assistant-chat.d.ts +140 -13
  16. package/dist-client/components/assistant-chat.js +659 -71
  17. package/dist-client/components/assistant-chat.js.map +1 -1
  18. package/dist-client/components/assistant-mode.d.ts +102 -68
  19. package/dist-client/components/assistant-mode.js +79 -19
  20. package/dist-client/components/assistant-mode.js.map +1 -1
  21. package/dist-client/components/assistant-mode.test.js +90 -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 +49 -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 +20 -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 +13 -5
  52. package/dist-client/utils/assistant-session-transport.js.map +1 -1
  53. package/dist-server/door-declaration-report.d.ts +14 -0
  54. package/dist-server/door-declaration-report.js +54 -0
  55. package/dist-server/door-declaration-report.js.map +1 -0
  56. package/dist-server/service/assistant-capability-resolver.d.ts +17 -0
  57. package/dist-server/service/assistant-capability-resolver.js +88 -0
  58. package/dist-server/service/assistant-capability-resolver.js.map +1 -0
  59. package/dist-server/service/assistant-chat-resolver.d.ts +6 -6
  60. package/dist-server/service/assistant-chat-resolver.js +185 -44
  61. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  62. package/dist-server/service/assistant-mode/assistant-mode-resolver.d.ts +33 -0
  63. package/dist-server/service/assistant-mode/assistant-mode-resolver.js +148 -0
  64. package/dist-server/service/assistant-mode/assistant-mode-resolver.js.map +1 -0
  65. package/dist-server/service/assistant-mode/index.d.ts +9 -0
  66. package/dist-server/service/assistant-mode/index.js +19 -0
  67. package/dist-server/service/assistant-mode/index.js.map +1 -0
  68. package/dist-server/service/assistant-mode/registry.d.ts +18 -0
  69. package/dist-server/service/assistant-mode/registry.js +136 -0
  70. package/dist-server/service/assistant-mode/registry.js.map +1 -0
  71. package/dist-server/service/assistant-mode/resolve.d.ts +43 -0
  72. package/dist-server/service/assistant-mode/resolve.js +65 -0
  73. package/dist-server/service/assistant-mode/resolve.js.map +1 -0
  74. package/dist-server/service/assistant-mode/types.d.ts +120 -0
  75. package/dist-server/service/assistant-mode/types.js +12 -0
  76. package/dist-server/service/assistant-mode/types.js.map +1 -0
  77. package/dist-server/service/chat-message/carried-turns.d.ts +48 -0
  78. package/dist-server/service/chat-message/carried-turns.js +92 -0
  79. package/dist-server/service/chat-message/carried-turns.js.map +1 -0
  80. package/dist-server/service/chat-message/chat-message-subscription.d.ts +1 -1
  81. package/dist-server/service/chat-message/chat-message-subscription.js +24 -6
  82. package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -1
  83. package/dist-server/service/chat-message/chat-message.d.ts +32 -0
  84. package/dist-server/service/chat-message/chat-message.js +17 -0
  85. package/dist-server/service/chat-message/chat-message.js.map +1 -1
  86. package/dist-server/service/chat-message/door-access.d.ts +27 -0
  87. package/dist-server/service/chat-message/door-access.js +34 -0
  88. package/dist-server/service/chat-message/door-access.js.map +1 -0
  89. package/dist-server/service/chat-message/fold-session-history.d.ts +30 -0
  90. package/dist-server/service/chat-message/fold-session-history.js +133 -0
  91. package/dist-server/service/chat-message/fold-session-history.js.map +1 -0
  92. package/dist-server/service/chat-message/llm-history.d.ts +7 -0
  93. package/dist-server/service/chat-message/llm-history.js +23 -11
  94. package/dist-server/service/chat-message/llm-history.js.map +1 -1
  95. package/dist-server/service/chat-message/message-doors.d.ts +29 -0
  96. package/dist-server/service/chat-message/message-doors.js +107 -0
  97. package/dist-server/service/chat-message/message-doors.js.map +1 -0
  98. package/dist-server/service/chat-message/proposal-outcome.d.ts +18 -0
  99. package/dist-server/service/chat-message/proposal-outcome.js +31 -0
  100. package/dist-server/service/chat-message/proposal-outcome.js.map +1 -0
  101. package/dist-server/service/chat-message/reader-gate.d.ts +3 -0
  102. package/dist-server/service/chat-message/reader-gate.js +39 -0
  103. package/dist-server/service/chat-message/reader-gate.js.map +1 -0
  104. package/dist-server/service/chat-message/room-message.d.ts +4 -0
  105. package/dist-server/service/chat-message/room-message.js +25 -0
  106. package/dist-server/service/chat-message/room-message.js.map +1 -0
  107. package/dist-server/service/chat-session/chat-session.d.ts +36 -0
  108. package/dist-server/service/chat-session/chat-session.js +27 -2
  109. package/dist-server/service/chat-session/chat-session.js.map +1 -1
  110. package/dist-server/service/chat-session/session-anchor.d.ts +10 -1
  111. package/dist-server/service/chat-session/session-anchor.js +12 -4
  112. package/dist-server/service/chat-session/session-anchor.js.map +1 -1
  113. package/dist-server/service/chat-session/session-inbox.d.ts +6 -1
  114. package/dist-server/service/chat-session/session-inbox.js +3 -2
  115. package/dist-server/service/chat-session/session-inbox.js.map +1 -1
  116. package/dist-server/service/chat-session/session-kind.d.ts +14 -0
  117. package/dist-server/service/chat-session/session-kind.js +30 -0
  118. package/dist-server/service/chat-session/session-kind.js.map +1 -0
  119. package/dist-server/service/chat-session/session-visibility.d.ts +15 -0
  120. package/dist-server/service/chat-session/session-visibility.js +35 -0
  121. package/dist-server/service/chat-session/session-visibility.js.map +1 -0
  122. package/dist-server/service/chat-session/station-session-guard.d.ts +1 -5
  123. package/dist-server/service/chat-session/station-session-guard.js +7 -2
  124. package/dist-server/service/chat-session/station-session-guard.js.map +1 -1
  125. package/dist-server/service/chat-session-participant/mention-invite.d.ts +29 -0
  126. package/dist-server/service/chat-session-participant/mention-invite.js +103 -0
  127. package/dist-server/service/chat-session-participant/mention-invite.js.map +1 -0
  128. package/dist-server/service/chat-session-resolver.d.ts +17 -0
  129. package/dist-server/service/chat-session-resolver.js +271 -15
  130. package/dist-server/service/chat-session-resolver.js.map +1 -1
  131. package/dist-server/service/grounding-sources.d.ts +14 -0
  132. package/dist-server/service/grounding-sources.js +26 -0
  133. package/dist-server/service/grounding-sources.js.map +1 -0
  134. package/dist-server/service/index.d.ts +9 -1
  135. package/dist-server/service/index.js +21 -0
  136. package/dist-server/service/index.js.map +1 -1
  137. package/dist-server/service/request-translator.d.ts +2 -0
  138. package/dist-server/service/request-translator.js +20 -0
  139. package/dist-server/service/request-translator.js.map +1 -0
  140. package/dist-server/service/testing/conversation-script.d.ts +123 -0
  141. package/dist-server/service/testing/conversation-script.js +202 -0
  142. package/dist-server/service/testing/conversation-script.js.map +1 -0
  143. package/dist-server/service/testing/mode-conformance.d.ts +50 -0
  144. package/dist-server/service/testing/mode-conformance.js +103 -0
  145. package/dist-server/service/testing/mode-conformance.js.map +1 -0
  146. package/dist-server/tool-registry-boot-report.d.ts +3 -0
  147. package/dist-server/tool-registry-boot-report.js +32 -2
  148. package/dist-server/tool-registry-boot-report.js.map +1 -1
  149. package/dist-server/tsconfig.tsbuildinfo +1 -1
  150. package/package.json +5 -5
  151. package/server/door-declaration-report.test.ts +53 -0
  152. package/server/door-declaration-report.ts +61 -0
  153. package/server/explicit-graphql-types.test.ts +56 -0
  154. package/server/service/assistant-capability-resolver.ts +66 -0
  155. package/server/service/assistant-chat-resolver.ts +205 -42
  156. package/server/service/assistant-mode/assistant-mode-resolver.ts +108 -0
  157. package/server/service/assistant-mode/index.ts +16 -0
  158. package/server/service/assistant-mode/registry.test.ts +105 -0
  159. package/server/service/assistant-mode/registry.ts +147 -0
  160. package/server/service/assistant-mode/resolve.test.ts +120 -0
  161. package/server/service/assistant-mode/resolve.ts +105 -0
  162. package/server/service/assistant-mode/subject-defaults.test.ts +86 -0
  163. package/server/service/assistant-mode/types.ts +121 -0
  164. package/server/service/chat-message/carried-turns.test.ts +80 -0
  165. package/server/service/chat-message/carried-turns.ts +112 -0
  166. package/server/service/chat-message/chat-message-subscription.ts +20 -4
  167. package/server/service/chat-message/chat-message.ts +44 -0
  168. package/server/service/chat-message/door-access.test.ts +72 -0
  169. package/server/service/chat-message/door-access.ts +65 -0
  170. package/server/service/chat-message/fold-history-window.test.ts +184 -0
  171. package/server/service/chat-message/fold-session-history.test.ts +98 -0
  172. package/server/service/chat-message/fold-session-history.ts +115 -0
  173. package/server/service/chat-message/llm-history.test.ts +58 -7
  174. package/server/service/chat-message/llm-history.ts +30 -11
  175. package/server/service/chat-message/message-doors.test.ts +87 -0
  176. package/server/service/chat-message/message-doors.ts +117 -0
  177. package/server/service/chat-message/proposal-outcome.test.ts +48 -0
  178. package/server/service/chat-message/proposal-outcome.ts +42 -0
  179. package/server/service/chat-message/reader-gate.ts +44 -0
  180. package/server/service/chat-message/room-message.test.ts +21 -0
  181. package/server/service/chat-message/room-message.ts +22 -0
  182. package/server/service/chat-session/chat-session.ts +46 -1
  183. package/server/service/chat-session/session-anchor.ts +12 -3
  184. package/server/service/chat-session/session-inbox.test.ts +18 -0
  185. package/server/service/chat-session/session-inbox.ts +11 -2
  186. package/server/service/chat-session/session-kind.test.ts +24 -0
  187. package/server/service/chat-session/session-kind.ts +32 -0
  188. package/server/service/chat-session/session-visibility.test.ts +52 -0
  189. package/server/service/chat-session/session-visibility.ts +34 -0
  190. package/server/service/chat-session/station-session-guard.ts +10 -2
  191. package/server/service/chat-session-participant/mention-invite.test.ts +125 -0
  192. package/server/service/chat-session-participant/mention-invite.ts +121 -0
  193. package/server/service/chat-session-resolver.ts +276 -15
  194. package/server/service/grounding-sources.test.ts +55 -0
  195. package/server/service/grounding-sources.ts +46 -0
  196. package/server/service/index.ts +21 -0
  197. package/server/service/request-translator.test.ts +45 -0
  198. package/server/service/request-translator.ts +33 -0
  199. package/server/service/station-session-guard.test.ts +12 -5
  200. package/server/service/testing/conversation-scenario.test.ts +163 -0
  201. package/server/service/testing/conversation-script.test.ts +131 -0
  202. package/server/service/testing/conversation-script.ts +288 -0
  203. package/server/service/testing/mode-conformance.ts +122 -0
  204. package/server/tool-registry-boot-report.ts +31 -2
  205. package/test/assistant-chat-call-sites.test.ts +82 -0
  206. package/translations/en.json +46 -3
  207. package/translations/ja.json +46 -3
  208. package/translations/ko.json +46 -3
  209. package/translations/ms.json +46 -3
  210. package/translations/zh.json +46 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/types.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG","sourcesContent":["/**\n * What a conversation is, declared by the app server that owns the screen (ADR-0061 decision 7).\n *\n * ADR-0058 put these five in one value so they could not fall out of step, but the value lived in\n * the browser and travelled on every request: the persona, the tool categories, the model and the\n * limits were the caller's to choose. A caller choosing its own tool categories is a caller\n * choosing its own authority, so the declaration moved here and the client sends only which one it\n * is talking in (`kind`) and what about (`target`).\n */\n\n/** A `/` sentence this mode offers, and what it needs to be answerable. */\nexport interface AssistantModeSlash {\n /** Short name shown in the list. */\n name: string\n /** i18n key for the one-line description. The server translates it into the request's locale. */\n descriptionKey: string\n /** i18n key for the sentence that lands in the input. */\n templateKey: string\n /**\n * The tools this sentence needs. A mode must not offer work it cannot do (ADR-0058 decision 3),\n * and a name outside `toolCategories` is reported at boot rather than when a person presses it.\n */\n tools?: string[]\n}\n\nexport interface AssistantModeSpec {\n /** `<app>.<surface>` — `twin.space`, `plant.station`, `figure.modeller`. */\n kind: string\n\n /**\n * Which kinds of subject this conversation stands for — `['board', 'space']` (ADR-0072 A17 ①).\n *\n * A screen says what it is showing (`context.subject`), and these say which conversations that\n * subject can carry. It is a list of types and not a predicate on purpose: a list can be counted\n * at boot and pinned by a test, while a function can only be run.\n *\n * On the twin modeller nothing could be asked about operations, because an open board pinned the\n * authoring conversation and that conversation opens one category. A conversation that also\n * stands for `board` is how the same screen carries both.\n */\n subjects?: string[]\n\n /**\n * The conversation a screen opens when the person has not chosen — one per subject type\n * (ADR-0072 A17 ②).\n *\n * Registration order must not decide this: whichever module booted last would silently become\n * the default. Exactly one mode declares each subject type, and the boot report says so when a\n * type has none or more than one.\n */\n defaultFor?: string[]\n\n /**\n * i18n key for what this conversation is called where a person chooses between them —\n * \"이 그림 고치기\", \"이 현장 지금 어때\". A mode a screen may offer needs one.\n */\n labelKey?: string\n /** Who the assistant is here, and what it must not claim to do. The server's, not the caller's. */\n systemPrompt: string\n /** Which tool categories this conversation may use. Absent or empty exposes none. */\n toolCategories?: string[]\n /** Make the model read before it answers — for surfaces whose subject is live state. */\n requireToolOnFirstTurn?: boolean\n /** Model and limits are policy, not a request field. */\n model?: string\n temperature?: number\n maxTokens?: number\n /** What `/` offers. Declared as keys; the server translates. */\n slashTemplates?: AssistantModeSlash[]\n /** What `@` offers. */\n catalogEntries?: any[]\n /**\n * i18n key for the words in the empty input.\n *\n * The screen says what this conversation is for, and that differs by mode: figure's modeller had\n * \"describe the figure you want\" while the person was there to change the figure already open.\n */\n placeholderKey?: string\n\n /**\n * 빈 대화가 무엇을 말하는가 — 제목과 한 줄 (ADR-0061 결정 7).\n *\n * 자리표와 `/` 는 대화를 따라가는데 이 블록만 host 속성에 남아 있었다. 그래서 머리에는 「이 도형\n * 고치기」가 서 있고 본문은 「만들 Figure 를 설명하세요」라고 적혀 있었다(2026-09-18, Figure 레인\n * :3300). 한 대화가 화면에 무엇을 말하는가는 한 자리에서 나와야 한다.\n */\n introKeys?: { title: string; subtitle: string }\n\n /**\n * 빈 대화의 예시 — 「이 대화에서 할 수 있는 말」.\n *\n * 예시는 그 도구로 **실제로 할 수 있는 말**이어야 한다. 할 수 없는 것을 예시로 두면 빈 화면이 거짓을\n * 가르친다. 대화마다 열리는 도구가 다르므로 예시도 대화를 따라간다.\n */\n exampleKeys?: Array<{ labelKey: string; itemKeys: string[] }>\n\n /** i18n key for the line under an empty conversation. */\n footerNoticeKey?: string\n /** i18n key for the line shown once the host reports a proposal staged. */\n stagedNoticeKey?: string\n /** Follow-up chips for a proposal that arrived without its own. */\n proposalChoices?: any[]\n}\n\n/** A declaration that registered but is not what the contract asks for. Code + params, not prose. */\nexport interface AssistantModeWarning {\n /**\n * `assistant-mode-slash-outside-tools` — a `/` sentence needs a tool this mode does not open.\n * `assistant-mode-subject-without-default` — a subject type no conversation claims as its default.\n * `assistant-mode-subject-default-twice` — two conversations claim the same subject type.\n * `assistant-mode-offered-without-label` — a conversation a screen may offer with nothing to call it.\n */\n code:\n | 'assistant-mode-slash-outside-tools'\n | 'assistant-mode-subject-without-default'\n | 'assistant-mode-subject-default-twice'\n | 'assistant-mode-offered-without-label'\n params: { kind: string; slash?: string; tool?: string; subject?: string; kinds?: string[] }\n /** For a person reading the boot log; callers act on the code and params. */\n message: string\n}\n"]}
@@ -0,0 +1,48 @@
1
+ /** One turn as the caller carried it over. */
2
+ export interface CarriedTurn {
3
+ role?: string;
4
+ content?: string;
5
+ /** Where an assistant turn came from, so the server can read the original instead of trusting it. */
6
+ sourceSessionId?: string;
7
+ sourceMessageId?: string;
8
+ }
9
+ export interface PlannedRow {
10
+ role: 'user' | 'assistant' | 'system';
11
+ content: string;
12
+ /** People's words are addressed to the room — this door is not the one that asks the assistant. */
13
+ addressee?: 'room';
14
+ /** Set when the row must be filled from a stored message before it is written. */
15
+ copyOf?: {
16
+ sessionId?: string;
17
+ messageId: string;
18
+ };
19
+ }
20
+ /** How much of a carried conversation is kept — context, not an archive. */
21
+ export declare const CARRIED_TURN_LIMIT = 40;
22
+ export declare const CARRIED_CONTENT_LIMIT = 4000;
23
+ /**
24
+ * What to write, before anything is looked up.
25
+ *
26
+ * Pure: the caller resolves `copyOf` against storage and decides what a missing original becomes.
27
+ */
28
+ export declare function planCarriedTurns(turns: CarriedTurn[]): PlannedRow[];
29
+ /**
30
+ * An answer with no original, kept as a quotation.
31
+ *
32
+ * English, like the other lines the model reads (`[system]`, the omission notices): this row is
33
+ * context for the model first, and a person reading it sees the quoted text either way.
34
+ */
35
+ export declare function quoteCarriedAnswer(content: string): string;
36
+ /**
37
+ * 원본을 찾는 조건.
38
+ *
39
+ * 문의 설명은 「`sourceSessionId` 와 `sourceMessageId` 로 원본을 댄다」고 말한다. 조회가 메시지 id 와
40
+ * 테넌트만 보면, 서로 어긋난 두 인자가 와도 설명과 다르게 통과한다 — 설명과 코드가 갈리는 자리다
41
+ * [수석 아키텍트가 ADR-0070 구현 검토에서 찾음, 2026-09-18].
42
+ *
43
+ * 테넌트는 언제나 건다. 대화를 대면 그것도 건다.
44
+ */
45
+ export declare function carriedOriginalWhere(domainId: string, copyOf: {
46
+ sessionId?: string;
47
+ messageId: string;
48
+ }): any;
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ /*
3
+ * A conversation carried in from somewhere else, turned into rows this session can hold.
4
+ *
5
+ * ── Why the assistant's turns are not simply written (ADR-0070 A15 decision ③) ──
6
+ * `appendSessionTurns` took whatever roles the caller sent, so a client could write a row that
7
+ * reads as the assistant speaking. Every participant then sees a sentence the assistant never
8
+ * said, and the model reads it back as its own earlier turn. Roles are the server's to write.
9
+ *
10
+ * So a carried assistant turn is kept only when its original can be found — the caller names it
11
+ * (`sourceSessionId` · `sourceMessageId`) and the server copies the stored text. Without an
12
+ * original it is kept as a quotation on a `system` row: the content is preserved, and nobody is
13
+ * told the assistant said it here.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.CARRIED_CONTENT_LIMIT = exports.CARRIED_TURN_LIMIT = void 0;
17
+ exports.planCarriedTurns = planCarriedTurns;
18
+ exports.quoteCarriedAnswer = quoteCarriedAnswer;
19
+ exports.carriedOriginalWhere = carriedOriginalWhere;
20
+ /** How much of a carried conversation is kept — context, not an archive. */
21
+ exports.CARRIED_TURN_LIMIT = 40;
22
+ exports.CARRIED_CONTENT_LIMIT = 4000;
23
+ /**
24
+ * What to write, before anything is looked up.
25
+ *
26
+ * Pure: the caller resolves `copyOf` against storage and decides what a missing original becomes.
27
+ */
28
+ function planCarriedTurns(turns) {
29
+ const planned = [];
30
+ for (const turn of Array.isArray(turns) ? turns : []) {
31
+ const content = (turn?.content ?? '').trim().slice(0, exports.CARRIED_CONTENT_LIMIT);
32
+ if (!content)
33
+ continue;
34
+ /*
35
+ * 이 문은 **사람의 말과 어시스턴트의 답**만 옮긴다(ADR-0070 결정 ③). 그 밖의 역할은 받지 않는다 —
36
+ * `system` 행은 서버가 쓰는 자리이고, 모델은 그 줄을 사람의 말보다 무겁게 읽는다. 클라이언트가
37
+ * 「이것은 시스템이 한 말」이라고 적을 수 있으면 역할을 서버가 쓴다는 결정이 무너진다.
38
+ *
39
+ * 사람이 친 것이 아닌 맥락은 `appendSessionNote` 가 자리다 — 그 문은 서버가 `system` 으로 적는다.
40
+ */
41
+ const asked = turn?.role === 'ai' ? 'assistant' : turn?.role;
42
+ if (asked !== 'assistant' && asked !== 'user') {
43
+ throw new Error(`[ai-assistant/carried-turns] role '${String(turn?.role)}' cannot be carried. This door carries what ` +
44
+ "people said and what the assistant answered. Context that nobody typed goes through `appendSessionNote`, " +
45
+ 'which writes the system row itself.');
46
+ }
47
+ const role = asked;
48
+ if (role === 'user') {
49
+ planned.push({ role: 'user', content, addressee: 'room' });
50
+ continue;
51
+ }
52
+ if (role === 'assistant') {
53
+ if (turn?.sourceMessageId) {
54
+ planned.push({
55
+ role: 'assistant',
56
+ content,
57
+ copyOf: { sessionId: turn.sourceSessionId, messageId: turn.sourceMessageId }
58
+ });
59
+ }
60
+ else {
61
+ planned.push({ role: 'system', content: quoteCarriedAnswer(content) });
62
+ }
63
+ continue;
64
+ }
65
+ }
66
+ return planned.slice(-exports.CARRIED_TURN_LIMIT);
67
+ }
68
+ /**
69
+ * An answer with no original, kept as a quotation.
70
+ *
71
+ * English, like the other lines the model reads (`[system]`, the omission notices): this row is
72
+ * context for the model first, and a person reading it sees the quoted text either way.
73
+ */
74
+ function quoteCarriedAnswer(content) {
75
+ return `carried from another conversation, quoted — the assistant did not say this here: "${content}"`;
76
+ }
77
+ /**
78
+ * 원본을 찾는 조건.
79
+ *
80
+ * 문의 설명은 「`sourceSessionId` 와 `sourceMessageId` 로 원본을 댄다」고 말한다. 조회가 메시지 id 와
81
+ * 테넌트만 보면, 서로 어긋난 두 인자가 와도 설명과 다르게 통과한다 — 설명과 코드가 갈리는 자리다
82
+ * [수석 아키텍트가 ADR-0070 구현 검토에서 찾음, 2026-09-18].
83
+ *
84
+ * 테넌트는 언제나 건다. 대화를 대면 그것도 건다.
85
+ */
86
+ function carriedOriginalWhere(domainId, copyOf) {
87
+ return {
88
+ id: copyOf.messageId,
89
+ session: copyOf.sessionId ? { id: copyOf.sessionId, domain: { id: domainId } } : { domain: { id: domainId } }
90
+ };
91
+ }
92
+ //# sourceMappingURL=carried-turns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"carried-turns.js","sourceRoot":"","sources":["../../../server/service/chat-message/carried-turns.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;;AA6BH,4CA4CC;AAQD,gDAEC;AAWD,oDAKC;AA/ED,4EAA4E;AAC/D,QAAA,kBAAkB,GAAG,EAAE,CAAA;AACvB,QAAA,qBAAqB,GAAG,IAAI,CAAA;AAEzC;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,KAAoB;IACnD,MAAM,OAAO,GAAiB,EAAE,CAAA;IAEhC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACrD,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,6BAAqB,CAAC,CAAA;QAC5E,IAAI,CAAC,OAAO;YAAE,SAAQ;QAEtB;;;;;;WAMG;QACH,MAAM,KAAK,GAAG,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAA;QAC5D,IAAI,KAAK,KAAK,WAAW,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;YAC9C,MAAM,IAAI,KAAK,CACb,sCAAsC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,8CAA8C;gBACpG,2GAA2G;gBAC3G,qCAAqC,CACxC,CAAA;QACH,CAAC;QACD,MAAM,IAAI,GAAG,KAAK,CAAA;QAElB,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YACpB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAA;YAC1D,SAAQ;QACV,CAAC;QAED,IAAI,IAAI,KAAK,WAAW,EAAE,CAAC;YACzB,IAAI,IAAI,EAAE,eAAe,EAAE,CAAC;gBAC1B,OAAO,CAAC,IAAI,CAAC;oBACX,IAAI,EAAE,WAAW;oBACjB,OAAO;oBACP,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,eAAe,EAAE,SAAS,EAAE,IAAI,CAAC,eAAe,EAAE;iBAC7E,CAAC,CAAA;YACJ,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;YACxE,CAAC;YACD,SAAQ;QACV,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,0BAAkB,CAAC,CAAA;AAC3C,CAAC;AAED;;;;;GAKG;AACH,SAAgB,kBAAkB,CAAC,OAAe;IAChD,OAAO,qFAAqF,OAAO,GAAG,CAAA;AACxG,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,oBAAoB,CAAC,QAAgB,EAAE,MAAiD;IACtG,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,SAAS;QACpB,OAAO,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE;KAC9G,CAAA;AACH,CAAC","sourcesContent":["/*\n * A conversation carried in from somewhere else, turned into rows this session can hold.\n *\n * ── Why the assistant's turns are not simply written (ADR-0070 A15 decision ③) ──\n * `appendSessionTurns` took whatever roles the caller sent, so a client could write a row that\n * reads as the assistant speaking. Every participant then sees a sentence the assistant never\n * said, and the model reads it back as its own earlier turn. Roles are the server's to write.\n *\n * So a carried assistant turn is kept only when its original can be found — the caller names it\n * (`sourceSessionId` · `sourceMessageId`) and the server copies the stored text. Without an\n * original it is kept as a quotation on a `system` row: the content is preserved, and nobody is\n * told the assistant said it here.\n */\n\n/** One turn as the caller carried it over. */\nexport interface CarriedTurn {\n role?: string\n content?: string\n /** Where an assistant turn came from, so the server can read the original instead of trusting it. */\n sourceSessionId?: string\n sourceMessageId?: string\n}\n\nexport interface PlannedRow {\n role: 'user' | 'assistant' | 'system'\n content: string\n /** People's words are addressed to the room — this door is not the one that asks the assistant. */\n addressee?: 'room'\n /** Set when the row must be filled from a stored message before it is written. */\n copyOf?: { sessionId?: string; messageId: string }\n}\n\n/** How much of a carried conversation is kept — context, not an archive. */\nexport const CARRIED_TURN_LIMIT = 40\nexport const CARRIED_CONTENT_LIMIT = 4000\n\n/**\n * What to write, before anything is looked up.\n *\n * Pure: the caller resolves `copyOf` against storage and decides what a missing original becomes.\n */\nexport function planCarriedTurns(turns: CarriedTurn[]): PlannedRow[] {\n const planned: PlannedRow[] = []\n\n for (const turn of Array.isArray(turns) ? turns : []) {\n const content = (turn?.content ?? '').trim().slice(0, CARRIED_CONTENT_LIMIT)\n if (!content) continue\n\n /*\n * 이 문은 **사람의 말과 어시스턴트의 답**만 옮긴다(ADR-0070 결정 ③). 그 밖의 역할은 받지 않는다 —\n * `system` 행은 서버가 쓰는 자리이고, 모델은 그 줄을 사람의 말보다 무겁게 읽는다. 클라이언트가\n * 「이것은 시스템이 한 말」이라고 적을 수 있으면 역할을 서버가 쓴다는 결정이 무너진다.\n *\n * 사람이 친 것이 아닌 맥락은 `appendSessionNote` 가 자리다 — 그 문은 서버가 `system` 으로 적는다.\n */\n const asked = turn?.role === 'ai' ? 'assistant' : turn?.role\n if (asked !== 'assistant' && asked !== 'user') {\n throw new Error(\n `[ai-assistant/carried-turns] role '${String(turn?.role)}' cannot be carried. This door carries what ` +\n \"people said and what the assistant answered. Context that nobody typed goes through `appendSessionNote`, \" +\n 'which writes the system row itself.'\n )\n }\n const role = asked\n\n if (role === 'user') {\n planned.push({ role: 'user', content, addressee: 'room' })\n continue\n }\n\n if (role === 'assistant') {\n if (turn?.sourceMessageId) {\n planned.push({\n role: 'assistant',\n content,\n copyOf: { sessionId: turn.sourceSessionId, messageId: turn.sourceMessageId }\n })\n } else {\n planned.push({ role: 'system', content: quoteCarriedAnswer(content) })\n }\n continue\n }\n }\n\n return planned.slice(-CARRIED_TURN_LIMIT)\n}\n\n/**\n * An answer with no original, kept as a quotation.\n *\n * English, like the other lines the model reads (`[system]`, the omission notices): this row is\n * context for the model first, and a person reading it sees the quoted text either way.\n */\nexport function quoteCarriedAnswer(content: string): string {\n return `carried from another conversation, quoted — the assistant did not say this here: \"${content}\"`\n}\n\n/**\n * 원본을 찾는 조건.\n *\n * 문의 설명은 「`sourceSessionId` 와 `sourceMessageId` 로 원본을 댄다」고 말한다. 조회가 메시지 id 와\n * 테넌트만 보면, 서로 어긋난 두 인자가 와도 설명과 다르게 통과한다 — 설명과 코드가 갈리는 자리다\n * [수석 아키텍트가 ADR-0070 구현 검토에서 찾음, 2026-09-18].\n *\n * 테넌트는 언제나 건다. 대화를 대면 그것도 건다.\n */\nexport function carriedOriginalWhere(domainId: string, copyOf: { sessionId?: string; messageId: string }): any {\n return {\n id: copyOf.messageId,\n session: copyOf.sessionId ? { id: copyOf.sessionId, domain: { id: domainId } } : { domain: { id: domainId } }\n }\n}\n"]}
@@ -4,5 +4,5 @@ export { publishChatMessage } from './chat-message-publish.js';
4
4
  export { CHAT_MESSAGE_TOPIC, chatMessageFilter } from './chat-message-broadcast.js';
5
5
  export type { ChatMessagePayload } from './chat-message-broadcast.js';
6
6
  export declare class ChatMessageSubscription {
7
- chatMessageActivity(payload: ChatMessagePayload, sessionId: string): ChatMessage;
7
+ chatMessageActivity(payload: ChatMessagePayload, sessionId: string, context: any): Promise<ChatMessage>;
8
8
  }
@@ -23,6 +23,7 @@ const shell_1 = require("@things-factory/shell");
23
23
  const chat_message_js_1 = require("./chat-message.js");
24
24
  const chat_message_broadcast_js_1 = require("./chat-message-broadcast.js");
25
25
  const subscribe_auth_js_1 = require("../subscribe-auth.js");
26
+ const reader_gate_js_1 = require("./reader-gate.js");
26
27
  // publishChatMessage 는 데코레이터 없는 chat-message-publish.ts 에 분리 (테스트 import 가능).
27
28
  var chat_message_publish_js_1 = require("./chat-message-publish.js");
28
29
  Object.defineProperty(exports, "publishChatMessage", { enumerable: true, get: function () { return chat_message_publish_js_1.publishChatMessage; } });
@@ -30,13 +31,24 @@ var chat_message_broadcast_js_2 = require("./chat-message-broadcast.js");
30
31
  Object.defineProperty(exports, "CHAT_MESSAGE_TOPIC", { enumerable: true, get: function () { return chat_message_broadcast_js_2.CHAT_MESSAGE_TOPIC; } });
31
32
  Object.defineProperty(exports, "chatMessageFilter", { enumerable: true, get: function () { return chat_message_broadcast_js_2.chatMessageFilter; } });
32
33
  let ChatMessageSubscription = class ChatMessageSubscription {
33
- chatMessageActivity(payload, sessionId) {
34
- return payload.message;
34
+ /*
35
+ * 방송도 **받는 사람 기준으로** 가린다 — ADR-0073 판정 1. 저장해 놓고 읽을 때만 가리면, 그 자리에
36
+ * 앉아 있던 사람은 방송으로 원문을 받는다. 같은 값이 두 길로 나가고 한쪽만 문을 본다.
37
+ */
38
+ async chatMessageActivity(payload, sessionId, context) {
39
+ const [message] = await (0, reader_gate_js_1.readerGate)(context)([payload.message]);
40
+ return message;
35
41
  }
36
42
  };
37
43
  exports.ChatMessageSubscription = ChatMessageSubscription;
38
44
  tslib_1.__decorate([
39
- (0, type_graphql_1.Subscription)({
45
+ (0, type_graphql_1.Subscription)(() => chat_message_js_1.ChatMessage, {
46
+ /*
47
+ * 돌려주는 형을 **명시한다**. 이 핸들러가 async 가 되면서(받는 사람마다 가리기 — ADR-0073 판정 1)
48
+ * 반환형이 `Promise<ChatMessage>` 가 되었고, type-graphql 의 reflection 은 Promise 안을 못 본다.
49
+ * 빠지면 스키마를 세울 때 NoExplicitTypeError 로 **패키지 적재 자체가 실패한다** — 시험이 아니라
50
+ * 부팅이 죽는다. plant 레인이 설치본에서 이 오류를 잡아 알려 주었다(2026-09-18).
51
+ */
40
52
  description: 'Subscribes to chat messages (user / assistant / system) of a session for real-time group chat.',
41
53
  subscribe: async ({ args, context }) => {
42
54
  const { domain, user } = context.state;
@@ -44,12 +56,18 @@ tslib_1.__decorate([
44
56
  await (0, subscribe_auth_js_1.assertDomainSubscribeAllowed)(domain, user, (d, u) => process.superUserGranted(d, u));
45
57
  return (0, graphql_yoga_1.pipe)(shell_1.pubsub.subscribe(chat_message_broadcast_js_1.CHAT_MESSAGE_TOPIC), (0, graphql_yoga_1.filter)((0, chat_message_broadcast_js_1.chatMessageFilter)(domain.id, sessionId)));
46
58
  }
47
- }),
59
+ })
60
+ /*
61
+ * 방송도 **받는 사람 기준으로** 가린다 — ADR-0073 판정 1. 저장해 놓고 읽을 때만 가리면, 그 자리에
62
+ * 앉아 있던 사람은 방송으로 원문을 받는다. 같은 값이 두 길로 나가고 한쪽만 문을 본다.
63
+ */
64
+ ,
48
65
  tslib_1.__param(0, (0, type_graphql_1.Root)()),
49
66
  tslib_1.__param(1, (0, type_graphql_1.Arg)('sessionId')),
67
+ tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
50
68
  tslib_1.__metadata("design:type", Function),
51
- tslib_1.__metadata("design:paramtypes", [Object, String]),
52
- tslib_1.__metadata("design:returntype", chat_message_js_1.ChatMessage)
69
+ tslib_1.__metadata("design:paramtypes", [Object, String, Object]),
70
+ tslib_1.__metadata("design:returntype", Promise)
53
71
  ], ChatMessageSubscription.prototype, "chatMessageActivity", null);
54
72
  exports.ChatMessageSubscription = ChatMessageSubscription = tslib_1.__decorate([
55
73
  (0, type_graphql_1.Resolver)(chat_message_js_1.ChatMessage)
@@ -1 +1 @@
1
- {"version":3,"file":"chat-message-subscription.js","sourceRoot":"","sources":["../../../server/service/chat-message/chat-message-subscription.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;;;;;;;;GAcG;AACH,+CAAgE;AAChE,+CAA2C;AAC3C,iDAA8C;AAC9C,uDAA+C;AAC/C,2EAA4G;AAC5G,4DAAmE;AAEnE,8EAA8E;AAC9E,qEAA8D;AAArD,6HAAA,kBAAkB,OAAA;AAC3B,yEAAmF;AAA1E,+HAAA,kBAAkB,OAAA;AAAE,8HAAA,iBAAiB,OAAA;AAIvC,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IAalC,mBAAmB,CAAS,OAA2B,EAAoB,SAAiB;QAC1F,OAAO,OAAO,CAAC,OAAO,CAAA;IACxB,CAAC;CACF,CAAA;AAhBY,0DAAuB;AAalC;IAZC,IAAA,2BAAY,EAAC;QACZ,WAAW,EACT,gGAAgG;QAClG,SAAS,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;YACrC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;YACtC,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAAA;YAE1B,MAAM,IAAA,gDAA4B,EAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YAE1F,OAAO,IAAA,mBAAI,EAAC,cAAM,CAAC,SAAS,CAAC,8CAAkB,CAAC,EAAE,IAAA,qBAAM,EAAC,IAAA,6CAAiB,EAAC,MAAM,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;QACpG,CAAC;KACF,CAAC;IACmB,mBAAA,IAAA,mBAAI,GAAE,CAAA;IAA+B,mBAAA,IAAA,kBAAG,EAAC,WAAW,CAAC,CAAA;;;4CAAqB,6BAAW;kEAEzG;kCAfU,uBAAuB;IADnC,IAAA,uBAAQ,EAAC,6BAAW,CAAC;GACT,uBAAuB,CAgBnC","sourcesContent":["/*\n * ChatMessageSubscription — 그룹챗 메시지 실시간 방송.\n *\n * 클래스 이름이 `BoardChatMessageSubscription` 이었다. 전선 이름(`chatMessageActivity`)은 처음부터\n * 중립이었고 클래스 이름만 보드였다 — 중립 패키지로 옮기면서 고쳤다(2026-09-10). 전선이 안 바뀌므로\n * 소비처는 그대로다.\n *\n * ChatMessage(사용자 / AI / system) 가 영속될 때마다 같은 세션 구독자에게 방송 →\n * 그룹챗의 척추. 클라이언트(board-ai-chat)가 구독해 원격 메시지를 transcript 에 append.\n * 자기 발신 에코는 클라이언트가 message.id 로 dedup (mutation 응답으로 이미 렌더).\n *\n * patch spine(board-patch-subscription.ts)과 동일 패턴 — PubSub + graphql-ws, domain 격리.\n * 차이: 필터 키가 board 가 아닌 sessionId (채팅은 ChatSession 단위).\n * 순수 로직(토픽/필터)은 chat-message-broadcast.ts (테스트 가능). 여기엔 pubsub 의존만.\n */\nimport { Resolver, Subscription, Root, Arg } from 'type-graphql'\nimport { filter, pipe } from 'graphql-yoga'\nimport { pubsub } from '@things-factory/shell'\nimport { ChatMessage } from './chat-message.js'\nimport { CHAT_MESSAGE_TOPIC, chatMessageFilter, type ChatMessagePayload } from './chat-message-broadcast.js'\nimport { assertDomainSubscribeAllowed } from '../subscribe-auth.js'\n\n// publishChatMessage 는 데코레이터 없는 chat-message-publish.ts 에 분리 (테스트 import 가능).\nexport { publishChatMessage } from './chat-message-publish.js'\nexport { CHAT_MESSAGE_TOPIC, chatMessageFilter } from './chat-message-broadcast.js'\nexport type { ChatMessagePayload } from './chat-message-broadcast.js'\n\n@Resolver(ChatMessage)\nexport class ChatMessageSubscription {\n @Subscription({\n description:\n 'Subscribes to chat messages (user / assistant / system) of a session for real-time group chat.',\n subscribe: async ({ args, context }) => {\n const { domain, user } = context.state\n const { sessionId } = args\n\n await assertDomainSubscribeAllowed(domain, user, (d, u) => process.superUserGranted(d, u))\n\n return pipe(pubsub.subscribe(CHAT_MESSAGE_TOPIC), filter(chatMessageFilter(domain.id, sessionId)))\n }\n })\n chatMessageActivity(@Root() payload: ChatMessagePayload, @Arg('sessionId') sessionId: string): ChatMessage {\n return payload.message\n }\n}\n"]}
1
+ {"version":3,"file":"chat-message-subscription.js","sourceRoot":"","sources":["../../../server/service/chat-message/chat-message-subscription.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;;;;;;;;GAcG;AACH,+CAAqE;AACrE,+CAA2C;AAC3C,iDAA8C;AAC9C,uDAA+C;AAC/C,2EAA4G;AAC5G,4DAAmE;AACnE,qDAA6C;AAE7C,8EAA8E;AAC9E,qEAA8D;AAArD,6HAAA,kBAAkB,OAAA;AAC3B,yEAAmF;AAA1E,+HAAA,kBAAkB,OAAA;AAAE,8HAAA,iBAAiB,OAAA;AAIvC,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IAuB5B,AAJN;;;OAGG;IACH,KAAK,CAAC,mBAAmB,CACf,OAA2B,EACjB,SAAiB,EAC5B,OAAY;QAEnB,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,IAAA,2BAAU,EAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAA;QAC9D,OAAO,OAAsB,CAAA;IAC/B,CAAC;CACF,CAAA;AA/BY,0DAAuB;AAuB5B;IAtBL,IAAA,2BAAY,EAAC,GAAG,EAAE,CAAC,6BAAW,EAAE;QAC/B;;;;;WAKG;QACH,WAAW,EACT,gGAAgG;QAClG,SAAS,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;YACrC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;YACtC,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAAA;YAE1B,MAAM,IAAA,gDAA4B,EAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YAE1F,OAAO,IAAA,mBAAI,EAAC,cAAM,CAAC,SAAS,CAAC,8CAAkB,CAAC,EAAE,IAAA,qBAAM,EAAC,IAAA,6CAAiB,EAAC,MAAM,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;QACpG,CAAC;KACF,CAAC;IACF;;;OAGG;;IAEA,mBAAA,IAAA,mBAAI,GAAE,CAAA;IACN,mBAAA,IAAA,kBAAG,EAAC,WAAW,CAAC,CAAA;IAChB,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;kEAIP;kCA9BU,uBAAuB;IADnC,IAAA,uBAAQ,EAAC,6BAAW,CAAC;GACT,uBAAuB,CA+BnC","sourcesContent":["/*\n * ChatMessageSubscription — 그룹챗 메시지 실시간 방송.\n *\n * 클래스 이름이 `BoardChatMessageSubscription` 이었다. 전선 이름(`chatMessageActivity`)은 처음부터\n * 중립이었고 클래스 이름만 보드였다 — 중립 패키지로 옮기면서 고쳤다(2026-09-10). 전선이 안 바뀌므로\n * 소비처는 그대로다.\n *\n * ChatMessage(사용자 / AI / system) 가 영속될 때마다 같은 세션 구독자에게 방송 →\n * 그룹챗의 척추. 클라이언트(board-ai-chat)가 구독해 원격 메시지를 transcript 에 append.\n * 자기 발신 에코는 클라이언트가 message.id 로 dedup (mutation 응답으로 이미 렌더).\n *\n * patch spine(board-patch-subscription.ts)과 동일 패턴 — PubSub + graphql-ws, domain 격리.\n * 차이: 필터 키가 board 가 아닌 sessionId (채팅은 ChatSession 단위).\n * 순수 로직(토픽/필터)은 chat-message-broadcast.ts (테스트 가능). 여기엔 pubsub 의존만.\n */\nimport { Resolver, Subscription, Root, Arg, Ctx } from 'type-graphql'\nimport { filter, pipe } from 'graphql-yoga'\nimport { pubsub } from '@things-factory/shell'\nimport { ChatMessage } from './chat-message.js'\nimport { CHAT_MESSAGE_TOPIC, chatMessageFilter, type ChatMessagePayload } from './chat-message-broadcast.js'\nimport { assertDomainSubscribeAllowed } from '../subscribe-auth.js'\nimport { readerGate } from './reader-gate.js'\n\n// publishChatMessage 는 데코레이터 없는 chat-message-publish.ts 에 분리 (테스트 import 가능).\nexport { publishChatMessage } from './chat-message-publish.js'\nexport { CHAT_MESSAGE_TOPIC, chatMessageFilter } from './chat-message-broadcast.js'\nexport type { ChatMessagePayload } from './chat-message-broadcast.js'\n\n@Resolver(ChatMessage)\nexport class ChatMessageSubscription {\n @Subscription(() => ChatMessage, {\n /*\n * 돌려주는 형을 **명시한다**. 이 핸들러가 async 가 되면서(받는 사람마다 가리기 — ADR-0073 판정 1)\n * 반환형이 `Promise<ChatMessage>` 가 되었고, type-graphql 의 reflection 은 Promise 안을 못 본다.\n * 빠지면 스키마를 세울 때 NoExplicitTypeError 로 **패키지 적재 자체가 실패한다** — 시험이 아니라\n * 부팅이 죽는다. plant 레인이 설치본에서 이 오류를 잡아 알려 주었다(2026-09-18).\n */\n description:\n 'Subscribes to chat messages (user / assistant / system) of a session for real-time group chat.',\n subscribe: async ({ args, context }) => {\n const { domain, user } = context.state\n const { sessionId } = args\n\n await assertDomainSubscribeAllowed(domain, user, (d, u) => process.superUserGranted(d, u))\n\n return pipe(pubsub.subscribe(CHAT_MESSAGE_TOPIC), filter(chatMessageFilter(domain.id, sessionId)))\n }\n })\n /*\n * 방송도 **받는 사람 기준으로** 가린다 — ADR-0073 판정 1. 저장해 놓고 읽을 때만 가리면, 그 자리에\n * 앉아 있던 사람은 방송으로 원문을 받는다. 같은 값이 두 길로 나가고 한쪽만 문을 본다.\n */\n async chatMessageActivity(\n @Root() payload: ChatMessagePayload,\n @Arg('sessionId') sessionId: string,\n @Ctx() context: any\n ): Promise<ChatMessage> {\n const [message] = await readerGate(context)([payload.message])\n return message as ChatMessage\n }\n}\n"]}
@@ -6,6 +6,38 @@ export declare class ChatMessage {
6
6
  sessionId?: string;
7
7
  role: string;
8
8
  content: string;
9
+ /**
10
+ * 이 말이 누구에게 한 말인가 — `'assistant'` · `'room'`(ADR-0070 A15 결정 ①).
11
+ *
12
+ * **값은 그 행을 쓴 문이 찍는다.** 어시스턴트에게 묻는 문(`assistantChat`)이 저장한 사람의 행은
13
+ * `assistant` 이고, 사람끼리의 문(`appendSessionTurns`)이 저장한 행은 `room` 이다. 클라이언트가
14
+ * 인자로 보내지 않는다 — 인자로 받으면 「어느 문을 불렀나」와 「무엇을 적어 보냈나」 두 사실이
15
+ * 생기고, 둘은 어긋날 수 있다.
16
+ *
17
+ * 사람의 행에만 뜻이 있다. 어시스턴트의 답은 `parentMessage` 로 자기를 부른 턴에 귀속되고,
18
+ * `system` 행은 누구에게 한 말이 아니다.
19
+ */
20
+ addressee?: string;
21
+ /**
22
+ * 이 행의 값이 지나온 문들 — ADR-0073 판정 1.
23
+ *
24
+ * 어시스턴트의 답과 제안 행에만 채워진다. 사람의 말은 빈 목록이다 — 사람이 자기 입으로 한 말에는
25
+ * 문이 없다. 답을 저장할 때 그 턴에 부른 도구들의 `doors`(ADR-0072)를 합쳐 옮겨 적는다.
26
+ *
27
+ * 읽을 때마다 **읽는 사람**이 이 문을 전부 지나는지 보고, 못 지나면 본문 대신 한 줄이 나간다. 그래서
28
+ * 이 칸은 "누가 볼 수 있나"를 적은 것이고, 저장된 값 자체는 누구에게도 달라지지 않는다.
29
+ *
30
+ * 목록이 비면 누구나 읽는다. 등록기가 모르는 도구가 답에 끼면 `unresolved:<도구>` 가 적히고 아무도
31
+ * 못 지난다 — 모르는 값은 가려지는 쪽으로 넘어뜨린다.
32
+ */
33
+ doors?: string[];
34
+ /**
35
+ * 이 행이 **이 사람에게** 가려져 나갔는가 — 저장하지 않는다.
36
+ *
37
+ * 같은 행이 사람마다 다르게 나가므로 칸에 적을 값이 없다. 읽어 나갈 때 채운다. 화면은 이것으로
38
+ * 자물쇠를 그리고, 본문에 온 한 줄을 어시스턴트의 말로 오해하지 않는다.
39
+ */
40
+ redacted?: boolean;
9
41
  relatedPatchId?: string;
10
42
  relatedImportSessionId?: string;
11
43
  /**
@@ -75,6 +75,23 @@ tslib_1.__decorate([
75
75
  (0, type_graphql_1.Field)({ description: 'Message content' }),
76
76
  tslib_1.__metadata("design:type", String)
77
77
  ], ChatMessage.prototype, "content", void 0);
78
+ tslib_1.__decorate([
79
+ (0, typeorm_1.Column)({ type: 'varchar', length: 16, nullable: true }),
80
+ (0, type_graphql_1.Field)({ nullable: true, description: "Who this turn was for: 'assistant' | 'room'. Written by the door that stored it, never sent by the client." }),
81
+ tslib_1.__metadata("design:type", String)
82
+ ], ChatMessage.prototype, "addressee", void 0);
83
+ tslib_1.__decorate([
84
+ (0, typeorm_1.Column)({ type: 'simple-array', nullable: true }),
85
+ (0, type_graphql_1.Field)(() => [String], {
86
+ nullable: true,
87
+ description: 'GraphQL doors the tools behind this answer went through. Empty for what people typed. A reader who does not pass all of them gets a one-line notice in place of the content.'
88
+ }),
89
+ tslib_1.__metadata("design:type", Array)
90
+ ], ChatMessage.prototype, "doors", void 0);
91
+ tslib_1.__decorate([
92
+ (0, type_graphql_1.Field)({ nullable: true, description: 'True when the content was replaced because this reader does not pass the row doors.' }),
93
+ tslib_1.__metadata("design:type", Boolean)
94
+ ], ChatMessage.prototype, "redacted", void 0);
78
95
  tslib_1.__decorate([
79
96
  (0, typeorm_1.Column)({ nullable: true }),
80
97
  (0, type_graphql_1.Field)({ nullable: true, description: 'PatchEntry.id this message triggered (if any).' }),
@@ -1 +1 @@
1
- {"version":3,"file":"chat-message.js","sourceRoot":"","sources":["../../../server/service/chat-message/chat-message.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ChatMessage — ChatSession 의 한 메시지.\n *\n * role: 'user' | 'assistant' | 'system'\n * 'system' 은 사용자 직접 편집/import 진행 등 시스템이 자동 생성하는 컨텍스트 메시지.\n *\n * relatedPatchId / relatedImportSessionId 로 메시지가 트리거한 패치·임포트와 연결.\n */\nimport {\n Column,\n CreateDateColumn,\n DeleteDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\n\nimport { User } from '@things-factory/auth-base'\nimport { config } from '@things-factory/env'\n\nimport { ChatSession } from '../chat-session/chat-session.js'\n\nconst ORMCONFIG = config.get('ormconfig', {})\nconst DATABASE_TYPE = ORMCONFIG.type\n\n@Entity()\n@Index('ix_chat_message_1', (message: ChatMessage) => [message.session, message.createdAt])\n@ObjectType({ description: 'A single chat message in a ChatSession.' })\nexport class ChatMessage {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { nullable: true })\n readonly id?: string\n\n @ManyToOne(type => ChatSession, session => session.messages, { onDelete: 'CASCADE' })\n session?: ChatSession\n\n @RelationId((message: ChatMessage) => message.session)\n sessionId?: string\n\n @Column({ type: 'varchar', length: 16 })\n @Field({ description: \"'user' | 'assistant' | 'system'\" })\n role!: string\n\n @Column({\n type:\n DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'\n ? 'longtext'\n : DATABASE_TYPE == 'oracle'\n ? 'clob'\n : DATABASE_TYPE == 'mssql'\n ? 'nvarchar'\n : 'text',\n length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined\n })\n @Field({ description: 'Message content' })\n content!: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'PatchEntry.id this message triggered (if any).' })\n relatedPatchId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'ImportSession.id this message triggered (if any).' })\n relatedImportSessionId?: string\n\n /**\n * AI 가 응답을 만드는 동안 호출한 도구 trace — JSON-stringified.\n *\n * 컬럼은 string (PatchEntry.ops 와 동일 패턴), GraphQL 노출은 toolUsagesJson getter\n * 로 parse 된 객체. UI 의 fold-able 박스 (\"AI 가 이런 도구를 사용했습니다\") 가\n * 페이지 reload / loadHistory 후에도 살아있도록 영속.\n *\n * 형태: ToolUsage[] (assistant.ts 의 시간순 trace, summarizeToolResult 로 압축).\n * null 이면 도구 호출 없었던 메시지 (text-only 응답).\n */\n @Column({\n type:\n DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'\n ? 'longtext'\n : DATABASE_TYPE == 'oracle'\n ? 'clob'\n : DATABASE_TYPE == 'mssql'\n ? 'nvarchar'\n : 'text',\n length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined,\n nullable: true\n })\n toolUsages?: string\n\n /** GraphQL 노출용 — JSON 배열로 parse. */\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Tool usage trace — array of {name, arguments, result, kind}. Only on assistant messages.'\n })\n get toolUsagesJson(): any {\n if (!this.toolUsages) return null\n try {\n return JSON.parse(this.toolUsages)\n } catch {\n return null\n }\n }\n\n /**\n * 접지 경고 — 이 답이 언급했으나 근거(프롬프트·보드 문맥·이력·도구 결과)에 없던 식별자 목록.\n *\n * **왜 영속하는가**: 도크의 협의는 여러 사람이 며칠에 걸쳐 읽는다. 경고가 그 자리에서만\n * 보이고 새로 고치면 사라지면, 나중에 읽는 사람은 검증되지 않은 문장을 검증된 것으로 읽는다.\n * 다시 계산할 수도 없다 — 근거였던 도구 결과 원문은 남지 않고 압축본만 남기 때문이다.\n *\n * 형태: string[] 를 JSON-stringify. 비었으면 null(= 접지 정상).\n */\n @Column({ type: 'varchar', length: 1024, nullable: true })\n groundingWarnings?: string\n\n /** GraphQL 노출용 — 식별자 배열로 parse. */\n @Field(() => GraphQLJSON, {\n nullable: true,\n description: 'Ungrounded identifiers mentioned by this reply (hallucination candidates). Null when grounded.'\n })\n get groundingWarningsJson(): any {\n if (!this.groundingWarnings) return null\n try {\n return JSON.parse(this.groundingWarnings)\n } catch {\n return null\n }\n }\n\n /**\n * 메시지를 일으킨 사용자.\n * - role='user' → 보낸 사용자\n * - role='assistant' → 그 응답을 트리거한 사용자 (직전 user 메시지의 작성자)\n * - role='system' → 시스템 액션을 일으킨 사용자 (예: 직접 편집한 사용자)\n *\n * nullable — legacy 메시지 (이 필드 추가 이전에 작성) 는 null. 또한 ad-hoc 모드 / 시스템\n * 자동 메시지 중 사용자 컨텍스트가 없는 경우도 null 가능.\n */\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'User who triggered this message.' })\n creator?: User\n\n @RelationId((message: ChatMessage) => message.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'Last user to update this message (for future edit support).' })\n updater?: User\n\n @RelationId((message: ChatMessage) => message.updater)\n updaterId?: string\n\n /**\n * 부모 메시지 — 이 메시지가 \"응답하는 대상\" / \"이어가는 메시지\" 의 id.\n *\n * 의미 (role 별):\n * - 'user' → 직전 in-session 메시지 (보통 assistant 또는 system). 첫 turn 은 null.\n * - 'assistant' → 이 응답이 답하는 user 메시지. (regenerate 로 같은 user 메시지를 부모로\n * 하는 형제 assistant 메시지가 생기면 자연스럽게 branching tree 가 됨)\n * - 'system' → 직전 in-session 메시지 (timeline 연속성 유지).\n *\n * 이 한 컬럼으로 향후 어떤 흐름이든 표현 가능 — linear chain / branching (regenerate) /\n * tree (replies-to-specific-message) 등. UI/쿼리는 점진 도입.\n *\n * onDelete: 'SET NULL' — 부모 메시지가 삭제돼도 child 는 남되 dangling 마킹.\n */\n @ManyToOne(type => ChatMessage, { nullable: true, onDelete: 'SET NULL' })\n parentMessage?: ChatMessage\n\n @RelationId((message: ChatMessage) => message.parentMessage)\n @Field({\n nullable: true,\n description:\n 'Id of the message this one is in-response-to / continues. Enables linear / branching / tree structures.'\n })\n parentMessageId?: string\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'Creation timestamp.' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'Last update timestamp (relevant once message editing is supported).' })\n updatedAt?: Date\n\n @DeleteDateColumn()\n @Field({ nullable: true, description: 'Soft delete timestamp.' })\n deletedAt?: Date\n}\n"]}
1
+ {"version":3,"file":"chat-message.js","sourceRoot":"","sources":["../../../server/service/chat-message/chat-message.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;GAOG;AACH,qCAUgB;AAChB,+CAAoD;AACpD,qDAA6C;AAE7C,yDAAgD;AAChD,6CAA4C;AAE5C,qEAA6D;AAE7D,MAAM,SAAS,GAAG,YAAM,CAAC,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;AAC7C,MAAM,aAAa,GAAG,SAAS,CAAC,IAAI,CAAA;AAK7B,IAAM,WAAW,GAAjB,MAAM,WAAW;IAyGtB,oCAAoC;IACpC,IAKI,cAAc;QAChB,IAAI,CAAC,IAAI,CAAC,UAAU;YAAE,OAAO,IAAI,CAAA;QACjC,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;QACpC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IAcD,mCAAmC;IACnC,IAII,qBAAqB;QACvB,IAAI,CAAC,IAAI,CAAC,iBAAiB;YAAE,OAAO,IAAI,CAAA;QACxC,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAA;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;CA6DF,CAAA;AA7MY,kCAAW;AAGb;IAFR,IAAA,gCAAsB,EAAC,MAAM,CAAC;IAC9B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;uCAClB;AAGpB;IADC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,6BAAW,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;sCAC3E,6BAAW;4CAAA;AAGrB;IADC,IAAA,oBAAU,EAAC,CAAC,OAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;;8CACpC;AAIlB;IAFC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACvC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,iCAAiC,EAAE,CAAC;;yCAC7C;AAcb;IAZC,IAAA,gBAAM,EAAC;QACN,IAAI,EACF,aAAa,IAAI,OAAO,IAAI,aAAa,IAAI,SAAS;YACpD,CAAC,CAAC,UAAU;YACZ,CAAC,CAAC,aAAa,IAAI,QAAQ;gBACzB,CAAC,CAAC,MAAM;gBACR,CAAC,CAAC,aAAa,IAAI,OAAO;oBACxB,CAAC,CAAC,UAAU;oBACZ,CAAC,CAAC,MAAM;QAChB,MAAM,EAAE,aAAa,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;KACrD,CAAC;IACD,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,iBAAiB,EAAE,CAAC;;4CAC1B;AAehB;IAFC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACvD,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,4GAA4G,EAAE,CAAC;;8CACnI;AAoBlB;IANC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,cAAc,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAChD,IAAA,oBAAK,EAAC,GAAG,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE;QACrB,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,8KAA8K;KACjL,CAAC;;0CACc;AAShB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,qFAAqF,EAAE,CAAC;;6CAC5G;AAIlB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gDAAgD,EAAE,CAAC;;mDAClE;AAIvB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,mDAAmD,EAAE,CAAC;;2DAC7D;AAwB/B;IAZC,IAAA,gBAAM,EAAC;QACN,IAAI,EACF,aAAa,IAAI,OAAO,IAAI,aAAa,IAAI,SAAS;YACpD,CAAC,CAAC,UAAU;YACZ,CAAC,CAAC,aAAa,IAAI,QAAQ;gBACzB,CAAC,CAAC,MAAM;gBACR,CAAC,CAAC,aAAa,IAAI,OAAO;oBACxB,CAAC,CAAC,UAAU;oBACZ,CAAC,CAAC,MAAM;QAChB,MAAM,EAAE,aAAa,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;QACpD,QAAQ,EAAE,IAAI;KACf,CAAC;;+CACiB;AAGnB;IAAC,IAAA,oBAAK,EAAC,GAAG,EAAE,CAAC,6BAAW,EAAE;QACxB,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,0FAA0F;KAC7F,CAAC;;;iDAQD;AAYD;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;sDAChC;AAG1B;IAAC,IAAA,oBAAK,EAAC,GAAG,EAAE,CAAC,6BAAW,EAAE;QACxB,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,gGAAgG;KAC9G,CAAC;;;wDAQD;AAaD;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC3C,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,kCAAkC,EAAE,CAAC;sCAC/E,gBAAI;4CAAA;AAGd;IADC,IAAA,oBAAU,EAAC,CAAC,OAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;;8CACpC;AAIlB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC3C,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,6DAA6D,EAAE,CAAC;sCAC1G,gBAAI;4CAAA;AAGd;IADC,IAAA,oBAAU,EAAC,CAAC,OAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;;8CACpC;AAiBlB;IADC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;sCACzD,WAAW;kDAAA;AAQ3B;IANC,IAAA,oBAAU,EAAC,CAAC,OAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,aAAa,CAAC;IAC3D,IAAA,oBAAK,EAAC;QACL,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,yGAAyG;KAC5G,CAAC;;oDACsB;AAIxB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,qBAAqB,EAAE,CAAC;sCAClD,IAAI;8CAAA;AAIhB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,qEAAqE,EAAE,CAAC;sCAClG,IAAI;8CAAA;AAIhB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,wBAAwB,EAAE,CAAC;sCACrD,IAAI;8CAAA;sBA5ML,WAAW;IAHvB,IAAA,gBAAM,GAAE;IACR,IAAA,eAAK,EAAC,mBAAmB,EAAE,CAAC,OAAoB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IAC1F,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,yCAAyC,EAAE,CAAC;GAC1D,WAAW,CA6MvB","sourcesContent":["/**\n * ChatMessage — ChatSession 의 한 메시지.\n *\n * role: 'user' | 'assistant' | 'system'\n * 'system' 은 사용자 직접 편집/import 진행 등 시스템이 자동 생성하는 컨텍스트 메시지.\n *\n * relatedPatchId / relatedImportSessionId 로 메시지가 트리거한 패치·임포트와 연결.\n */\nimport {\n Column,\n CreateDateColumn,\n DeleteDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\n\nimport { User } from '@things-factory/auth-base'\nimport { config } from '@things-factory/env'\n\nimport { ChatSession } from '../chat-session/chat-session.js'\n\nconst ORMCONFIG = config.get('ormconfig', {})\nconst DATABASE_TYPE = ORMCONFIG.type\n\n@Entity()\n@Index('ix_chat_message_1', (message: ChatMessage) => [message.session, message.createdAt])\n@ObjectType({ description: 'A single chat message in a ChatSession.' })\nexport class ChatMessage {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { nullable: true })\n readonly id?: string\n\n @ManyToOne(type => ChatSession, session => session.messages, { onDelete: 'CASCADE' })\n session?: ChatSession\n\n @RelationId((message: ChatMessage) => message.session)\n sessionId?: string\n\n @Column({ type: 'varchar', length: 16 })\n @Field({ description: \"'user' | 'assistant' | 'system'\" })\n role!: string\n\n @Column({\n type:\n DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'\n ? 'longtext'\n : DATABASE_TYPE == 'oracle'\n ? 'clob'\n : DATABASE_TYPE == 'mssql'\n ? 'nvarchar'\n : 'text',\n length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined\n })\n @Field({ description: 'Message content' })\n content!: string\n\n /**\n * 이 말이 누구에게 한 말인가 — `'assistant'` · `'room'`(ADR-0070 A15 결정 ①).\n *\n * **값은 그 행을 쓴 문이 찍는다.** 어시스턴트에게 묻는 문(`assistantChat`)이 저장한 사람의 행은\n * `assistant` 이고, 사람끼리의 문(`appendSessionTurns`)이 저장한 행은 `room` 이다. 클라이언트가\n * 인자로 보내지 않는다 — 인자로 받으면 「어느 문을 불렀나」와 「무엇을 적어 보냈나」 두 사실이\n * 생기고, 둘은 어긋날 수 있다.\n *\n * 사람의 행에만 뜻이 있다. 어시스턴트의 답은 `parentMessage` 로 자기를 부른 턴에 귀속되고,\n * `system` 행은 누구에게 한 말이 아니다.\n */\n @Column({ type: 'varchar', length: 16, nullable: true })\n @Field({ nullable: true, description: \"Who this turn was for: 'assistant' | 'room'. Written by the door that stored it, never sent by the client.\" })\n addressee?: string\n\n /**\n * 이 행의 값이 지나온 문들 — ADR-0073 판정 1.\n *\n * 어시스턴트의 답과 제안 행에만 채워진다. 사람의 말은 빈 목록이다 — 사람이 자기 입으로 한 말에는\n * 문이 없다. 답을 저장할 때 그 턴에 부른 도구들의 `doors`(ADR-0072)를 합쳐 옮겨 적는다.\n *\n * 읽을 때마다 **읽는 사람**이 이 문을 전부 지나는지 보고, 못 지나면 본문 대신 한 줄이 나간다. 그래서\n * 이 칸은 \"누가 볼 수 있나\"를 적은 것이고, 저장된 값 자체는 누구에게도 달라지지 않는다.\n *\n * 목록이 비면 누구나 읽는다. 등록기가 모르는 도구가 답에 끼면 `unresolved:<도구>` 가 적히고 아무도\n * 못 지난다 — 모르는 값은 가려지는 쪽으로 넘어뜨린다.\n */\n @Column({ type: 'simple-array', nullable: true })\n @Field(() => [String], {\n nullable: true,\n description:\n 'GraphQL doors the tools behind this answer went through. Empty for what people typed. A reader who does not pass all of them gets a one-line notice in place of the content.'\n })\n doors?: string[]\n\n /**\n * 이 행이 **이 사람에게** 가려져 나갔는가 — 저장하지 않는다.\n *\n * 같은 행이 사람마다 다르게 나가므로 칸에 적을 값이 없다. 읽어 나갈 때 채운다. 화면은 이것으로\n * 자물쇠를 그리고, 본문에 온 한 줄을 어시스턴트의 말로 오해하지 않는다.\n */\n @Field({ nullable: true, description: 'True when the content was replaced because this reader does not pass the row doors.' })\n redacted?: boolean\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'PatchEntry.id this message triggered (if any).' })\n relatedPatchId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'ImportSession.id this message triggered (if any).' })\n relatedImportSessionId?: string\n\n /**\n * AI 가 응답을 만드는 동안 호출한 도구 trace — JSON-stringified.\n *\n * 컬럼은 string (PatchEntry.ops 와 동일 패턴), GraphQL 노출은 toolUsagesJson getter\n * 로 parse 된 객체. UI 의 fold-able 박스 (\"AI 가 이런 도구를 사용했습니다\") 가\n * 페이지 reload / loadHistory 후에도 살아있도록 영속.\n *\n * 형태: ToolUsage[] (assistant.ts 의 시간순 trace, summarizeToolResult 로 압축).\n * null 이면 도구 호출 없었던 메시지 (text-only 응답).\n */\n @Column({\n type:\n DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'\n ? 'longtext'\n : DATABASE_TYPE == 'oracle'\n ? 'clob'\n : DATABASE_TYPE == 'mssql'\n ? 'nvarchar'\n : 'text',\n length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined,\n nullable: true\n })\n toolUsages?: string\n\n /** GraphQL 노출용 — JSON 배열로 parse. */\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Tool usage trace — array of {name, arguments, result, kind}. Only on assistant messages.'\n })\n get toolUsagesJson(): any {\n if (!this.toolUsages) return null\n try {\n return JSON.parse(this.toolUsages)\n } catch {\n return null\n }\n }\n\n /**\n * 접지 경고 — 이 답이 언급했으나 근거(프롬프트·보드 문맥·이력·도구 결과)에 없던 식별자 목록.\n *\n * **왜 영속하는가**: 도크의 협의는 여러 사람이 며칠에 걸쳐 읽는다. 경고가 그 자리에서만\n * 보이고 새로 고치면 사라지면, 나중에 읽는 사람은 검증되지 않은 문장을 검증된 것으로 읽는다.\n * 다시 계산할 수도 없다 — 근거였던 도구 결과 원문은 남지 않고 압축본만 남기 때문이다.\n *\n * 형태: string[] 를 JSON-stringify. 비었으면 null(= 접지 정상).\n */\n @Column({ type: 'varchar', length: 1024, nullable: true })\n groundingWarnings?: string\n\n /** GraphQL 노출용 — 식별자 배열로 parse. */\n @Field(() => GraphQLJSON, {\n nullable: true,\n description: 'Ungrounded identifiers mentioned by this reply (hallucination candidates). Null when grounded.'\n })\n get groundingWarningsJson(): any {\n if (!this.groundingWarnings) return null\n try {\n return JSON.parse(this.groundingWarnings)\n } catch {\n return null\n }\n }\n\n /**\n * 메시지를 일으킨 사용자.\n * - role='user' → 보낸 사용자\n * - role='assistant' → 그 응답을 트리거한 사용자 (직전 user 메시지의 작성자)\n * - role='system' → 시스템 액션을 일으킨 사용자 (예: 직접 편집한 사용자)\n *\n * nullable — legacy 메시지 (이 필드 추가 이전에 작성) 는 null. 또한 ad-hoc 모드 / 시스템\n * 자동 메시지 중 사용자 컨텍스트가 없는 경우도 null 가능.\n */\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'User who triggered this message.' })\n creator?: User\n\n @RelationId((message: ChatMessage) => message.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'Last user to update this message (for future edit support).' })\n updater?: User\n\n @RelationId((message: ChatMessage) => message.updater)\n updaterId?: string\n\n /**\n * 부모 메시지 — 이 메시지가 \"응답하는 대상\" / \"이어가는 메시지\" 의 id.\n *\n * 의미 (role 별):\n * - 'user' → 직전 in-session 메시지 (보통 assistant 또는 system). 첫 turn 은 null.\n * - 'assistant' → 이 응답이 답하는 user 메시지. (regenerate 로 같은 user 메시지를 부모로\n * 하는 형제 assistant 메시지가 생기면 자연스럽게 branching tree 가 됨)\n * - 'system' → 직전 in-session 메시지 (timeline 연속성 유지).\n *\n * 이 한 컬럼으로 향후 어떤 흐름이든 표현 가능 — linear chain / branching (regenerate) /\n * tree (replies-to-specific-message) 등. UI/쿼리는 점진 도입.\n *\n * onDelete: 'SET NULL' — 부모 메시지가 삭제돼도 child 는 남되 dangling 마킹.\n */\n @ManyToOne(type => ChatMessage, { nullable: true, onDelete: 'SET NULL' })\n parentMessage?: ChatMessage\n\n @RelationId((message: ChatMessage) => message.parentMessage)\n @Field({\n nullable: true,\n description:\n 'Id of the message this one is in-response-to / continues. Enables linear / branching / tree structures.'\n })\n parentMessageId?: string\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'Creation timestamp.' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'Last update timestamp (relevant once message editing is supported).' })\n updatedAt?: Date\n\n @DeleteDateColumn()\n @Field({ nullable: true, description: 'Soft delete timestamp.' })\n deletedAt?: Date\n}\n"]}
@@ -0,0 +1,27 @@
1
+ import type { FieldGate } from '@things-factory/auth-base';
2
+ /** `privilegeOfField` 와 같은 모양 — 이 파일은 표를 **읽기만** 하고 어디서 오는지는 모른다. */
3
+ export type FieldLookup = (typeName: string, fieldName: string) => {
4
+ known: false;
5
+ } | {
6
+ known: true;
7
+ gate: FieldGate | null;
8
+ };
9
+ export interface DoorCheckDeps {
10
+ /** AUTH 의 필드→권한 표. 운영에서는 `privilegeOfField` 가 들어온다(`reader-gate.ts`). */
11
+ lookup: FieldLookup;
12
+ /** `checkPermission` 을 그대로 싸서 넘긴다 — 판정은 AUTH 가 한다. */
13
+ permits(gate: FieldGate): Promise<boolean>;
14
+ }
15
+ /** `Query.jobOrders` 를 타입과 필드로 가른다. 모양이 아니면 undefined — 아무도 못 지난다. */
16
+ export declare function doorField(door: string): {
17
+ typeName: string;
18
+ fieldName: string;
19
+ } | undefined;
20
+ /** 이 문이 스키마에 있는가 — 없으면 선언이 틀린 것이고, 부팅 보고가 그것을 말한다. */
21
+ export declare function isKnownDoor(door: string, lookup: FieldLookup): boolean;
22
+ /**
23
+ * 문 하나를 이 사람이 지나는가 — `doorGate` 에 넣을 검사.
24
+ *
25
+ * 되풀이 조회를 줄이는 기억은 `doorGate` 가 한다. 여기는 한 번의 판정만 한다.
26
+ */
27
+ export declare function doorCheck(deps: DoorCheckDeps): (door: string) => Promise<boolean>;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.doorField = doorField;
4
+ exports.isKnownDoor = isKnownDoor;
5
+ exports.doorCheck = doorCheck;
6
+ /** `Query.jobOrders` 를 타입과 필드로 가른다. 모양이 아니면 undefined — 아무도 못 지난다. */
7
+ function doorField(door) {
8
+ const match = /^(Query|Mutation|Subscription)\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(door ?? '');
9
+ return match ? { typeName: match[1], fieldName: match[2] } : undefined;
10
+ }
11
+ /** 이 문이 스키마에 있는가 — 없으면 선언이 틀린 것이고, 부팅 보고가 그것을 말한다. */
12
+ function isKnownDoor(door, lookup) {
13
+ const field = doorField(door);
14
+ return !!field && lookup(field.typeName, field.fieldName).known;
15
+ }
16
+ /**
17
+ * 문 하나를 이 사람이 지나는가 — `doorGate` 에 넣을 검사.
18
+ *
19
+ * 되풀이 조회를 줄이는 기억은 `doorGate` 가 한다. 여기는 한 번의 판정만 한다.
20
+ */
21
+ function doorCheck(deps) {
22
+ return async (door) => {
23
+ const field = doorField(door);
24
+ if (!field)
25
+ return false;
26
+ const asked = deps.lookup(field.typeName, field.fieldName);
27
+ if (!asked.known)
28
+ return false;
29
+ if (asked.gate === null)
30
+ return true;
31
+ return await deps.permits(asked.gate);
32
+ };
33
+ }
34
+ //# sourceMappingURL=door-access.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"door-access.js","sourceRoot":"","sources":["../../../server/service/chat-message/door-access.ts"],"names":[],"mappings":";;AAqCA,8BAGC;AAGD,kCAGC;AAOD,8BAWC;AA5BD,sEAAsE;AACtE,SAAgB,SAAS,CAAC,IAAY;IACpC,MAAM,KAAK,GAAG,2DAA2D,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAA;IAC1F,OAAO,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;AACxE,CAAC;AAED,sDAAsD;AACtD,SAAgB,WAAW,CAAC,IAAY,EAAE,MAAmB;IAC3D,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;IAC7B,OAAO,CAAC,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,KAAK,CAAA;AACjE,CAAC;AAED;;;;GAIG;AACH,SAAgB,SAAS,CAAC,IAAmB;IAC3C,OAAO,KAAK,EAAE,IAAY,EAAE,EAAE;QAC5B,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;QAC7B,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAA;QAExB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,CAAA;QAC1D,IAAI,CAAC,KAAK,CAAC,KAAK;YAAE,OAAO,KAAK,CAAA;QAC9B,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI;YAAE,OAAO,IAAI,CAAA;QAEpC,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACvC,CAAC,CAAA;AACH,CAAC","sourcesContent":["/*\n * 문 → 그 문을 지키는 권한 — ADR-0073 판정 1 의 관문을 세우는 자리.\n *\n * ── 정본은 AUTH 의 표다 ───────────────────────────────────────────────────\n * 행에 적힌 것은 문 이름(`Query.jobOrders`)이고, 사람이 가진 것은 권한(`job-order:query`)이다. 둘을\n * 잇는 것은 `privilegeOfField(typeName, fieldName)` 이다(ADR-0072 결정 5). 스키마를 지을 때 모든 루트\n * 필드가 기록되므로, 이 표는 **그 문이 실제로 무엇을 묻는지**를 그대로 답한다.\n *\n * 그 전에는 도구 명세가 들고 있는 짝을 베껴 썼다. 베낀 값은 문이 선언을 바꾸면 조용히 어긋나고,\n * 어긋나면 느슨한 쪽이 이긴다. 표가 서면서 그 사본을 걷었다.\n *\n * ── 판정하지 않고 AUTH 에게 묻는다 ────────────────────────────────────────\n * 표가 주는 gate 는 `checkPermission` 이 받는 넷(category · privilege · owner · super)과 같은 모양이고,\n * 그대로 `checkPermission` 에 넘긴다. 여기서 「category 가 없으면 소유권만 본다」 같은 규칙을 다시 쓰면\n * 그 순간 AUTH 와 이 파일이 두 벌이 된다. 문에서 통하는 것이 여기서도 통해야 한다.\n *\n * ── 셋 중 둘은 닫는다 ─────────────────────────────────────────────────────\n * `{ known: false }` 그런 루트 필드가 없다. 도구가 문 이름을 틀리게 적은 것이다 —\n * **열림으로 읽지 않는다.** 부팅 보고가 따로 말한다.\n * `{ known: true, gate: null }` 필드는 있고 선언이 없다. 그 문은 열려 있으므로 이 행도 열린다.\n * `{ known: true, gate }` 그 문이 묻는 것을 이 사람이 가졌는지 AUTH 에게 묻는다.\n *\n * 스키마를 짓기 전에는 표가 비어 있어 모든 문이 `known: false` 로 답한다 — 가려지는 쪽이다.\n */\nimport type { FieldGate } from '@things-factory/auth-base'\n\n/** `privilegeOfField` 와 같은 모양 — 이 파일은 표를 **읽기만** 하고 어디서 오는지는 모른다. */\nexport type FieldLookup = (typeName: string, fieldName: string) => { known: false } | { known: true; gate: FieldGate | null }\n\nexport interface DoorCheckDeps {\n /** AUTH 의 필드→권한 표. 운영에서는 `privilegeOfField` 가 들어온다(`reader-gate.ts`). */\n lookup: FieldLookup\n /** `checkPermission` 을 그대로 싸서 넘긴다 — 판정은 AUTH 가 한다. */\n permits(gate: FieldGate): Promise<boolean>\n}\n\n/** `Query.jobOrders` 를 타입과 필드로 가른다. 모양이 아니면 undefined — 아무도 못 지난다. */\nexport function doorField(door: string): { typeName: string; fieldName: string } | undefined {\n const match = /^(Query|Mutation|Subscription)\\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(door ?? '')\n return match ? { typeName: match[1], fieldName: match[2] } : undefined\n}\n\n/** 이 문이 스키마에 있는가 — 없으면 선언이 틀린 것이고, 부팅 보고가 그것을 말한다. */\nexport function isKnownDoor(door: string, lookup: FieldLookup): boolean {\n const field = doorField(door)\n return !!field && lookup(field.typeName, field.fieldName).known\n}\n\n/**\n * 문 하나를 이 사람이 지나는가 — `doorGate` 에 넣을 검사.\n *\n * 되풀이 조회를 줄이는 기억은 `doorGate` 가 한다. 여기는 한 번의 판정만 한다.\n */\nexport function doorCheck(deps: DoorCheckDeps): (door: string) => Promise<boolean> {\n return async (door: string) => {\n const field = doorField(door)\n if (!field) return false\n\n const asked = deps.lookup(field.typeName, field.fieldName)\n if (!asked.known) return false\n if (asked.gate === null) return true\n\n return await deps.permits(asked.gate)\n }\n}\n"]}
@@ -0,0 +1,30 @@
1
+ import type { AIClient } from '@things-factory/ai-client-base';
2
+ /** How many newly-dropped turns are worth another summarisation call. */
3
+ export declare const DEFAULT_SUMMARY_THRESHOLD = 10;
4
+ export interface FoldSessionHistoryInput {
5
+ sessionId: string;
6
+ domainId: string;
7
+ /** The same client that answered — a summary is written by whatever this deployment has. */
8
+ client: Pick<AIClient, 'chat'>;
9
+ /** The cap the door uses when it builds the history, so both sides agree on what fell off. */
10
+ maxTurns: number;
11
+ threshold?: number;
12
+ locale?: string;
13
+ /** Test seam: the two repositories this reads and writes. */
14
+ repos?: {
15
+ sessions: any;
16
+ messages: any;
17
+ };
18
+ }
19
+ /**
20
+ * Where the rows after the summary's watermark are.
21
+ *
22
+ * Kept separate so the window can be read without a database: the array is an OR, and the second
23
+ * branch continues within one timestamp by id — the same axis the ordering uses, or a burst of
24
+ * messages in the same millisecond would be summarised twice or not at all.
25
+ */
26
+ export declare function rowsAfterWatermark(sessionId: string, mark: {
27
+ id?: string;
28
+ createdAt?: Date;
29
+ } | undefined): any;
30
+ export declare function foldSessionHistory(input: FoldSessionHistoryInput): Promise<void>;