@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,133 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DEFAULT_SUMMARY_THRESHOLD = void 0;
37
+ exports.rowsAfterWatermark = rowsAfterWatermark;
38
+ exports.foldSessionHistory = foldSessionHistory;
39
+ /*
40
+ * Folding a long conversation's earlier turns into a summary — for every surface, not only boards.
41
+ *
42
+ * ── The gap this closes (2026-09-18) ───────────────────────────────────────────
43
+ * The pieces were all here: a cap on how many turns reach the model, a notice when turns are left
44
+ * out, `ChatSession.lastSummary` to carry what was dropped, and pure rules for when to re-summarise
45
+ * (`history-summary`, `fold-history`). Only the board product ever called them. The neutral door —
46
+ * the one the twin, plant and figure conversations run through — never did, so those sessions kept
47
+ * announcing "the first N messages were left out" and never summarised them.
48
+ *
49
+ * That is worst exactly where it was designed to help: a twin deliberation runs for days, and what
50
+ * the room agreed on in the first hour is what falls off the front.
51
+ *
52
+ * ── What is here and what is not ───────────────────────────────────────────────
53
+ * The rules stay pure and tested elsewhere. This is the reading and the writing: find what the
54
+ * stored summary does not cover yet, ask the model for a new one, store it. It is called after the
55
+ * reply has gone out, and it never throws — a conversation must not fail because a summary did not
56
+ * get written, and the next turn says "N left out" honestly if it did not.
57
+ */
58
+ const typeorm_1 = require("typeorm");
59
+ const fold_history_js_1 = require("./fold-history.js");
60
+ const history_summary_js_1 = require("./history-summary.js");
61
+ /*
62
+ * 엔티티는 **늦게 가져온다.** 위에서 import 하면 이 파일을 읽는 순간 TypeGraphQL 데코레이터가 돌고,
63
+ * 데코레이터 메타데이터가 없는 자리(시험 러너)에서 모듈 적재가 깨진다. 규칙 자체는 데이터베이스가
64
+ * 없어도 검사할 수 있어야 한다.
65
+ */
66
+ async function defaultRepos() {
67
+ const { getRepository } = await Promise.resolve().then(() => __importStar(require('@things-factory/shell')));
68
+ const { ChatSession } = await Promise.resolve().then(() => __importStar(require('../chat-session/chat-session.js')));
69
+ const { ChatMessage } = await Promise.resolve().then(() => __importStar(require('./chat-message.js')));
70
+ return { sessions: getRepository(ChatSession), messages: getRepository(ChatMessage) };
71
+ }
72
+ /** How many newly-dropped turns are worth another summarisation call. */
73
+ exports.DEFAULT_SUMMARY_THRESHOLD = 10;
74
+ /**
75
+ * Where the rows after the summary's watermark are.
76
+ *
77
+ * Kept separate so the window can be read without a database: the array is an OR, and the second
78
+ * branch continues within one timestamp by id — the same axis the ordering uses, or a burst of
79
+ * messages in the same millisecond would be summarised twice or not at all.
80
+ */
81
+ function rowsAfterWatermark(sessionId, mark) {
82
+ const session = { id: sessionId };
83
+ if (!mark?.createdAt)
84
+ return { session };
85
+ return [
86
+ { session, createdAt: (0, typeorm_1.MoreThan)(mark.createdAt) },
87
+ { session, createdAt: mark.createdAt, id: (0, typeorm_1.MoreThan)(mark.id) }
88
+ ];
89
+ }
90
+ async function foldSessionHistory(input) {
91
+ try {
92
+ const { sessions, messages } = input.repos ?? (await defaultRepos());
93
+ const session = await sessions.findOneBy({ id: input.sessionId, domain: { id: input.domainId } });
94
+ if (!session)
95
+ return;
96
+ const stored = { text: session.lastSummary, upToMessageId: session.summaryUpToMessageId };
97
+ const order = { createdAt: 'ASC', id: 'ASC' };
98
+ const relations = { creator: true };
99
+ /* 요약이 덮은 지점을 알면 그 뒤만 읽는다 — 매 턴 전체 대화를 읽지 않는다. */
100
+ let mark;
101
+ if (stored.text && stored.upToMessageId) {
102
+ mark = await messages.findOne({ where: { session: { id: input.sessionId }, id: stored.upToMessageId } });
103
+ }
104
+ /* 워터마크 행이 사라졌으면 덮은 범위를 모른다 — 전부 다시 본다. */
105
+ const startsAfterSummary = Boolean(stored.text && mark?.createdAt);
106
+ const rows = await messages.find({ where: rowsAfterWatermark(input.sessionId, startsAfterSummary ? mark : undefined), relations, order });
107
+ const { patch } = await (0, fold_history_js_1.foldHistorySummary)({
108
+ rows: rows.map((row) => ({
109
+ id: row.id,
110
+ role: row.role,
111
+ content: row.content ?? '',
112
+ senderId: row.creator?.id,
113
+ senderName: row.creator?.name || row.creator?.username
114
+ })),
115
+ rowsStartAfterSummary: startsAfterSummary,
116
+ stored,
117
+ maxTurns: input.maxTurns,
118
+ threshold: input.threshold ?? exports.DEFAULT_SUMMARY_THRESHOLD,
119
+ summarize: content => input.client.chat([{ role: 'user', content }], {
120
+ systemPrompt: (0, history_summary_js_1.summaryInstruction)(input.locale),
121
+ maxTokens: 700,
122
+ /* 요약은 창작이 아니다 — 흔들림을 줄인다. */
123
+ temperature: 0
124
+ })
125
+ });
126
+ if (patch)
127
+ await sessions.update(input.sessionId, patch);
128
+ }
129
+ catch {
130
+ /* 요약 실패는 대화를 막지 않는다 — 다음 턴이 「N 개 생략」으로 정직하게 알린다. */
131
+ }
132
+ }
133
+ //# sourceMappingURL=fold-session-history.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fold-session-history.js","sourceRoot":"","sources":["../../../server/service/chat-message/fold-session-history.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Folding a long conversation's earlier turns into a summary — for every surface, not only boards.\n *\n * ── The gap this closes (2026-09-18) ───────────────────────────────────────────\n * The pieces were all here: a cap on how many turns reach the model, a notice when turns are left\n * out, `ChatSession.lastSummary` to carry what was dropped, and pure rules for when to re-summarise\n * (`history-summary`, `fold-history`). Only the board product ever called them. The neutral door —\n * the one the twin, plant and figure conversations run through — never did, so those sessions kept\n * announcing \"the first N messages were left out\" and never summarised them.\n *\n * That is worst exactly where it was designed to help: a twin deliberation runs for days, and what\n * the room agreed on in the first hour is what falls off the front.\n *\n * ── What is here and what is not ───────────────────────────────────────────────\n * The rules stay pure and tested elsewhere. This is the reading and the writing: find what the\n * stored summary does not cover yet, ask the model for a new one, store it. It is called after the\n * reply has gone out, and it never throws — a conversation must not fail because a summary did not\n * get written, and the next turn says \"N left out\" honestly if it did not.\n */\nimport { MoreThan } from 'typeorm'\n\nimport type { AIClient } from '@things-factory/ai-client-base'\n\nimport { foldHistorySummary } from './fold-history.js'\nimport { summaryInstruction } from './history-summary.js'\n\n/*\n * 엔티티는 **늦게 가져온다.** 위에서 import 하면 이 파일을 읽는 순간 TypeGraphQL 데코레이터가 돌고,\n * 데코레이터 메타데이터가 없는 자리(시험 러너)에서 모듈 적재가 깨진다. 규칙 자체는 데이터베이스가\n * 없어도 검사할 수 있어야 한다.\n */\nasync function defaultRepos() {\n const { getRepository } = await import('@things-factory/shell')\n const { ChatSession } = await import('../chat-session/chat-session.js')\n const { ChatMessage } = await import('./chat-message.js')\n return { sessions: getRepository(ChatSession as any), messages: getRepository(ChatMessage as any) }\n}\n\n/** How many newly-dropped turns are worth another summarisation call. */\nexport const DEFAULT_SUMMARY_THRESHOLD = 10\n\nexport interface FoldSessionHistoryInput {\n sessionId: string\n domainId: string\n /** The same client that answered — a summary is written by whatever this deployment has. */\n client: Pick<AIClient, 'chat'>\n /** The cap the door uses when it builds the history, so both sides agree on what fell off. */\n maxTurns: number\n threshold?: number\n locale?: string\n /** Test seam: the two repositories this reads and writes. */\n repos?: { sessions: any; messages: any }\n}\n\n/**\n * Where the rows after the summary's watermark are.\n *\n * Kept separate so the window can be read without a database: the array is an OR, and the second\n * branch continues within one timestamp by id — the same axis the ordering uses, or a burst of\n * messages in the same millisecond would be summarised twice or not at all.\n */\nexport function rowsAfterWatermark(sessionId: string, mark: { id?: string; createdAt?: Date } | undefined): any {\n const session = { id: sessionId } as any\n if (!mark?.createdAt) return { session }\n return [\n { session, createdAt: MoreThan(mark.createdAt) },\n { session, createdAt: mark.createdAt, id: MoreThan(mark.id) }\n ]\n}\n\nexport async function foldSessionHistory(input: FoldSessionHistoryInput): Promise<void> {\n try {\n const { sessions, messages } = input.repos ?? (await defaultRepos())\n const session = await sessions.findOneBy({ id: input.sessionId, domain: { id: input.domainId } as any })\n if (!session) return\n\n const stored = { text: (session as any).lastSummary, upToMessageId: (session as any).summaryUpToMessageId }\n const order = { createdAt: 'ASC', id: 'ASC' } as any\n const relations = { creator: true } as any\n\n /* 요약이 덮은 지점을 알면 그 뒤만 읽는다 — 매 턴 전체 대화를 읽지 않는다. */\n let mark: { id?: string; createdAt?: Date } | undefined\n if (stored.text && stored.upToMessageId) {\n mark = await messages.findOne({ where: { session: { id: input.sessionId } as any, id: stored.upToMessageId } })\n }\n /* 워터마크 행이 사라졌으면 덮은 범위를 모른다 — 전부 다시 본다. */\n const startsAfterSummary = Boolean(stored.text && mark?.createdAt)\n const rows = await messages.find({ where: rowsAfterWatermark(input.sessionId, startsAfterSummary ? mark : undefined), relations, order })\n\n const { patch } = await foldHistorySummary({\n rows: rows.map((row: any) => ({\n id: row.id,\n role: row.role,\n content: row.content ?? '',\n senderId: row.creator?.id,\n senderName: row.creator?.name || row.creator?.username\n })),\n rowsStartAfterSummary: startsAfterSummary,\n stored,\n maxTurns: input.maxTurns,\n threshold: input.threshold ?? DEFAULT_SUMMARY_THRESHOLD,\n summarize: content =>\n input.client.chat!([{ role: 'user', content }], {\n systemPrompt: summaryInstruction(input.locale),\n maxTokens: 700,\n /* 요약은 창작이 아니다 — 흔들림을 줄인다. */\n temperature: 0\n })\n })\n\n if (patch) await sessions.update(input.sessionId, patch as any)\n } catch {\n /* 요약 실패는 대화를 막지 않는다 — 다음 턴이 「N 개 생략」으로 정직하게 알린다. */\n }\n}\n"]}
@@ -10,6 +10,13 @@ export interface HistoryRow {
10
10
  senderId?: string;
11
11
  /** 프롬프트에 노출할 표시 이름. 이메일 같은 개인정보는 넣지 않는다. */
12
12
  senderName?: string;
13
+ /**
14
+ * 누구에게 한 말인가 — `'assistant'` · `'room'`(ADR-0070 A15). 문이 저장한 값 그대로.
15
+ *
16
+ * 없는 행(이 칸이 생기기 전에 저장된 것)은 **모른다.** 모르는 것을 `room` 으로 적으면 그것이
17
+ * 지어낸 사실이다 — 그때는 대상 없이 발신자만 적는다.
18
+ */
19
+ addressee?: string;
13
20
  }
14
21
  export interface BuildLlmHistoryOptions {
15
22
  /** 이 메시지까지만 유지하고 이후는 버린다(편집·재생성으로 접은 구간). 미지정이면 전부 사용. */
@@ -2,12 +2,17 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildLlmHistory = buildLlmHistory;
4
4
  const history_summary_js_1 = require("./history-summary.js");
5
- /** 사람이 아닌 기록(캔버스 직접 편집 등)의 표기. */
6
- const SYSTEM_LABEL = '시스템';
7
- /** 이름을 모르는 참여자의 표기 — 빈 대괄호를 남기지 않는다. */
8
- const UNKNOWN_LABEL = '참여자';
9
- /** 상한으로 잘라낸 앞부분을 알리는 표기 — 모델이 남은 첫 줄을 대화의 시작으로 오해하지 않게. */
10
- const OMITTED_LABEL = '시스템';
5
+ /*
6
+ * These labels and notices are read by the model, so they are English. The reply the model writes is in the
7
+ * language the person used; that is the model's doing, not this file's. They were Korean, which made every
8
+ * conversation in this house speak Korean to the model whatever the reader's language was.
9
+ */
10
+ /** Marks a record no person wrote (a direct canvas edit, for one). */
11
+ const SYSTEM_LABEL = 'system';
12
+ /** Marks a participant whose name we do not have — better than empty brackets. */
13
+ const UNKNOWN_LABEL = 'participant';
14
+ /** Marks the notice about messages the turn limit dropped, so the model does not read the first kept line as the start. */
15
+ const OMITTED_LABEL = 'system';
11
16
  /**
12
17
  * 저장된 이력 → LLM 대화.
13
18
  *
@@ -30,19 +35,17 @@ function buildLlmHistory(rows, options = {}) {
30
35
  /* 요약이 있으면 그것을 싣는다. **요약임을 밝힌다** — 모델이 원문처럼 인용하지 않게.
31
36
  * 요약이 밀려난 구간을 다 덮지 못하면 남은 개수도 함께 알린다(최신이 아님을 숨기지 않는다). */
32
37
  const uncovered = (0, history_summary_js_1.pendingForSummary)(dropped, options.summary).length;
33
- const tail = uncovered
34
- ? ` 그 뒤 ${uncovered}개 메시지는 요약에 아직 반영되지 않았습니다.`
35
- : '';
38
+ const tail = uncovered ? ` ${uncovered} message(s) after it are not in the summary yet.` : '';
36
39
  out.push({
37
40
  role: 'user',
38
- content: `[${OMITTED_LABEL}] 이 대화의 앞부분 ${dropped.length}개 메시지는 길이 제한으로 아래 요약으로 대체되었습니다(원문 아님 — 요약에 없는 사실은 사용자에게 확인하세요).${tail}\n${summaryText}`
41
+ content: `[${OMITTED_LABEL}] The first ${dropped.length} message(s) of this conversation were replaced by the summary below because of the length limit. It is a summary, not the original — ask the person about anything it does not say.${tail}\n${summaryText}`
39
42
  });
40
43
  }
41
44
  else {
42
45
  /* 요약이 아직 없으면 생략 사실만 알린다 — 개수만 밝히고 내용은 지어내지 않는다. */
43
46
  out.push({
44
47
  role: 'user',
45
- content: `[${OMITTED_LABEL}] 이 대화의 앞부분 ${omitted}개 메시지는 길이 제한으로 생략되었습니다. 필요하면 사용자에게 다시 확인하세요.`
48
+ content: `[${OMITTED_LABEL}] The first ${omitted} message(s) of this conversation were left out because of the length limit. Ask the person again if you need them.`
46
49
  });
47
50
  }
48
51
  }
@@ -58,6 +61,15 @@ function buildLlmHistory(rows, options = {}) {
58
61
  out.push({ role: 'user', content: `[${SYSTEM_LABEL}] ${content}` });
59
62
  continue;
60
63
  }
64
+ /*
65
+ * 사람의 말은 **누구에게 한 말인지까지** 적는다(ADR-0070 A15 결정 ②). 대상이 있을 때만 적으면
66
+ * 모델은 빈 자리에서 추론한다 — 옆 사람이 방에 한 말을 자기에게 온 지시로 읽는 것이 그 결과다.
67
+ */
68
+ const addressee = (row.addressee ?? '').trim();
69
+ if (addressee) {
70
+ out.push({ role: 'user', content: `[${speaker(row)} → ${addressee}] ${content}` });
71
+ continue;
72
+ }
61
73
  out.push({ role: 'user', content: label ? `[${speaker(row)}] ${content}` : content });
62
74
  }
63
75
  return out;
@@ -1 +1 @@
1
- {"version":3,"file":"llm-history.js","sourceRoot":"","sources":["../../../server/service/chat-message/llm-history.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 에이전트에게 넘길 대화 이력 조립 — 순수 로직(리졸버와 분리해 단위 테스트 가능).\n *\n * ── 왜 서버 정본인가 ─────────────────────────────────────────────────────────\n * 예전에는 클라이언트가 화면에 들고 있던 줄들을 그대로 LLM 에 넘겼다. 협의(여러 참여자)에서는\n * 참여자마다 화면이 달라 **에이전트가 사람마다 다른 대화를 본다**. 서버는 ChatMessage 에 정본을\n * 갖고 있으므로 그것을 쓴다. 세션이 없는 즉석 대화(ad-hoc)만 클라이언트 이력을 쓴다.\n *\n * ── 발신자 표기 ─────────────────────────────────────────────────────────────\n * LLMMessage 는 `{ role, content }` 뿐이다 — 발신자 칸이 없다(Anthropic·Gemini 는 message.name 을\n * 지원하지 않아 이식성이 없다). 그래서 발신자는 **본문 맨 앞 표기**로만 전달할 수 있다.\n *\n * [김철수] 승인합니다\n *\n * **이 표기는 추론용 문맥이며 권한 판정 근거가 아니다.** 사용자가 본문에 같은 형식을 직접 적을 수\n * 있으므로, 되돌릴 수 없는 조치의 승인 여부는 반드시 서버 게이트(요청자 신원 기반)가 판정한다.\n * 표기는 \"누가 무슨 맥락에서 말했는지\"를 모델이 이해하게 하는 용도로만 쓴다.\n *\n * 발신자가 **한 명뿐이면 표기하지 않는다** — 혼자 쓰는 대화(대다수)에 노이즈와 토큰을 늘리지 않고,\n * 기존 동작과 같은 프롬프트를 유지한다.\n *\n * ── system 행 ───────────────────────────────────────────────────────────────\n * 캔버스 직접 편집 기록(`User directly edited the board: …`)은 원래 에이전트에게 알리려고 남긴 것인데,\n * 클라이언트가 role='system' 을 걸러내 사장돼 있었다. LLMMessage 에 system 역할이 없으므로\n * `[시스템]` 표기를 붙인 user 메시지로 넘긴다 — 사람이 아니므로 발신자 수 계산에는 넣지 않는다.\n *\n * ── 편집·재생성으로 접은 구간 ───────────────────────────────────────────────\n * 클라이언트의 메시지 편집은 화면에서만 접고 **서버 이력은 유지**한다(board-ai-chat.editUserMessage).\n * 서버 정본으로 바꾸면 접은 말이 에이전트 문맥에 되살아난다. 그래서 호출자가 `truncateAfterMessageId`\n * (마지막으로 남긴 메시지 id)를 주면 그 뒤 행을 버린다. 단 `keepMessageIds` 로 지정한 행(방금 저장한\n * 새 사용자 메시지)은 시간상 뒤에 있어도 항상 남긴다.\n *\n * 알려진 한계(현행 동작과 동일): 공유 세션에서 접은 구간에 **다른 사람의 발언**이 있으면 그것도\n * 함께 제외된다. 오늘 클라이언트가 보내는 이력도 같으므로 이 단계에서는 동등성을 유지하고,\n * \"내 발언만 접기\"는 별도 개선으로 다룬다.\n */\nimport type { LLMMessage } from '../types.js'\nimport { pendingForSummary, type StoredSummary } from './history-summary.js'\n\n/** 이력 한 줄 — 저장된 ChatMessage 에서 필요한 것만(엔티티·typeorm 비의존). */\nexport interface HistoryRow {\n id?: string\n role: string\n /** 멘션 마커(#token{refid:N})는 호출자가 미리 제거해 넘긴다 — 모델 주의 분산·토큰 낭비 방지. */\n content: string\n /** 발신자 식별자(서버 내부용 — 발신자 수 계산에만 쓰고 프롬프트에 넣지 않는다). */\n senderId?: string\n /** 프롬프트에 노출할 표시 이름. 이메일 같은 개인정보는 넣지 않는다. */\n senderName?: string\n}\n\nexport interface BuildLlmHistoryOptions {\n /** 이 메시지까지만 유지하고 이후는 버린다(편집·재생성으로 접은 구간). 미지정이면 전부 사용. */\n truncateAfterMessageId?: string\n /** 잘라내기와 무관하게 항상 유지할 메시지 id(방금 저장한 새 사용자 메시지 등). */\n keepMessageIds?: string[]\n /** 발신자 표기 강제 — 미지정이면 사람 발신자가 둘 이상일 때만 표기. 테스트·특수 호출용. */\n labelSenders?: boolean\n /**\n * 모델에 넘길 **최근 메시지 수 상한**. 초과분(오래된 앞부분)은 버리고 생략 표시를 남긴다.\n *\n * 왜 필요한가: 협의는 며칠에 걸친다. 상한이 없으면 매 턴 전체 이력을 보내 프롬프트가 무한히\n * 자라고(비용·지연·문맥 한계) 오래된 말이 최근 상황을 덮는다. 미지정이면 전부 보낸다(기존 동작).\n *\n * **생략은 반드시 알린다** — 조용히 버리면 모델은 남은 첫 줄을 대화의 시작으로 오해한다.\n * 이건 상한일 뿐 요약이 아니다. 버린 내용을 요약해 실어 보내는 것은 다음 단계다(ChatSession.lastSummary).\n */\n maxTurns?: number\n /**\n * 상한으로 밀려난 앞부분을 대신할 **요약**(세션에 저장된 것).\n *\n * 있으면 \"N개 생략\" 대신 요약을 첫 줄로 싣는다 — 며칠 이어지는 협의에서 앞에서 합의한 것이\n * 없던 일이 되지 않게. 요약이 밀려난 구간을 **전부 덮지 못하면** 덮지 못한 개수도 함께 밝힌다\n * (요약이 최신이 아닐 수 있다는 사실을 숨기지 않는다).\n */\n summary?: StoredSummary\n}\n\n/** 사람이 아닌 기록(캔버스 직접 편집 등)의 표기. */\nconst SYSTEM_LABEL = '시스템'\n/** 이름을 모르는 참여자의 표기 — 빈 대괄호를 남기지 않는다. */\nconst UNKNOWN_LABEL = '참여자'\n/** 상한으로 잘라낸 앞부분을 알리는 표기 — 모델이 남은 첫 줄을 대화의 시작으로 오해하지 않게. */\nconst OMITTED_LABEL = '시스템'\n\n/**\n * 저장된 이력 → LLM 대화.\n *\n * 입력은 **시간 오름차순**을 가정한다(호출자가 createdAt ASC 로 조회). 정렬을 다시 하지 않는다 —\n * 같은 시각의 순서까지 보장하려면 조회 쪽에서 결정해야 하고, 여기서 추측하면 두 곳이 어긋난다.\n */\nexport function buildLlmHistory(rows: HistoryRow[], options: BuildLlmHistoryOptions = {}): LLMMessage[] {\n const keep = new Set(options.keepMessageIds ?? [])\n const truncated = truncate(rows, options.truncateAfterMessageId, keep)\n /* 상한 — 최근 것부터 남긴다. 오래된 말보다 최근 상황이 답을 좌우한다. */\n const limit = options.maxTurns && options.maxTurns > 0 ? options.maxTurns : 0\n const omitted = limit && truncated.length > limit ? truncated.length - limit : 0\n const kept = omitted ? truncated.slice(-limit) : truncated\n\n const label =\n options.labelSenders ?? new Set(kept.filter(r => isHuman(r)).map(r => r.senderId ?? '')).size > 1\n\n const out: LLMMessage[] = []\n if (omitted) {\n const dropped = truncated.slice(0, truncated.length - kept.length)\n const summaryText = (options.summary?.text ?? '').trim()\n if (summaryText) {\n /* 요약이 있으면 그것을 싣는다. **요약임을 밝힌다** — 모델이 원문처럼 인용하지 않게.\n * 요약이 밀려난 구간을 다 덮지 못하면 남은 개수도 함께 알린다(최신이 아님을 숨기지 않는다). */\n const uncovered = pendingForSummary(dropped, options.summary).length\n const tail = uncovered\n ? ` 그 뒤 ${uncovered}개 메시지는 요약에 아직 반영되지 않았습니다.`\n : ''\n out.push({\n role: 'user',\n content: `[${OMITTED_LABEL}] 이 대화의 앞부분 ${dropped.length}개 메시지는 길이 제한으로 아래 요약으로 대체되었습니다(원문 아님 — 요약에 없는 사실은 사용자에게 확인하세요).${tail}\\n${summaryText}`\n })\n } else {\n /* 요약이 아직 없으면 생략 사실만 알린다 — 개수만 밝히고 내용은 지어내지 않는다. */\n out.push({\n role: 'user',\n content: `[${OMITTED_LABEL}] 이 대화의 앞부분 ${omitted}개 메시지는 길이 제한으로 생략되었습니다. 필요하면 사용자에게 다시 확인하세요.`\n })\n }\n }\n for (const row of kept) {\n const content = (row.content ?? '').trim()\n if (!content) continue // 빈 본문(pending 자리 등)은 모델에 넘길 것이 없다\n if (row.role === 'assistant') {\n out.push({ role: 'assistant', content })\n continue\n }\n if (row.role === 'system') {\n out.push({ role: 'user', content: `[${SYSTEM_LABEL}] ${content}` })\n continue\n }\n out.push({ role: 'user', content: label ? `[${speaker(row)}] ${content}` : content })\n }\n return out\n}\n\n/** 사람이 보낸 줄인가 — 발신자 수 계산 대상. system 기록은 사람이 아니다. */\nfunction isHuman(row: HistoryRow): boolean {\n return row.role !== 'assistant' && row.role !== 'system'\n}\n\nfunction speaker(row: HistoryRow): string {\n const name = (row.senderName ?? '').trim()\n return name || UNKNOWN_LABEL\n}\n\n/**\n * 접은 구간 제거. `truncateAfterMessageId` 가 없거나 이력에서 못 찾으면 **전부 유지**한다\n * (조용히 다 버리는 것보다 다 보내는 쪽이 안전하다 — 문맥 상실은 답을 망친다).\n */\nfunction truncate(rows: HistoryRow[], afterId: string | undefined, keep: Set<string>): HistoryRow[] {\n if (!afterId) return rows\n const idx = rows.findIndex(r => r.id === afterId)\n if (idx < 0) return rows\n const head = rows.slice(0, idx + 1)\n const tail = rows.slice(idx + 1).filter(r => r.id && keep.has(r.id))\n return [...head, ...tail]\n}\n"]}
1
+ {"version":3,"file":"llm-history.js","sourceRoot":"","sources":["../../../server/service/chat-message/llm-history.ts"],"names":[],"mappings":";;AAuGA,0CAuDC;AAzHD,6DAA4E;AAgD5E;;;;GAIG;AACH,sEAAsE;AACtE,MAAM,YAAY,GAAG,QAAQ,CAAA;AAC7B,kFAAkF;AAClF,MAAM,aAAa,GAAG,aAAa,CAAA;AACnC,2HAA2H;AAC3H,MAAM,aAAa,GAAG,QAAQ,CAAA;AAE9B;;;;;GAKG;AACH,SAAgB,eAAe,CAAC,IAAkB,EAAE,UAAkC,EAAE;IACtF,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,cAAc,IAAI,EAAE,CAAC,CAAA;IAClD,MAAM,SAAS,GAAG,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,sBAAsB,EAAE,IAAI,CAAC,CAAA;IACtE,8CAA8C;IAC9C,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7E,MAAM,OAAO,GAAG,KAAK,IAAI,SAAS,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IAChF,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAE1D,MAAM,KAAK,GACT,OAAO,CAAC,YAAY,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAA;IAEnG,MAAM,GAAG,GAAiB,EAAE,CAAA;IAC5B,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAA;QAClE,MAAM,WAAW,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QACxD,IAAI,WAAW,EAAE,CAAC;YAChB;sEAC0D;YAC1D,MAAM,SAAS,GAAG,IAAA,sCAAiB,EAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,CAAA;YACpE,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,SAAS,kDAAkD,CAAC,CAAC,CAAC,EAAE,CAAA;YAC7F,GAAG,CAAC,IAAI,CAAC;gBACP,IAAI,EAAE,MAAM;gBACZ,OAAO,EAAE,IAAI,aAAa,eAAe,OAAO,CAAC,MAAM,sLAAsL,IAAI,KAAK,WAAW,EAAE;aACpQ,CAAC,CAAA;QACJ,CAAC;aAAM,CAAC;YACN,mDAAmD;YACnD,GAAG,CAAC,IAAI,CAAC;gBACP,IAAI,EAAE,MAAM;gBACZ,OAAO,EAAE,IAAI,aAAa,eAAe,OAAO,oHAAoH;aACrK,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IACD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QAC1C,IAAI,CAAC,OAAO;YAAE,SAAQ,CAAC,mCAAmC;QAC1D,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAC7B,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,CAAA;YACxC,SAAQ;QACV,CAAC;QACD,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC1B,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,YAAY,KAAK,OAAO,EAAE,EAAE,CAAC,CAAA;YACnE,SAAQ;QACV,CAAC;QACD;;;WAGG;QACH,MAAM,SAAS,GAAG,CAAC,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QAC9C,IAAI,SAAS,EAAE,CAAC;YACd,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,SAAS,KAAK,OAAO,EAAE,EAAE,CAAC,CAAA;YAClF,SAAQ;QACV,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAA;IACvF,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,oDAAoD;AACpD,SAAS,OAAO,CAAC,GAAe;IAC9B,OAAO,GAAG,CAAC,IAAI,KAAK,WAAW,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAA;AAC1D,CAAC;AAED,SAAS,OAAO,CAAC,GAAe;IAC9B,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;IAC1C,OAAO,IAAI,IAAI,aAAa,CAAA;AAC9B,CAAC;AAED;;;GAGG;AACH,SAAS,QAAQ,CAAC,IAAkB,EAAE,OAA2B,EAAE,IAAiB;IAClF,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAA;IACzB,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,CAAA;IACjD,IAAI,GAAG,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IACxB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAA;IACnC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IACpE,OAAO,CAAC,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC,CAAA;AAC3B,CAAC","sourcesContent":["/*\n * 에이전트에게 넘길 대화 이력 조립 — 순수 로직(리졸버와 분리해 단위 테스트 가능).\n *\n * ── 왜 서버 정본인가 ─────────────────────────────────────────────────────────\n * 예전에는 클라이언트가 화면에 들고 있던 줄들을 그대로 LLM 에 넘겼다. 협의(여러 참여자)에서는\n * 참여자마다 화면이 달라 **에이전트가 사람마다 다른 대화를 본다**. 서버는 ChatMessage 에 정본을\n * 갖고 있으므로 그것을 쓴다. 세션이 없는 즉석 대화(ad-hoc)만 클라이언트 이력을 쓴다.\n *\n * ── 발신자 표기 ─────────────────────────────────────────────────────────────\n * LLMMessage 는 `{ role, content }` 뿐이다 — 발신자 칸이 없다(Anthropic·Gemini 는 message.name 을\n * 지원하지 않아 이식성이 없다). 그래서 발신자는 **본문 맨 앞 표기**로만 전달할 수 있다.\n *\n * [김철수] 승인합니다\n *\n * **이 표기는 추론용 문맥이며 권한 판정 근거가 아니다.** 사용자가 본문에 같은 형식을 직접 적을 수\n * 있으므로, 되돌릴 수 없는 조치의 승인 여부는 반드시 서버 게이트(요청자 신원 기반)가 판정한다.\n * 표기는 \"누가 무슨 맥락에서 말했는지\"를 모델이 이해하게 하는 용도로만 쓴다.\n *\n * 발신자가 **한 명뿐이면 표기하지 않는다** — 혼자 쓰는 대화(대다수)에 노이즈와 토큰을 늘리지 않고,\n * 기존 동작과 같은 프롬프트를 유지한다.\n *\n * ── system 행 ───────────────────────────────────────────────────────────────\n * 캔버스 직접 편집 기록(`User directly edited the board: …`)은 원래 에이전트에게 알리려고 남긴 것인데,\n * 클라이언트가 role='system' 을 걸러내 사장돼 있었다. LLMMessage 에 system 역할이 없으므로\n * `[시스템]` 표기를 붙인 user 메시지로 넘긴다 — 사람이 아니므로 발신자 수 계산에는 넣지 않는다.\n *\n * ── 편집·재생성으로 접은 구간 ───────────────────────────────────────────────\n * 클라이언트의 메시지 편집은 화면에서만 접고 **서버 이력은 유지**한다(board-ai-chat.editUserMessage).\n * 서버 정본으로 바꾸면 접은 말이 에이전트 문맥에 되살아난다. 그래서 호출자가 `truncateAfterMessageId`\n * (마지막으로 남긴 메시지 id)를 주면 그 뒤 행을 버린다. 단 `keepMessageIds` 로 지정한 행(방금 저장한\n * 새 사용자 메시지)은 시간상 뒤에 있어도 항상 남긴다.\n *\n * 알려진 한계(현행 동작과 동일): 공유 세션에서 접은 구간에 **다른 사람의 발언**이 있으면 그것도\n * 함께 제외된다. 오늘 클라이언트가 보내는 이력도 같으므로 이 단계에서는 동등성을 유지하고,\n * \"내 발언만 접기\"는 별도 개선으로 다룬다.\n */\nimport type { LLMMessage } from '../types.js'\nimport { pendingForSummary, type StoredSummary } from './history-summary.js'\n\n/** 이력 한 줄 — 저장된 ChatMessage 에서 필요한 것만(엔티티·typeorm 비의존). */\nexport interface HistoryRow {\n id?: string\n role: string\n /** 멘션 마커(#token{refid:N})는 호출자가 미리 제거해 넘긴다 — 모델 주의 분산·토큰 낭비 방지. */\n content: string\n /** 발신자 식별자(서버 내부용 — 발신자 수 계산에만 쓰고 프롬프트에 넣지 않는다). */\n senderId?: string\n /** 프롬프트에 노출할 표시 이름. 이메일 같은 개인정보는 넣지 않는다. */\n senderName?: string\n /**\n * 누구에게 한 말인가 — `'assistant'` · `'room'`(ADR-0070 A15). 문이 저장한 값 그대로.\n *\n * 없는 행(이 칸이 생기기 전에 저장된 것)은 **모른다.** 모르는 것을 `room` 으로 적으면 그것이\n * 지어낸 사실이다 — 그때는 대상 없이 발신자만 적는다.\n */\n addressee?: string\n}\n\nexport interface BuildLlmHistoryOptions {\n /** 이 메시지까지만 유지하고 이후는 버린다(편집·재생성으로 접은 구간). 미지정이면 전부 사용. */\n truncateAfterMessageId?: string\n /** 잘라내기와 무관하게 항상 유지할 메시지 id(방금 저장한 새 사용자 메시지 등). */\n keepMessageIds?: string[]\n /** 발신자 표기 강제 — 미지정이면 사람 발신자가 둘 이상일 때만 표기. 테스트·특수 호출용. */\n labelSenders?: boolean\n /**\n * 모델에 넘길 **최근 메시지 수 상한**. 초과분(오래된 앞부분)은 버리고 생략 표시를 남긴다.\n *\n * 왜 필요한가: 협의는 며칠에 걸친다. 상한이 없으면 매 턴 전체 이력을 보내 프롬프트가 무한히\n * 자라고(비용·지연·문맥 한계) 오래된 말이 최근 상황을 덮는다. 미지정이면 전부 보낸다(기존 동작).\n *\n * **생략은 반드시 알린다** — 조용히 버리면 모델은 남은 첫 줄을 대화의 시작으로 오해한다.\n * 이건 상한일 뿐 요약이 아니다. 버린 내용을 요약해 실어 보내는 것은 다음 단계다(ChatSession.lastSummary).\n */\n maxTurns?: number\n /**\n * 상한으로 밀려난 앞부분을 대신할 **요약**(세션에 저장된 것).\n *\n * 있으면 \"N개 생략\" 대신 요약을 첫 줄로 싣는다 — 며칠 이어지는 협의에서 앞에서 합의한 것이\n * 없던 일이 되지 않게. 요약이 밀려난 구간을 **전부 덮지 못하면** 덮지 못한 개수도 함께 밝힌다\n * (요약이 최신이 아닐 수 있다는 사실을 숨기지 않는다).\n */\n summary?: StoredSummary\n}\n\n/*\n * These labels and notices are read by the model, so they are English. The reply the model writes is in the\n * language the person used; that is the model's doing, not this file's. They were Korean, which made every\n * conversation in this house speak Korean to the model whatever the reader's language was.\n */\n/** Marks a record no person wrote (a direct canvas edit, for one). */\nconst SYSTEM_LABEL = 'system'\n/** Marks a participant whose name we do not have — better than empty brackets. */\nconst UNKNOWN_LABEL = 'participant'\n/** Marks the notice about messages the turn limit dropped, so the model does not read the first kept line as the start. */\nconst OMITTED_LABEL = 'system'\n\n/**\n * 저장된 이력 → LLM 대화.\n *\n * 입력은 **시간 오름차순**을 가정한다(호출자가 createdAt ASC 로 조회). 정렬을 다시 하지 않는다 —\n * 같은 시각의 순서까지 보장하려면 조회 쪽에서 결정해야 하고, 여기서 추측하면 두 곳이 어긋난다.\n */\nexport function buildLlmHistory(rows: HistoryRow[], options: BuildLlmHistoryOptions = {}): LLMMessage[] {\n const keep = new Set(options.keepMessageIds ?? [])\n const truncated = truncate(rows, options.truncateAfterMessageId, keep)\n /* 상한 — 최근 것부터 남긴다. 오래된 말보다 최근 상황이 답을 좌우한다. */\n const limit = options.maxTurns && options.maxTurns > 0 ? options.maxTurns : 0\n const omitted = limit && truncated.length > limit ? truncated.length - limit : 0\n const kept = omitted ? truncated.slice(-limit) : truncated\n\n const label =\n options.labelSenders ?? new Set(kept.filter(r => isHuman(r)).map(r => r.senderId ?? '')).size > 1\n\n const out: LLMMessage[] = []\n if (omitted) {\n const dropped = truncated.slice(0, truncated.length - kept.length)\n const summaryText = (options.summary?.text ?? '').trim()\n if (summaryText) {\n /* 요약이 있으면 그것을 싣는다. **요약임을 밝힌다** — 모델이 원문처럼 인용하지 않게.\n * 요약이 밀려난 구간을 다 덮지 못하면 남은 개수도 함께 알린다(최신이 아님을 숨기지 않는다). */\n const uncovered = pendingForSummary(dropped, options.summary).length\n const tail = uncovered ? ` ${uncovered} message(s) after it are not in the summary yet.` : ''\n out.push({\n role: 'user',\n content: `[${OMITTED_LABEL}] The first ${dropped.length} message(s) of this conversation were replaced by the summary below because of the length limit. It is a summary, not the original — ask the person about anything it does not say.${tail}\\n${summaryText}`\n })\n } else {\n /* 요약이 아직 없으면 생략 사실만 알린다 — 개수만 밝히고 내용은 지어내지 않는다. */\n out.push({\n role: 'user',\n content: `[${OMITTED_LABEL}] The first ${omitted} message(s) of this conversation were left out because of the length limit. Ask the person again if you need them.`\n })\n }\n }\n for (const row of kept) {\n const content = (row.content ?? '').trim()\n if (!content) continue // 빈 본문(pending 자리 등)은 모델에 넘길 것이 없다\n if (row.role === 'assistant') {\n out.push({ role: 'assistant', content })\n continue\n }\n if (row.role === 'system') {\n out.push({ role: 'user', content: `[${SYSTEM_LABEL}] ${content}` })\n continue\n }\n /*\n * 사람의 말은 **누구에게 한 말인지까지** 적는다(ADR-0070 A15 결정 ②). 대상이 있을 때만 적으면\n * 모델은 빈 자리에서 추론한다 — 옆 사람이 방에 한 말을 자기에게 온 지시로 읽는 것이 그 결과다.\n */\n const addressee = (row.addressee ?? '').trim()\n if (addressee) {\n out.push({ role: 'user', content: `[${speaker(row)} → ${addressee}] ${content}` })\n continue\n }\n out.push({ role: 'user', content: label ? `[${speaker(row)}] ${content}` : content })\n }\n return out\n}\n\n/** 사람이 보낸 줄인가 — 발신자 수 계산 대상. system 기록은 사람이 아니다. */\nfunction isHuman(row: HistoryRow): boolean {\n return row.role !== 'assistant' && row.role !== 'system'\n}\n\nfunction speaker(row: HistoryRow): string {\n const name = (row.senderName ?? '').trim()\n return name || UNKNOWN_LABEL\n}\n\n/**\n * 접은 구간 제거. `truncateAfterMessageId` 가 없거나 이력에서 못 찾으면 **전부 유지**한다\n * (조용히 다 버리는 것보다 다 보내는 쪽이 안전하다 — 문맥 상실은 답을 망친다).\n */\nfunction truncate(rows: HistoryRow[], afterId: string | undefined, keep: Set<string>): HistoryRow[] {\n if (!afterId) return rows\n const idx = rows.findIndex(r => r.id === afterId)\n if (idx < 0) return rows\n const head = rows.slice(0, idx + 1)\n const tail = rows.slice(idx + 1).filter(r => r.id && keep.has(r.id))\n return [...head, ...tail]\n}\n"]}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 이 답이 지난 문들 — 부른 도구들의 문을 합친다(중복 없이, 정렬).
3
+ *
4
+ * 등록기에 없는 도구가 답에 끼면 그 도구가 무엇을 읽었는지 알 길이 없다. 그때 `[]`(누구나 읽는 답)로
5
+ * 적으면 모르는 값이 공개된다. 아무도 못 지나는 이름을 적어 **가려지는 쪽**으로 넘어뜨리고, 이름에
6
+ * 원인을 실어 둔다.
7
+ */
8
+ export declare function doorsForAnswer(toolNames: readonly string[], doorsOf: (name: string) => string[] | undefined): string[];
9
+ /**
10
+ * 한 요청 안에서 (사람, 문) 별로 한 번만 묻는 관문.
11
+ *
12
+ * 한 화면이 백 줄을 읽고 그 줄들이 같은 문 서넛을 가리킨다. 줄마다 물으면 권한 조회가 백 번 간다.
13
+ * 같은 요청 안에서 답이 바뀔 일은 없으므로 문 하나에 한 번 묻고 그 답을 쓴다. 관문은 요청마다 새로
14
+ * 만든다 — 프로세스가 들고 있으면 권한이 바뀌어도 낡은 답을 준다.
15
+ */
16
+ export declare function doorGate(check: (door: string) => Promise<boolean>): (door: string) => Promise<boolean>;
17
+ /** 이 사람이 이 행을 열 수 있나 — 적힌 문을 **전부** 지나야 한다(AND). */
18
+ export declare function passesRow(doors: readonly string[] | null | undefined, gate: (door: string) => Promise<boolean>): Promise<boolean>;
19
+ /** 문 판정에 필요한 만큼만 — 엔티티를 이 자리로 끌고 오지 않는다. */
20
+ export interface DoorReadableRow {
21
+ doors?: string[] | null;
22
+ content?: string | null;
23
+ }
24
+ /**
25
+ * 못 지나는 행의 값을 가린다 — 행은 남기고 본문을 한 줄로 바꾼다.
26
+ *
27
+ * `notice` 는 부르는 쪽이 번역해 넘긴다(서버 번역, 화면 말은 서버가 만든다 — ADR-0061 결정 7).
28
+ */
29
+ export declare function redactByDoors<T extends DoorReadableRow>(rows: readonly T[], gate: (door: string) => Promise<boolean>, notice: (doors: string[]) => string): Promise<T[]>;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ /*
3
+ * 답에 실린 문 — ADR-0073 판정 1.
4
+ *
5
+ * ── 무엇을 막는가 ─────────────────────────────────────────────────────────
6
+ * 대화방은 도메인 안에서 공개다(ADR-0073 판정 2). 그런데 대화 안에는 어시스턴트가 도구 결과로 근거를
7
+ * 대고 쓴 답이 들어 있고, 그 근거는 **부른 사람의 문**을 지나 얻은 값이다. 도구는 문을 지켰는데
8
+ * 어시스턴트의 문장이 그 값을 이미 평문으로 적어 놓았다. 나중에 읽는 사람은 자기가 못 여는 문 너머의
9
+ * 값을 어시스턴트의 말로 읽는다 — 도구 권한을 다 지켜도 **글로 새는 길**이다.
10
+ *
11
+ * 그래서 답 행에 그 답을 만든 도구들이 지난 문 목록을 적어 두고(사람의 말은 `[]`), 읽는 사람이 그 문을
12
+ * 전부 지나는지 볼 때마다 확인한다. 못 지나면 본문 대신 한 줄이 나간다 — **행이 있다는 사실은 보이고
13
+ * 값만 안 보인다.** 방에 무슨 일이 있었는지는 알 수 있고, 값은 자기 권한만큼만 읽는다.
14
+ *
15
+ * ── 시점은 뜻이 없다 ──────────────────────────────────────────────────────
16
+ * 언제 들어왔는지로 가르지 않는다. 오전에 있었든 방금 들어왔든, 볼 수 있는 것은 본다. 권한이 나중에
17
+ * 붙으면 그때부터 보이고, 떨어지면 그때부터 안 보인다. 저장된 행은 그대로 있고 **보는 일만** 달라진다.
18
+ *
19
+ * ── 모델이 읽는 이력도 같다 ───────────────────────────────────────────────
20
+ * 부른 사람이 못 여는 문의 답은 모델에게도 가리고 보낸다. 가리지 않으면 모델이 그 값을 다음 답에 옮겨
21
+ * 적어서, 문을 한 번도 안 지난 사람이 값을 갖게 된다. 가린 자리를 지우지 않고 한 줄로 남기는 이유도
22
+ * 같다 — 모델이 "그런 말 한 적 없다"고 답하면 그것은 거짓말이다.
23
+ *
24
+ * 이 파일은 저장소·엔티티를 모르는 순수 판정이다.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.doorsForAnswer = doorsForAnswer;
28
+ exports.doorGate = doorGate;
29
+ exports.passesRow = passesRow;
30
+ exports.redactByDoors = redactByDoors;
31
+ /** 문 이름으로 보이지 않는 값 — 등록되지 않은 도구가 답에 끼었을 때. */
32
+ const UNRESOLVED = 'unresolved:';
33
+ /**
34
+ * 이 답이 지난 문들 — 부른 도구들의 문을 합친다(중복 없이, 정렬).
35
+ *
36
+ * 등록기에 없는 도구가 답에 끼면 그 도구가 무엇을 읽었는지 알 길이 없다. 그때 `[]`(누구나 읽는 답)로
37
+ * 적으면 모르는 값이 공개된다. 아무도 못 지나는 이름을 적어 **가려지는 쪽**으로 넘어뜨리고, 이름에
38
+ * 원인을 실어 둔다.
39
+ */
40
+ function doorsForAnswer(toolNames, doorsOf) {
41
+ const doors = new Set();
42
+ for (const name of toolNames) {
43
+ const declared = doorsOf(name);
44
+ if (declared === undefined)
45
+ doors.add(`${UNRESOLVED}${name}`);
46
+ else
47
+ for (const door of declared)
48
+ doors.add(door);
49
+ }
50
+ return [...doors].sort();
51
+ }
52
+ /**
53
+ * 한 요청 안에서 (사람, 문) 별로 한 번만 묻는 관문.
54
+ *
55
+ * 한 화면이 백 줄을 읽고 그 줄들이 같은 문 서넛을 가리킨다. 줄마다 물으면 권한 조회가 백 번 간다.
56
+ * 같은 요청 안에서 답이 바뀔 일은 없으므로 문 하나에 한 번 묻고 그 답을 쓴다. 관문은 요청마다 새로
57
+ * 만든다 — 프로세스가 들고 있으면 권한이 바뀌어도 낡은 답을 준다.
58
+ */
59
+ function doorGate(check) {
60
+ const asked = new Map();
61
+ return (door) => {
62
+ /* 등록되지 않은 도구의 자리는 물을 것도 없다 — 지나는 사람이 없다. */
63
+ if (door.startsWith(UNRESOLVED))
64
+ return Promise.resolve(false);
65
+ let answer = asked.get(door);
66
+ if (!answer) {
67
+ answer = check(door);
68
+ asked.set(door, answer);
69
+ }
70
+ return answer;
71
+ };
72
+ }
73
+ /** 이 사람이 이 행을 열 수 있나 — 적힌 문을 **전부** 지나야 한다(AND). */
74
+ async function passesRow(doors, gate) {
75
+ for (const door of doors ?? []) {
76
+ if (!(await gate(door)))
77
+ return false;
78
+ }
79
+ return true;
80
+ }
81
+ /** 가린 행에서 값이 실려 있는 칸들 — 본문만 지우고 두면 그리로 새어 나간다. */
82
+ const VALUE_CARRYING = ['extra', 'proposals', 'grounding', 'patchEntryId', 'importSessionId'];
83
+ /**
84
+ * 못 지나는 행의 값을 가린다 — 행은 남기고 본문을 한 줄로 바꾼다.
85
+ *
86
+ * `notice` 는 부르는 쪽이 번역해 넘긴다(서버 번역, 화면 말은 서버가 만든다 — ADR-0061 결정 7).
87
+ */
88
+ async function redactByDoors(rows, gate, notice) {
89
+ const out = [];
90
+ for (const row of rows) {
91
+ const doors = row.doors ?? [];
92
+ if (doors.length === 0 || (await passesRow(doors, gate))) {
93
+ out.push(row);
94
+ continue;
95
+ }
96
+ const hidden = { ...row, content: notice(doors) };
97
+ for (const field of VALUE_CARRYING) {
98
+ if (field in hidden)
99
+ hidden[field] = null;
100
+ }
101
+ /* 가려졌다는 것 자체는 사실이다. 화면이 자물쇠를 그릴 수 있게 남긴다. */
102
+ hidden.redacted = true;
103
+ out.push(hidden);
104
+ }
105
+ return out;
106
+ }
107
+ //# sourceMappingURL=message-doors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-doors.js","sourceRoot":"","sources":["../../../server/service/chat-message/message-doors.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;;AAYH,wCAUC;AASD,4BAcC;AAGD,8BAKC;AAgBD,sCAwBC;AA3FD,8CAA8C;AAC9C,MAAM,UAAU,GAAG,aAAa,CAAA;AAEhC;;;;;;GAMG;AACH,SAAgB,cAAc,CAAC,SAA4B,EAAE,OAA+C;IAC1G,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAA;IAE/B,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;QAC9B,IAAI,QAAQ,KAAK,SAAS;YAAE,KAAK,CAAC,GAAG,CAAC,GAAG,UAAU,GAAG,IAAI,EAAE,CAAC,CAAA;;YACxD,KAAK,MAAM,IAAI,IAAI,QAAQ;gBAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IACnD,CAAC;IAED,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,EAAE,CAAA;AAC1B,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,QAAQ,CAAC,KAAyC;IAChE,MAAM,KAAK,GAAG,IAAI,GAAG,EAA4B,CAAA;IAEjD,OAAO,CAAC,IAAY,EAAE,EAAE;QACtB,4CAA4C;QAC5C,IAAI,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;YAAE,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;QAE9D,IAAI,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QAC5B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,CAAA;YACpB,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QACzB,CAAC;QACD,OAAO,MAAM,CAAA;IACf,CAAC,CAAA;AACH,CAAC;AAED,oDAAoD;AAC7C,KAAK,UAAU,SAAS,CAAC,KAA2C,EAAE,IAAwC;IACnH,KAAK,MAAM,IAAI,IAAI,KAAK,IAAI,EAAE,EAAE,CAAC;QAC/B,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;YAAE,OAAO,KAAK,CAAA;IACvC,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAQD,kDAAkD;AAClD,MAAM,cAAc,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,iBAAiB,CAAU,CAAA;AAEtG;;;;GAIG;AACI,KAAK,UAAU,aAAa,CACjC,IAAkB,EAClB,IAAwC,EACxC,MAAmC;IAEnC,MAAM,GAAG,GAAQ,EAAE,CAAA;IAEnB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,IAAI,EAAE,CAAA;QAC7B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC;YACzD,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YACb,SAAQ;QACV,CAAC;QAED,MAAM,MAAM,GAAQ,EAAE,GAAG,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAA;QACtD,KAAK,MAAM,KAAK,IAAI,cAAc,EAAE,CAAC;YACnC,IAAI,KAAK,IAAI,MAAM;gBAAE,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAA;QAC3C,CAAC;QACD,6CAA6C;QAC7C,MAAM,CAAC,QAAQ,GAAG,IAAI,CAAA;QACtB,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAClB,CAAC;IAED,OAAO,GAAG,CAAA;AACZ,CAAC","sourcesContent":["/*\n * 답에 실린 문 — ADR-0073 판정 1.\n *\n * ── 무엇을 막는가 ─────────────────────────────────────────────────────────\n * 대화방은 도메인 안에서 공개다(ADR-0073 판정 2). 그런데 대화 안에는 어시스턴트가 도구 결과로 근거를\n * 대고 쓴 답이 들어 있고, 그 근거는 **부른 사람의 문**을 지나 얻은 값이다. 도구는 문을 지켰는데\n * 어시스턴트의 문장이 그 값을 이미 평문으로 적어 놓았다. 나중에 읽는 사람은 자기가 못 여는 문 너머의\n * 값을 어시스턴트의 말로 읽는다 — 도구 권한을 다 지켜도 **글로 새는 길**이다.\n *\n * 그래서 답 행에 그 답을 만든 도구들이 지난 문 목록을 적어 두고(사람의 말은 `[]`), 읽는 사람이 그 문을\n * 전부 지나는지 볼 때마다 확인한다. 못 지나면 본문 대신 한 줄이 나간다 — **행이 있다는 사실은 보이고\n * 값만 안 보인다.** 방에 무슨 일이 있었는지는 알 수 있고, 값은 자기 권한만큼만 읽는다.\n *\n * ── 시점은 뜻이 없다 ──────────────────────────────────────────────────────\n * 언제 들어왔는지로 가르지 않는다. 오전에 있었든 방금 들어왔든, 볼 수 있는 것은 본다. 권한이 나중에\n * 붙으면 그때부터 보이고, 떨어지면 그때부터 안 보인다. 저장된 행은 그대로 있고 **보는 일만** 달라진다.\n *\n * ── 모델이 읽는 이력도 같다 ───────────────────────────────────────────────\n * 부른 사람이 못 여는 문의 답은 모델에게도 가리고 보낸다. 가리지 않으면 모델이 그 값을 다음 답에 옮겨\n * 적어서, 문을 한 번도 안 지난 사람이 값을 갖게 된다. 가린 자리를 지우지 않고 한 줄로 남기는 이유도\n * 같다 — 모델이 \"그런 말 한 적 없다\"고 답하면 그것은 거짓말이다.\n *\n * 이 파일은 저장소·엔티티를 모르는 순수 판정이다.\n */\n\n/** 문 이름으로 보이지 않는 값 — 등록되지 않은 도구가 답에 끼었을 때. */\nconst UNRESOLVED = 'unresolved:'\n\n/**\n * 이 답이 지난 문들 — 부른 도구들의 문을 합친다(중복 없이, 정렬).\n *\n * 등록기에 없는 도구가 답에 끼면 그 도구가 무엇을 읽었는지 알 길이 없다. 그때 `[]`(누구나 읽는 답)로\n * 적으면 모르는 값이 공개된다. 아무도 못 지나는 이름을 적어 **가려지는 쪽**으로 넘어뜨리고, 이름에\n * 원인을 실어 둔다.\n */\nexport function doorsForAnswer(toolNames: readonly string[], doorsOf: (name: string) => string[] | undefined): string[] {\n const doors = new Set<string>()\n\n for (const name of toolNames) {\n const declared = doorsOf(name)\n if (declared === undefined) doors.add(`${UNRESOLVED}${name}`)\n else for (const door of declared) doors.add(door)\n }\n\n return [...doors].sort()\n}\n\n/**\n * 한 요청 안에서 (사람, 문) 별로 한 번만 묻는 관문.\n *\n * 한 화면이 백 줄을 읽고 그 줄들이 같은 문 서넛을 가리킨다. 줄마다 물으면 권한 조회가 백 번 간다.\n * 같은 요청 안에서 답이 바뀔 일은 없으므로 문 하나에 한 번 묻고 그 답을 쓴다. 관문은 요청마다 새로\n * 만든다 — 프로세스가 들고 있으면 권한이 바뀌어도 낡은 답을 준다.\n */\nexport function doorGate(check: (door: string) => Promise<boolean>): (door: string) => Promise<boolean> {\n const asked = new Map<string, Promise<boolean>>()\n\n return (door: string) => {\n /* 등록되지 않은 도구의 자리는 물을 것도 없다 — 지나는 사람이 없다. */\n if (door.startsWith(UNRESOLVED)) return Promise.resolve(false)\n\n let answer = asked.get(door)\n if (!answer) {\n answer = check(door)\n asked.set(door, answer)\n }\n return answer\n }\n}\n\n/** 이 사람이 이 행을 열 수 있나 — 적힌 문을 **전부** 지나야 한다(AND). */\nexport async function passesRow(doors: readonly string[] | null | undefined, gate: (door: string) => Promise<boolean>): Promise<boolean> {\n for (const door of doors ?? []) {\n if (!(await gate(door))) return false\n }\n return true\n}\n\n/** 문 판정에 필요한 만큼만 — 엔티티를 이 자리로 끌고 오지 않는다. */\nexport interface DoorReadableRow {\n doors?: string[] | null\n content?: string | null\n}\n\n/** 가린 행에서 값이 실려 있는 칸들 — 본문만 지우고 두면 그리로 새어 나간다. */\nconst VALUE_CARRYING = ['extra', 'proposals', 'grounding', 'patchEntryId', 'importSessionId'] as const\n\n/**\n * 못 지나는 행의 값을 가린다 — 행은 남기고 본문을 한 줄로 바꾼다.\n *\n * `notice` 는 부르는 쪽이 번역해 넘긴다(서버 번역, 화면 말은 서버가 만든다 — ADR-0061 결정 7).\n */\nexport async function redactByDoors<T extends DoorReadableRow>(\n rows: readonly T[],\n gate: (door: string) => Promise<boolean>,\n notice: (doors: string[]) => string\n): Promise<T[]> {\n const out: T[] = []\n\n for (const row of rows) {\n const doors = row.doors ?? []\n if (doors.length === 0 || (await passesRow(doors, gate))) {\n out.push(row)\n continue\n }\n\n const hidden: any = { ...row, content: notice(doors) }\n for (const field of VALUE_CARRYING) {\n if (field in hidden) hidden[field] = null\n }\n /* 가려졌다는 것 자체는 사실이다. 화면이 자물쇠를 그릴 수 있게 남긴다. */\n hidden.redacted = true\n out.push(hidden)\n }\n\n return out\n}\n"]}
@@ -0,0 +1,18 @@
1
+ /** ADR-0042's verdicts, plus the one for a person who backed out at the confirm step. */
2
+ export type ProposalVerdict = 'accepted' | 'refused' | 'unavailable' | 'declined';
3
+ export interface ProposalOutcome {
4
+ verdict: ProposalVerdict;
5
+ /** The host's own reason code, carried through untouched — no mapping table (ADR-0042). */
6
+ errorCode?: string;
7
+ /** For a proposal made of several edits: how many went in, and how many did not. */
8
+ applied?: number;
9
+ missed?: number;
10
+ }
11
+ export declare const PROPOSAL_OUTCOME_PREFIX = "proposal ";
12
+ export declare const PROPOSAL_VERDICTS: ProposalVerdict[];
13
+ /**
14
+ * `proposal <key>: <verdict> (<errorCode>) 2/3`
15
+ *
16
+ * English, like the other lines the model reads. The panel turns it into the reader's language.
17
+ */
18
+ export declare function formatProposalOutcomeLine(key: string, outcome: ProposalOutcome): string;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ /*
3
+ * What the host did with a proposal, written into the conversation (ADR-0070 A12-a).
4
+ *
5
+ * The card used to change to "executed" the moment it was pressed, which is the one thing it knew
6
+ * and the one thing that is not the answer. The host's answer comes back through a server door and
7
+ * is stored as a `system` row, so three readers get it from the same place: the card that is open,
8
+ * the card after a refresh, and the model on its next turn — a model that is not told its proposal
9
+ * was refused goes on as though it had been applied.
10
+ *
11
+ * The line is the row's content. It is parsed back by the panel, so the shape is fixed here and the
12
+ * panel's parser is tested against this formatter.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.PROPOSAL_VERDICTS = exports.PROPOSAL_OUTCOME_PREFIX = void 0;
16
+ exports.formatProposalOutcomeLine = formatProposalOutcomeLine;
17
+ exports.PROPOSAL_OUTCOME_PREFIX = 'proposal ';
18
+ exports.PROPOSAL_VERDICTS = ['accepted', 'refused', 'unavailable', 'declined'];
19
+ /**
20
+ * `proposal <key>: <verdict> (<errorCode>) 2/3`
21
+ *
22
+ * English, like the other lines the model reads. The panel turns it into the reader's language.
23
+ */
24
+ function formatProposalOutcomeLine(key, outcome) {
25
+ const reason = outcome.errorCode ? ` (${outcome.errorCode})` : '';
26
+ const counted = typeof outcome.applied === 'number' || typeof outcome.missed === 'number'
27
+ ? ` ${outcome.applied ?? 0}/${(outcome.applied ?? 0) + (outcome.missed ?? 0)}`
28
+ : '';
29
+ return `${exports.PROPOSAL_OUTCOME_PREFIX}${key}: ${outcome.verdict}${reason}${counted}`;
30
+ }
31
+ //# sourceMappingURL=proposal-outcome.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposal-outcome.js","sourceRoot":"","sources":["../../../server/service/chat-message/proposal-outcome.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;AAuBH,8DAOC;AAhBY,QAAA,uBAAuB,GAAG,WAAW,CAAA;AAErC,QAAA,iBAAiB,GAAsB,CAAC,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,CAAC,CAAA;AAEtG;;;;GAIG;AACH,SAAgB,yBAAyB,CAAC,GAAW,EAAE,OAAwB;IAC7E,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,EAAE,CAAA;IACjE,MAAM,OAAO,GACX,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,IAAI,OAAO,OAAO,CAAC,MAAM,KAAK,QAAQ;QACvE,CAAC,CAAC,IAAI,OAAO,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,EAAE;QAC9E,CAAC,CAAC,EAAE,CAAA;IACR,OAAO,GAAG,+BAAuB,GAAG,GAAG,KAAK,OAAO,CAAC,OAAO,GAAG,MAAM,GAAG,OAAO,EAAE,CAAA;AAClF,CAAC","sourcesContent":["/*\n * What the host did with a proposal, written into the conversation (ADR-0070 A12-a).\n *\n * The card used to change to \"executed\" the moment it was pressed, which is the one thing it knew\n * and the one thing that is not the answer. The host's answer comes back through a server door and\n * is stored as a `system` row, so three readers get it from the same place: the card that is open,\n * the card after a refresh, and the model on its next turn — a model that is not told its proposal\n * was refused goes on as though it had been applied.\n *\n * The line is the row's content. It is parsed back by the panel, so the shape is fixed here and the\n * panel's parser is tested against this formatter.\n */\n\n/** ADR-0042's verdicts, plus the one for a person who backed out at the confirm step. */\nexport type ProposalVerdict = 'accepted' | 'refused' | 'unavailable' | 'declined'\n\nexport interface ProposalOutcome {\n verdict: ProposalVerdict\n /** The host's own reason code, carried through untouched — no mapping table (ADR-0042). */\n errorCode?: string\n /** For a proposal made of several edits: how many went in, and how many did not. */\n applied?: number\n missed?: number\n}\n\nexport const PROPOSAL_OUTCOME_PREFIX = 'proposal '\n\nexport const PROPOSAL_VERDICTS: ProposalVerdict[] = ['accepted', 'refused', 'unavailable', 'declined']\n\n/**\n * `proposal <key>: <verdict> (<errorCode>) 2/3`\n *\n * English, like the other lines the model reads. The panel turns it into the reader's language.\n */\nexport function formatProposalOutcomeLine(key: string, outcome: ProposalOutcome): string {\n const reason = outcome.errorCode ? ` (${outcome.errorCode})` : ''\n const counted =\n typeof outcome.applied === 'number' || typeof outcome.missed === 'number'\n ? ` ${outcome.applied ?? 0}/${(outcome.applied ?? 0) + (outcome.missed ?? 0)}`\n : ''\n return `${PROPOSAL_OUTCOME_PREFIX}${key}: ${outcome.verdict}${reason}${counted}`\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import { type DoorReadableRow } from './message-doors.js';
2
+ /** 이 요청에서 행을 가려 내보내는 함수. 같은 요청 안에서 계속 쓴다. */
3
+ export declare function readerGate(context: any): <T extends DoorReadableRow>(rows: readonly T[]) => Promise<T[]>;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readerGate = readerGate;
4
+ /*
5
+ * 읽는 사람의 관문 — ADR-0073 판정 1·2 를 요청 하나에 세운다.
6
+ *
7
+ * 리졸버는 이것을 한 번 만들고 행 목록을 통과시킨다. 문 판정은 (사람, 문) 별로 한 번만 일어나고,
8
+ * 세션이 private 인지도 여기서 함께 답한다. 리졸버마다 같은 판정을 다시 적으면 한 곳이 빠진 날
9
+ * 그 문으로 샌다.
10
+ */
11
+ const auth_base_1 = require("@things-factory/auth-base");
12
+ const door_access_js_1 = require("./door-access.js");
13
+ const message_doors_js_1 = require("./message-doors.js");
14
+ const request_translator_js_1 = require("../request-translator.js");
15
+ /** 이 요청에서 행을 가려 내보내는 함수. 같은 요청 안에서 계속 쓴다. */
16
+ function readerGate(context) {
17
+ const { domain, user } = context.state;
18
+ const t = (0, request_translator_js_1.requestTranslator)(context);
19
+ const { unsafeIP, prohibitedPrivileges } = context.state;
20
+ const gate = (0, message_doors_js_1.doorGate)(
21
+ /* 문에서 쓰는 판정을 그대로 쓴다 — 같은 인자, 같은 함수(ADR-0072 결정 5). */
22
+ (0, door_access_js_1.doorCheck)({
23
+ lookup: auth_base_1.privilegeOfField,
24
+ permits: fieldGate => (0, auth_base_1.checkPermission)(fieldGate, user, domain, unsafeIP, prohibitedPrivileges)
25
+ }));
26
+ /*
27
+ * 문 이름(`Query.productionReports`)은 이 문장에 넣지 않는다.
28
+ *
29
+ * 현장에서 읽는 사람에게 GraphQL 이름은 아무 말도 아니다. 문 이름을 사람의 말로 바꿀 표는 AUTH 의
30
+ * 필드→권한 표와 같은 자리에서 나와야 하고, 그것은 아직 없다(ADR-0072 결정 2 (b)). 없는 표를 흉내
31
+ * 내느니 문장은 사실만 말하고, 목록은 `ChatMessage.doors` 로 따로 나간다 — 관리자에게 무엇을
32
+ * 청할지 알아야 하는 화면은 거기서 읽는다. 문구는 UX 레인이 정한다.
33
+ */
34
+ return rows => (0, message_doors_js_1.redactByDoors)(rows, gate, () => t('ai-assistant.text.answer-needs-permission', {
35
+ /* 구독에는 요청 번역기가 없다 — `request-translator.ts` 참조. */
36
+ defaultValue: 'you do not have permission to see this answer. ask an administrator for access.'
37
+ }));
38
+ }
39
+ //# sourceMappingURL=reader-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reader-gate.js","sourceRoot":"","sources":["../../../server/service/chat-message/reader-gate.ts"],"names":[],"mappings":";;AAcA,gCA6BC;AA3CD;;;;;;GAMG;AACH,yDAA6E;AAE7E,qDAA4C;AAC5C,yDAAkF;AAClF,oEAA4D;AAE5D,6CAA6C;AAC7C,SAAgB,UAAU,CAAC,OAAY;IACrC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IACtC,MAAM,CAAC,GAAG,IAAA,yCAAiB,EAAC,OAAO,CAAC,CAAA;IAEpC,MAAM,EAAE,QAAQ,EAAE,oBAAoB,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAExD,MAAM,IAAI,GAAG,IAAA,2BAAQ;IACnB,sDAAsD;IACtD,IAAA,0BAAS,EAAC;QACR,MAAM,EAAE,4BAAgB;QACxB,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC,IAAA,2BAAe,EAAC,SAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,CAAC;KACtG,CAAC,CACH,CAAA;IAED;;;;;;;OAOG;IACH,OAAO,IAAI,CAAC,EAAE,CACZ,IAAA,gCAAa,EAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,CAC7B,CAAC,CAAC,2CAA2C,EAAE;QAC7C,mDAAmD;QACnD,YAAY,EAAE,iFAAiF;KAChG,CAAC,CACH,CAAA;AACL,CAAC","sourcesContent":["/*\n * 읽는 사람의 관문 — ADR-0073 판정 1·2 를 요청 하나에 세운다.\n *\n * 리졸버는 이것을 한 번 만들고 행 목록을 통과시킨다. 문 판정은 (사람, 문) 별로 한 번만 일어나고,\n * 세션이 private 인지도 여기서 함께 답한다. 리졸버마다 같은 판정을 다시 적으면 한 곳이 빠진 날\n * 그 문으로 샌다.\n */\nimport { checkPermission, privilegeOfField } from '@things-factory/auth-base'\n\nimport { doorCheck } from './door-access.js'\nimport { doorGate, redactByDoors, type DoorReadableRow } from './message-doors.js'\nimport { requestTranslator } from '../request-translator.js'\n\n/** 이 요청에서 행을 가려 내보내는 함수. 같은 요청 안에서 계속 쓴다. */\nexport function readerGate(context: any): <T extends DoorReadableRow>(rows: readonly T[]) => Promise<T[]> {\n const { domain, user } = context.state\n const t = requestTranslator(context)\n\n const { unsafeIP, prohibitedPrivileges } = context.state\n\n const gate = doorGate(\n /* 문에서 쓰는 판정을 그대로 쓴다 — 같은 인자, 같은 함수(ADR-0072 결정 5). */\n doorCheck({\n lookup: privilegeOfField,\n permits: fieldGate => checkPermission(fieldGate as any, user, domain, unsafeIP, prohibitedPrivileges)\n })\n )\n\n /*\n * 문 이름(`Query.productionReports`)은 이 문장에 넣지 않는다.\n *\n * 현장에서 읽는 사람에게 GraphQL 이름은 아무 말도 아니다. 문 이름을 사람의 말로 바꿀 표는 AUTH 의\n * 필드→권한 표와 같은 자리에서 나와야 하고, 그것은 아직 없다(ADR-0072 결정 2 (b)). 없는 표를 흉내\n * 내느니 문장은 사실만 말하고, 목록은 `ChatMessage.doors` 로 따로 나간다 — 관리자에게 무엇을\n * 청할지 알아야 하는 화면은 거기서 읽는다. 문구는 UX 레인이 정한다.\n */\n return rows =>\n redactByDoors(rows, gate, () =>\n t('ai-assistant.text.answer-needs-permission', {\n /* 구독에는 요청 번역기가 없다 — `request-translator.ts` 참조. */\n defaultValue: 'you do not have permission to see this answer. ask an administrator for access.'\n })\n )\n}\n"]}
@@ -0,0 +1,4 @@
1
+ /** How much of one turn is kept — the same cap the carried turns use. */
2
+ export declare const ROOM_MESSAGE_LIMIT = 4000;
3
+ /** Trimmed and capped, or nothing when there is no message. */
4
+ export declare function roomMessageBody(content: string | undefined | null): string | undefined;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ /*
3
+ * A person saying something to the room (ADR-0070 A15).
4
+ *
5
+ * The addressee column has two values and, until this door, a screen could only produce one of
6
+ * them: every turn a person typed went through `assistantChat`, which means every turn was for the
7
+ * assistant. `room` existed only on turns carried in from somewhere else. So the half of the design
8
+ * that says "people also talk to each other here, and the assistant reads it as context" had no way
9
+ * to happen.
10
+ *
11
+ * The rule the door follows is the same one: the addressee comes from **which door was called**,
12
+ * never from an argument. This one is the people's door — it stores the turn and does not call the
13
+ * model. Nobody is answered, and that silence is the point (A15 decision ⑦).
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.ROOM_MESSAGE_LIMIT = void 0;
17
+ exports.roomMessageBody = roomMessageBody;
18
+ /** How much of one turn is kept — the same cap the carried turns use. */
19
+ exports.ROOM_MESSAGE_LIMIT = 4000;
20
+ /** Trimmed and capped, or nothing when there is no message. */
21
+ function roomMessageBody(content) {
22
+ const body = (content ?? '').trim().slice(0, exports.ROOM_MESSAGE_LIMIT);
23
+ return body || undefined;
24
+ }
25
+ //# sourceMappingURL=room-message.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-message.js","sourceRoot":"","sources":["../../../server/service/chat-message/room-message.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;;AAMH,0CAGC;AAPD,yEAAyE;AAC5D,QAAA,kBAAkB,GAAG,IAAI,CAAA;AAEtC,+DAA+D;AAC/D,SAAgB,eAAe,CAAC,OAAkC;IAChE,MAAM,IAAI,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,0BAAkB,CAAC,CAAA;IAChE,OAAO,IAAI,IAAI,SAAS,CAAA;AAC1B,CAAC","sourcesContent":["/*\n * A person saying something to the room (ADR-0070 A15).\n *\n * The addressee column has two values and, until this door, a screen could only produce one of\n * them: every turn a person typed went through `assistantChat`, which means every turn was for the\n * assistant. `room` existed only on turns carried in from somewhere else. So the half of the design\n * that says \"people also talk to each other here, and the assistant reads it as context\" had no way\n * to happen.\n *\n * The rule the door follows is the same one: the addressee comes from **which door was called**,\n * never from an argument. This one is the people's door — it stores the turn and does not call the\n * model. Nobody is answered, and that silence is the point (A15 decision ⑦).\n */\n\n/** How much of one turn is kept — the same cap the carried turns use. */\nexport const ROOM_MESSAGE_LIMIT = 4000\n\n/** Trimmed and capped, or nothing when there is no message. */\nexport function roomMessageBody(content: string | undefined | null): string | undefined {\n const body = (content ?? '').trim().slice(0, ROOM_MESSAGE_LIMIT)\n return body || undefined\n}\n"]}