@things-factory/ai-assistant 10.1.234 → 10.1.244

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 (37) hide show
  1. package/dist-client/tsconfig.tsbuildinfo +1 -1
  2. package/dist-server/service/assistant-mode/mode-categories.js +5 -0
  3. package/dist-server/service/assistant-mode/mode-categories.js.map +1 -1
  4. package/dist-server/service/chat-message/chat-message.js +3 -3
  5. package/dist-server/service/chat-message/chat-message.js.map +1 -1
  6. package/dist-server/service/chat-session/chat-session.js +2 -2
  7. package/dist-server/service/chat-session/chat-session.js.map +1 -1
  8. package/dist-server/service/chat-session-participant/chat-session-participant.js +1 -1
  9. package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -1
  10. package/dist-server/service/help-documents/help-keys.d.ts +1 -0
  11. package/dist-server/service/help-documents/help-keys.js +6 -0
  12. package/dist-server/service/help-documents/help-keys.js.map +1 -0
  13. package/dist-server/service/help-documents/help-tools.d.ts +38 -0
  14. package/dist-server/service/help-documents/help-tools.js +85 -0
  15. package/dist-server/service/help-documents/help-tools.js.map +1 -0
  16. package/dist-server/service/index.d.ts +1 -0
  17. package/dist-server/service/index.js +1 -0
  18. package/dist-server/service/index.js.map +1 -1
  19. package/dist-server/service/unattended-run/assistant-run.js +1 -1
  20. package/dist-server/service/unattended-run/assistant-run.js.map +1 -1
  21. package/dist-server/service/unattended-run/run-unattended.js +1 -1
  22. package/dist-server/service/unattended-run/run-unattended.js.map +1 -1
  23. package/dist-server/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +7 -6
  25. package/server/service/assistant-mode/mode-categories.ts +5 -0
  26. package/server/service/assistant-mode/mode-front.test.ts +4 -3
  27. package/server/service/assistant-mode/resolve.test.ts +1 -1
  28. package/server/service/chat-message/chat-message.ts +3 -3
  29. package/server/service/chat-session/chat-session.ts +2 -2
  30. package/server/service/chat-session-participant/chat-session-participant.ts +1 -1
  31. package/server/service/data-entry/data-entry.test.ts +2 -2
  32. package/server/service/help-documents/help-keys.ts +2 -0
  33. package/server/service/help-documents/help-tools.test.ts +40 -0
  34. package/server/service/help-documents/help-tools.ts +86 -0
  35. package/server/service/index.ts +1 -0
  36. package/server/service/unattended-run/assistant-run.ts +1 -1
  37. package/server/service/unattended-run/run-unattended.ts +1 -1
@@ -14,6 +14,7 @@ exports.modeScreens = modeScreens;
14
14
  /* Read by the resolver and by the boot report, so the report and the conversation open the same tools. */
15
15
  const entry_target_js_1 = require("../data-entry/entry-target.js");
16
16
  const read_target_js_1 = require("../data-read/read-target.js");
17
+ const help_keys_js_1 = require("../help-documents/help-keys.js");
17
18
  const people_keys_js_1 = require("../people/people-keys.js");
18
19
  const conversationCategories = new Map();
19
20
  function registerConversationCategory(category) {
@@ -40,6 +41,8 @@ function modeToolCategories(mode) {
40
41
  'assistant-workspace',
41
42
  /* Every conversation may read who works here and offer to bring one of them in (people/, ADR-0073 decision 3). */
42
43
  ...people_keys_js_1.PEOPLE_CATEGORIES,
44
+ /* Every conversation may read the application's help (ADR-0181). */
45
+ help_keys_js_1.HELP_CATEGORY,
43
46
  ...(mode.attachments?.kinds?.length ? ['chat-attachments'] : []),
44
47
  ...(mode.entryTargets ?? []).map(key => `data-entry:${key}`),
45
48
  ...(mode.readTargets ?? []).map(key => `data-read:${key}`),
@@ -111,6 +114,8 @@ function modeFrontCategories(mode, screen = {}) {
111
114
  * there was no tool to look people up (2026-10-24).
112
115
  */
113
116
  ...people_keys_js_1.PEOPLE_CATEGORIES,
117
+ /* Two tools, always in front: 「지출결의는 어디서 올려?」 is asked on any screen (ADR-0181). */
118
+ help_keys_js_1.HELP_CATEGORY,
114
119
  ...(mode.attachments?.kinds?.length ? ['chat-attachments'] : []),
115
120
  ...keys.filter(key => (mode.readTargets ?? []).includes(key)).map(key => `data-read:${key}`),
116
121
  ...keys.filter(key => (mode.entryTargets ?? []).includes(key)).map(key => `data-entry:${key}`),
@@ -1 +1 @@
1
- {"version":3,"file":"mode-categories.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/mode-categories.ts"],"names":[],"mappings":"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","sourcesContent":["/* Read by the resolver and by the boot report, so the report and the conversation open the same tools. */\nimport { entryTargetOf } from '../data-entry/entry-target.js'\nimport { detailReadOf, readTargetOf } from '../data-read/read-target.js'\nimport { PEOPLE_CATEGORIES } from '../people/people-keys.js'\n/**\n * A category another package opens in every conversation - a dashboard card (ADR-0160 decision 9). It is in reach of\n * every conversation; it is put in front only on the pages it names, and found with `openTools` elsewhere (ADR-0061\n * decision 9). Never in an unattended run: its tools are proposals, and nobody is there to press the card.\n */\nexport interface ConversationCategory {\n name: string\n /** The application routes whose conversations put it in front (`my-dashboard`). */\n frontPages?: string[]\n /**\n * The axes of the records whose selection or anchor puts it in front (`dashboard-card`) - a card picked to change is\n * changed by this category's tools, wherever the dock is open.\n */\n frontAxes?: string[]\n /**\n * What a turn asked from one of its front pages must end in - said in that turn's system prompt only. On 나의 대시보드\n * a request to see a figure is a card; asked 「이번 달 지출 합계」 there, the model answered 「0원」 in words (2026-10-25).\n */\n pageRule?: string\n}\n\nconst conversationCategories = new Map<string, ConversationCategory>()\n\nexport function registerConversationCategory(category: ConversationCategory): void {\n conversationCategories.set(category.name, category)\n}\n\nexport function conversationCategoriesOf(): ConversationCategory[] {\n return [...conversationCategories.values()]\n}\n\nexport function clearConversationCategories(): void {\n conversationCategories.clear()\n}\n\n/** The system prompt of one turn - the mode's, the open record's line, and the rules of the page it is asked from. */\nexport function turnSystemPrompt(modePrompt: string, screen: { page?: string; subjectLine?: string } = {}): string {\n const pageRules = screen.page ? conversationCategoriesOf().filter(one => one.pageRule && (one.frontPages ?? []).includes(screen.page!)).map(one => one.pageRule!) : []\n return [modePrompt, screen.subjectLine, ...pageRules].filter(Boolean).join('\\n\\n')\n}\n\n/**\n * The categories a mode opens: its own, the AI 작업 패널 (design/ai/workspace.md), the attachment reader when it\n * takes files (design/ai/shared-dock.md §9-4), and one propose tool per record it may enter (design/ai/data-entry.md).\n */\nexport function modeToolCategories(mode: { toolCategories?: string[]; attachments?: { kinds?: string[] }; entryTargets?: string[]; readTargets?: string[] }): string[] | undefined {\n const extra = [\n /* Every conversation may put a large result on the AI 작업 패널 (design/ai/workspace.md). */\n 'assistant-workspace',\n /* Every conversation may read who works here and offer to bring one of them in (people/, ADR-0073 decision 3). */\n ...PEOPLE_CATEGORIES,\n ...(mode.attachments?.kinds?.length ? ['chat-attachments'] : []),\n ...(mode.entryTargets ?? []).map(key => `data-entry:${key}`),\n ...(mode.readTargets ?? []).map(key => `data-read:${key}`),\n ...conversationCategoriesOf().map(one => one.name)\n ]\n return [...new Set([...(mode.toolCategories ?? []), ...extra])]\n}\n\n/**\n * The categories an unattended run of a mode opens: its own and the lists it reads (design/ai/unattended-run.md §3).\n *\n * Not the 입력안 tools - nobody is there to press the card - nor the AI 작업 패널 or the attachment reader: there is no\n * conversation for a card or a file to belong to.\n */\nexport function unattendedToolCategories(mode: { toolCategories?: string[]; readTargets?: string[] }): string[] {\n return [...new Set([...(mode.toolCategories ?? []), ...(mode.readTargets ?? []).map(key => `data-read:${key}`)])]\n}\n\n/** The screen a turn is asked from - the page the dock is on, and the record its conversation is anchored to. */\nexport interface TurnScreen {\n /** The application's route of the open page (`cash-transaction-list`), as the dock sends it. */\n page?: string\n /** The axis of the record the conversation is anchored to (`contract` of `subject` · `contract:<id>`), from the stored anchor. */\n subjectAxis?: string\n /** The axes of the records the latest selection named (`contract` of `[keys] contracts contract:<id>`) - `selectionAxesOf`. */\n selectionAxes?: string[]\n}\n\n/** The line of a selection note the model reads - the client's `SELECTION_KEYS_PREFIX` (assistant-selection.ts). */\nexport const SELECTION_KEYS_LINE = '[keys]'\n\n/**\n * The axes of the records the latest selection note names (「AI와 작업」, shared-dock §2-11-1): a list hands its rows as\n * `<axis>:<uuid>`, so the rows' list is read with the tool of that axis. Put in front like an anchored record's - picked\n * on 미수금, the contracts' read was not in front, and the model looked for them among attachments and a dashboard's cards\n * before it opened the right tool (2026-10-25). A note is the server's (`appendSessionNote`, system); a person's words\n * are never read for it.\n */\nexport function selectionAxesOf(rows: { role?: string; content?: string | null }[]): string[] {\n for (let i = rows.length - 1; i >= 0; i--) {\n if (rows[i].role !== 'system') continue\n const line = String(rows[i].content ?? '')\n .split('\\n')\n .find(one => one.startsWith(SELECTION_KEYS_LINE))\n if (!line) continue\n const keys = line.slice(SELECTION_KEYS_LINE.length).trim().split(/\\s+/)\n return [...new Set(keys.map(one => one.match(/^([a-z0-9-]+):/)?.[1]).filter((one): one is string => !!one))]\n }\n return []\n}\n\ntype FrontMode = { toolCategories?: string[]; attachments?: { kinds?: string[] }; entryTargets?: string[]; readTargets?: string[] }\n\n/**\n * The records the open screen is about, among those the mode reads and enters (ADR-0061 decision 9). Nothing new is\n * declared: a read target's screen route is the page that lists it, and a key's last part is the axis of its record\n * (`office.contract` is the record of the axis `contract`) - reading and entering one record share its key.\n */\nexport function screenTargetKeys(mode: FrontMode, screen: TurnScreen, routeOf: (key: string) => string | undefined = key => readTargetOf(key)?.screen?.route): string[] {\n const keys = [...new Set([...(mode.readTargets ?? []), ...(mode.entryTargets ?? [])])]\n return keys.filter(\n key =>\n (!!screen.page && routeOf(key) === screen.page) ||\n (!!screen.subjectAxis && axesOfKey(mode, key).includes(screen.subjectAxis)) ||\n (!!screen.selectionAxes?.length && axesOfKey(mode, key).some(axis => screen.selectionAxes!.includes(axis)))\n )\n}\n\n/** The records a key is about: its last part, and the axes a card joins or a whole answer covers (EntryTarget.axes · DetailRead.axes). */\nfunction axesOfKey(mode: FrontMode, key: string): string[] {\n const card = (mode.entryTargets ?? []).includes(key) ? entryTargetOf(key)?.axes ?? [] : []\n const answer = (mode.readTargets ?? []).includes(key) ? detailReadOf(key)?.axes ?? [] : []\n return [key.split('.').pop()!, ...card, ...answer]\n}\n\n/**\n * The categories a turn puts in front when the mode's tools do not fit in one call: the mode's own, the AI 작업 패널, the\n * people list and the invite card, the attachment reader, and the open screen's records. The rest the model opens with `openTools`.\n */\nexport function modeFrontCategories(mode: FrontMode, screen: TurnScreen = {}): string[] {\n const keys = screenTargetKeys(mode, screen)\n return [\n ...new Set([\n ...(mode.toolCategories ?? []),\n 'assistant-workspace',\n /*\n * Two tools, always in front: 「정연희 초대해 줘」 asked in a conversation whose tools did not fit got the answer that\n * there was no tool to look people up (2026-10-24).\n */\n ...PEOPLE_CATEGORIES,\n ...(mode.attachments?.kinds?.length ? ['chat-attachments'] : []),\n ...keys.filter(key => (mode.readTargets ?? []).includes(key)).map(key => `data-read:${key}`),\n ...keys.filter(key => (mode.entryTargets ?? []).includes(key)).map(key => `data-entry:${key}`),\n ...conversationCategoriesOf()\n .filter(one => (!!screen.page && (one.frontPages ?? []).includes(screen.page)) || (one.frontAxes ?? []).some(axis => axis === screen.subjectAxis || !!screen.selectionAxes?.includes(axis)))\n .map(one => one.name)\n ])\n ]\n}\n\n/** Each screen the mode could be asked from - a page that lists one of its records, a record's axis - for the boot report. */\nexport function modeScreens(mode: FrontMode, routeOf: (key: string) => string | undefined = key => readTargetOf(key)?.screen?.route): TurnScreen[] {\n const keys = [...new Set([...(mode.readTargets ?? []), ...(mode.entryTargets ?? [])])]\n const pages = [...new Set([...keys.map(routeOf), ...conversationCategoriesOf().flatMap(one => one.frontPages ?? [])].filter(Boolean) as string[])]\n const axes = [...new Set(keys.flatMap(key => axesOfKey(mode, key)))]\n return [{}, ...pages.map(page => ({ page })), ...axes.map(subjectAxis => ({ subjectAxis }))]\n}\n"]}
1
+ {"version":3,"file":"mode-categories.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/mode-categories.ts"],"names":[],"mappings":"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","sourcesContent":["/* Read by the resolver and by the boot report, so the report and the conversation open the same tools. */\nimport { entryTargetOf } from '../data-entry/entry-target.js'\nimport { detailReadOf, readTargetOf } from '../data-read/read-target.js'\nimport { HELP_CATEGORY } from '../help-documents/help-keys.js'\nimport { PEOPLE_CATEGORIES } from '../people/people-keys.js'\n/**\n * A category another package opens in every conversation - a dashboard card (ADR-0160 decision 9). It is in reach of\n * every conversation; it is put in front only on the pages it names, and found with `openTools` elsewhere (ADR-0061\n * decision 9). Never in an unattended run: its tools are proposals, and nobody is there to press the card.\n */\nexport interface ConversationCategory {\n name: string\n /** The application routes whose conversations put it in front (`my-dashboard`). */\n frontPages?: string[]\n /**\n * The axes of the records whose selection or anchor puts it in front (`dashboard-card`) - a card picked to change is\n * changed by this category's tools, wherever the dock is open.\n */\n frontAxes?: string[]\n /**\n * What a turn asked from one of its front pages must end in - said in that turn's system prompt only. On 나의 대시보드\n * a request to see a figure is a card; asked 「이번 달 지출 합계」 there, the model answered 「0원」 in words (2026-10-25).\n */\n pageRule?: string\n}\n\nconst conversationCategories = new Map<string, ConversationCategory>()\n\nexport function registerConversationCategory(category: ConversationCategory): void {\n conversationCategories.set(category.name, category)\n}\n\nexport function conversationCategoriesOf(): ConversationCategory[] {\n return [...conversationCategories.values()]\n}\n\nexport function clearConversationCategories(): void {\n conversationCategories.clear()\n}\n\n/** The system prompt of one turn - the mode's, the open record's line, and the rules of the page it is asked from. */\nexport function turnSystemPrompt(modePrompt: string, screen: { page?: string; subjectLine?: string } = {}): string {\n const pageRules = screen.page ? conversationCategoriesOf().filter(one => one.pageRule && (one.frontPages ?? []).includes(screen.page!)).map(one => one.pageRule!) : []\n return [modePrompt, screen.subjectLine, ...pageRules].filter(Boolean).join('\\n\\n')\n}\n\n/**\n * The categories a mode opens: its own, the AI 작업 패널 (design/ai/workspace.md), the attachment reader when it\n * takes files (design/ai/shared-dock.md §9-4), and one propose tool per record it may enter (design/ai/data-entry.md).\n */\nexport function modeToolCategories(mode: { toolCategories?: string[]; attachments?: { kinds?: string[] }; entryTargets?: string[]; readTargets?: string[] }): string[] | undefined {\n const extra = [\n /* Every conversation may put a large result on the AI 작업 패널 (design/ai/workspace.md). */\n 'assistant-workspace',\n /* Every conversation may read who works here and offer to bring one of them in (people/, ADR-0073 decision 3). */\n ...PEOPLE_CATEGORIES,\n /* Every conversation may read the application's help (ADR-0181). */\n HELP_CATEGORY,\n ...(mode.attachments?.kinds?.length ? ['chat-attachments'] : []),\n ...(mode.entryTargets ?? []).map(key => `data-entry:${key}`),\n ...(mode.readTargets ?? []).map(key => `data-read:${key}`),\n ...conversationCategoriesOf().map(one => one.name)\n ]\n return [...new Set([...(mode.toolCategories ?? []), ...extra])]\n}\n\n/**\n * The categories an unattended run of a mode opens: its own and the lists it reads (design/ai/unattended-run.md §3).\n *\n * Not the 입력안 tools - nobody is there to press the card - nor the AI 작업 패널 or the attachment reader: there is no\n * conversation for a card or a file to belong to.\n */\nexport function unattendedToolCategories(mode: { toolCategories?: string[]; readTargets?: string[] }): string[] {\n return [...new Set([...(mode.toolCategories ?? []), ...(mode.readTargets ?? []).map(key => `data-read:${key}`)])]\n}\n\n/** The screen a turn is asked from - the page the dock is on, and the record its conversation is anchored to. */\nexport interface TurnScreen {\n /** The application's route of the open page (`cash-transaction-list`), as the dock sends it. */\n page?: string\n /** The axis of the record the conversation is anchored to (`contract` of `subject` · `contract:<id>`), from the stored anchor. */\n subjectAxis?: string\n /** The axes of the records the latest selection named (`contract` of `[keys] contracts contract:<id>`) - `selectionAxesOf`. */\n selectionAxes?: string[]\n}\n\n/** The line of a selection note the model reads - the client's `SELECTION_KEYS_PREFIX` (assistant-selection.ts). */\nexport const SELECTION_KEYS_LINE = '[keys]'\n\n/**\n * The axes of the records the latest selection note names (「AI와 작업」, shared-dock §2-11-1): a list hands its rows as\n * `<axis>:<uuid>`, so the rows' list is read with the tool of that axis. Put in front like an anchored record's - picked\n * on 미수금, the contracts' read was not in front, and the model looked for them among attachments and a dashboard's cards\n * before it opened the right tool (2026-10-25). A note is the server's (`appendSessionNote`, system); a person's words\n * are never read for it.\n */\nexport function selectionAxesOf(rows: { role?: string; content?: string | null }[]): string[] {\n for (let i = rows.length - 1; i >= 0; i--) {\n if (rows[i].role !== 'system') continue\n const line = String(rows[i].content ?? '')\n .split('\\n')\n .find(one => one.startsWith(SELECTION_KEYS_LINE))\n if (!line) continue\n const keys = line.slice(SELECTION_KEYS_LINE.length).trim().split(/\\s+/)\n return [...new Set(keys.map(one => one.match(/^([a-z0-9-]+):/)?.[1]).filter((one): one is string => !!one))]\n }\n return []\n}\n\ntype FrontMode = { toolCategories?: string[]; attachments?: { kinds?: string[] }; entryTargets?: string[]; readTargets?: string[] }\n\n/**\n * The records the open screen is about, among those the mode reads and enters (ADR-0061 decision 9). Nothing new is\n * declared: a read target's screen route is the page that lists it, and a key's last part is the axis of its record\n * (`office.contract` is the record of the axis `contract`) - reading and entering one record share its key.\n */\nexport function screenTargetKeys(mode: FrontMode, screen: TurnScreen, routeOf: (key: string) => string | undefined = key => readTargetOf(key)?.screen?.route): string[] {\n const keys = [...new Set([...(mode.readTargets ?? []), ...(mode.entryTargets ?? [])])]\n return keys.filter(\n key =>\n (!!screen.page && routeOf(key) === screen.page) ||\n (!!screen.subjectAxis && axesOfKey(mode, key).includes(screen.subjectAxis)) ||\n (!!screen.selectionAxes?.length && axesOfKey(mode, key).some(axis => screen.selectionAxes!.includes(axis)))\n )\n}\n\n/** The records a key is about: its last part, and the axes a card joins or a whole answer covers (EntryTarget.axes · DetailRead.axes). */\nfunction axesOfKey(mode: FrontMode, key: string): string[] {\n const card = (mode.entryTargets ?? []).includes(key) ? entryTargetOf(key)?.axes ?? [] : []\n const answer = (mode.readTargets ?? []).includes(key) ? detailReadOf(key)?.axes ?? [] : []\n return [key.split('.').pop()!, ...card, ...answer]\n}\n\n/**\n * The categories a turn puts in front when the mode's tools do not fit in one call: the mode's own, the AI 작업 패널, the\n * people list and the invite card, the attachment reader, and the open screen's records. The rest the model opens with `openTools`.\n */\nexport function modeFrontCategories(mode: FrontMode, screen: TurnScreen = {}): string[] {\n const keys = screenTargetKeys(mode, screen)\n return [\n ...new Set([\n ...(mode.toolCategories ?? []),\n 'assistant-workspace',\n /*\n * Two tools, always in front: 「정연희 초대해 줘」 asked in a conversation whose tools did not fit got the answer that\n * there was no tool to look people up (2026-10-24).\n */\n ...PEOPLE_CATEGORIES,\n /* Two tools, always in front: 「지출결의는 어디서 올려?」 is asked on any screen (ADR-0181). */\n HELP_CATEGORY,\n ...(mode.attachments?.kinds?.length ? ['chat-attachments'] : []),\n ...keys.filter(key => (mode.readTargets ?? []).includes(key)).map(key => `data-read:${key}`),\n ...keys.filter(key => (mode.entryTargets ?? []).includes(key)).map(key => `data-entry:${key}`),\n ...conversationCategoriesOf()\n .filter(one => (!!screen.page && (one.frontPages ?? []).includes(screen.page)) || (one.frontAxes ?? []).some(axis => axis === screen.subjectAxis || !!screen.selectionAxes?.includes(axis)))\n .map(one => one.name)\n ])\n ]\n}\n\n/** Each screen the mode could be asked from - a page that lists one of its records, a record's axis - for the boot report. */\nexport function modeScreens(mode: FrontMode, routeOf: (key: string) => string | undefined = key => readTargetOf(key)?.screen?.route): TurnScreen[] {\n const keys = [...new Set([...(mode.readTargets ?? []), ...(mode.entryTargets ?? [])])]\n const pages = [...new Set([...keys.map(routeOf), ...conversationCategoriesOf().flatMap(one => one.frontPages ?? [])].filter(Boolean) as string[])]\n const axes = [...new Set(keys.flatMap(key => axesOfKey(mode, key)))]\n return [{}, ...pages.map(page => ({ page })), ...axes.map(subjectAxis => ({ subjectAxis }))]\n}\n"]}
@@ -77,7 +77,7 @@ tslib_1.__decorate([
77
77
  tslib_1.__metadata("design:type", String)
78
78
  ], ChatMessage.prototype, "sessionId", void 0);
79
79
  tslib_1.__decorate([
80
- (0, typeorm_1.Column)({ type: 'varchar', length: 16 }),
80
+ (0, typeorm_1.Column)({ length: 16 }),
81
81
  (0, type_graphql_1.Field)({ description: "'user' | 'assistant' | 'system'" }),
82
82
  tslib_1.__metadata("design:type", String)
83
83
  ], ChatMessage.prototype, "role", void 0);
@@ -96,7 +96,7 @@ tslib_1.__decorate([
96
96
  tslib_1.__metadata("design:type", String)
97
97
  ], ChatMessage.prototype, "content", void 0);
98
98
  tslib_1.__decorate([
99
- (0, typeorm_1.Column)({ type: 'varchar', length: 16, nullable: true }),
99
+ (0, typeorm_1.Column)({ length: 16, nullable: true }),
100
100
  (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." }),
101
101
  tslib_1.__metadata("design:type", String)
102
102
  ], ChatMessage.prototype, "addressee", void 0);
@@ -173,7 +173,7 @@ tslib_1.__decorate([
173
173
  tslib_1.__metadata("design:paramtypes", [])
174
174
  ], ChatMessage.prototype, "attachmentsJson", null);
175
175
  tslib_1.__decorate([
176
- (0, typeorm_1.Column)({ type: 'varchar', length: 1024, nullable: true }),
176
+ (0, typeorm_1.Column)({ type: 'text', nullable: true }),
177
177
  tslib_1.__metadata("design:type", String)
178
178
  ], ChatMessage.prototype, "groundingWarnings", void 0);
179
179
  tslib_1.__decorate([
@@ -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 /**\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[] (시간순 trace). result 는 값이 아니라 모양(resultShapeOf — 크기 · 건수 · 키 이름 · 오류 코드,\n * ADR-0061 결정 8). 급여 · 지분 값이 대화 행 · 백업 · 내보내기로 복제되지 않는다.\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 * The proposals this answer made - JSON-stringified, like `toolUsages` (ADR-0097 Q-D4 ④).\n *\n * A card lived only in the mutation's answer, so a refresh took it away, and with it the one place the\n * person could see that a proposal was requested and never answered. Stored, the card comes back with\n * its state read from the conversation's rows. Cleared with the body when the row is redacted\n * (`message-doors.ts` VALUE_CARRYING) - a proposal carries the same values as the answer.\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 proposals?: string\n\n @Field(() => GraphQLJSON, { nullable: true, description: 'Proposals this answer made - the cards the panel draws. Only on assistant messages.' })\n get proposalsJson(): any {\n if (!this.proposals) return null\n try {\n return JSON.parse(this.proposals)\n } catch {\n return null\n }\n }\n\n /**\n * 접지 경고 — 이 답이 언급했으나 근거(프롬프트·보드 문맥·이력·도구 결과)에 없던 식별자 목록.\n *\n * **왜 영속하는가**: 도크의 협의는 여러 사람이 며칠에 걸쳐 읽는다. 경고가 그 자리에서만\n * 보이고 새로 고치면 사라지면, 나중에 읽는 사람은 검증되지 않은 문장을 검증된 것으로 읽는다.\n * 다시 계산할 수도 없다 — 근거였던 도구 결과 원문은 남지 않고 압축본만 남기 때문이다.\n *\n * 형태: string[] 를 JSON-stringify. 비었으면 null(= 접지 정상).\n */\n /**\n * The files sent with this turn - `[{ id, name, mimetype, size }]` as JSON (design/ai/shared-dock.md §9-2).\n * The files themselves live in attachment-base under the conversation and are opened only through it.\n */\n @Column({ type: 'text', nullable: true })\n attachments?: string\n\n @Field(() => GraphQLJSON, { nullable: true, description: 'Files sent with this turn: [{ id, name, mimetype, size }].' })\n get attachmentsJson(): any {\n if (!this.attachments) return null\n try {\n return JSON.parse(this.attachments)\n } catch {\n return null\n }\n }\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":"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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({ 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({ 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[] (시간순 trace). result 는 값이 아니라 모양(resultShapeOf — 크기 · 건수 · 키 이름 · 오류 코드,\n * ADR-0061 결정 8). 급여 · 지분 값이 대화 행 · 백업 · 내보내기로 복제되지 않는다.\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 * The proposals this answer made - JSON-stringified, like `toolUsages` (ADR-0097 Q-D4 ④).\n *\n * A card lived only in the mutation's answer, so a refresh took it away, and with it the one place the\n * person could see that a proposal was requested and never answered. Stored, the card comes back with\n * its state read from the conversation's rows. Cleared with the body when the row is redacted\n * (`message-doors.ts` VALUE_CARRYING) - a proposal carries the same values as the answer.\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 proposals?: string\n\n @Field(() => GraphQLJSON, { nullable: true, description: 'Proposals this answer made - the cards the panel draws. Only on assistant messages.' })\n get proposalsJson(): any {\n if (!this.proposals) return null\n try {\n return JSON.parse(this.proposals)\n } catch {\n return null\n }\n }\n\n /**\n * 접지 경고 — 이 답이 언급했으나 근거(프롬프트·보드 문맥·이력·도구 결과)에 없던 식별자 목록.\n *\n * **왜 영속하는가**: 도크의 협의는 여러 사람이 며칠에 걸쳐 읽는다. 경고가 그 자리에서만\n * 보이고 새로 고치면 사라지면, 나중에 읽는 사람은 검증되지 않은 문장을 검증된 것으로 읽는다.\n * 다시 계산할 수도 없다 — 근거였던 도구 결과 원문은 남지 않고 압축본만 남기 때문이다.\n *\n * 형태: string[] 를 JSON-stringify. 비었으면 null(= 접지 정상).\n */\n /**\n * The files sent with this turn - `[{ id, name, mimetype, size }]` as JSON (design/ai/shared-dock.md §9-2).\n * The files themselves live in attachment-base under the conversation and are opened only through it.\n */\n @Column({ type: 'text', nullable: true })\n attachments?: string\n\n @Field(() => GraphQLJSON, { nullable: true, description: 'Files sent with this turn: [{ id, name, mimetype, size }].' })\n get attachmentsJson(): any {\n if (!this.attachments) return null\n try {\n return JSON.parse(this.attachments)\n } catch {\n return null\n }\n }\n\n @Column({ type: 'text', 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"]}
@@ -104,12 +104,12 @@ tslib_1.__decorate([
104
104
  )
105
105
  ], ChatSession.prototype, "lastMessageAt", void 0);
106
106
  tslib_1.__decorate([
107
- (0, typeorm_1.Column)({ type: 'varchar', length: 64, nullable: true }),
107
+ (0, typeorm_1.Column)({ length: 64, nullable: true }),
108
108
  (0, type_graphql_1.Field)({ nullable: true, description: "Which conversation this session is — the registered mode kind. Written by the server, never sent by the client." }),
109
109
  tslib_1.__metadata("design:type", String)
110
110
  ], ChatSession.prototype, "kind", void 0);
111
111
  tslib_1.__decorate([
112
- (0, typeorm_1.Column)({ type: 'varchar', length: 16, default: 'open' }),
112
+ (0, typeorm_1.Column)({ length: 16, default: 'open' }),
113
113
  (0, type_graphql_1.Field)({ description: "Who may read this conversation: 'open' (anyone in the domain), 'group' (the owner and the people the owner brought in) or 'private' (the owner alone)." }),
114
114
  tslib_1.__metadata("design:type", String)
115
115
  ], ChatSession.prototype, "visibility", void 0);
@@ -1 +1 @@
1
- {"version":3,"file":"chat-session.js","sourceRoot":"","sources":["../../../server/service/chat-session/chat-session.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;;;;;;;;GAcG;AACH,qCAWgB;AAChB,+CAAoD;AAEpD,iDAA8C;AAC9C,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;AAW7B,IAAM,WAAW,GAAjB,MAAM,WAAW;CAoMvB,CAAA;AApMY,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;AAIpB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,CAAC;IACzB,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCACjC,cAAM;2CAAA;AAGf;IADC,IAAA,oBAAU,EAAC,CAAC,OAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;;6CACpC;AAYjB;IANC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC;QACL,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,gMAAgM;KACnM,CAAC;;4CACc;AAehB;IANC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC;QACL,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,sQAAsQ;KACzQ,CAAC;;+CACiB;AAKnB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gEAAgE,EAAE,CAAC;;6CACxF;AAUjB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IACvC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,+DAA+D,EAAE,CAAC;;yCAC3F;AAIb;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,CAAC;IACvB,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gCAAgC,EAAE,CAAC;sCAC7E,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,CAAC;IACvB,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,qCAAqC,EAAE,CAAC;sCAClF,gBAAI;4CAAA;AAGd;IADC,IAAA,oBAAU,EAAC,CAAC,OAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;;8CACpC;AAYlB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,wKAAwK,EAAE,CAAC;sCACjM,IAAI;IAEpB;;;;;;;;;;OAUG;;kDAZiB;AAepB;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,iHAAiH,EAAE,CAAC;;yCAC7I;AAoBb;IAFC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IACxD,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,wJAAwJ,EAAE,CAAC;;+CAC9J;AAKnB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IACvC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gGAAgG,EAAE,CAAC;;uDAC9G;AAW3B;IAFC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,cAAc,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAChD,IAAA,oBAAK,EAAC,GAAG,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,yGAAyG,EAAE,CAAC;;qDACvI;AAe3B;IAbC,IAAA,gBAAM,EAAC;QACN,QAAQ,EAAE,IAAI;QACd,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,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,0DAA0D,EAAE,CAAC;;gDAC/E;AAWpB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,6HAA6H,EAAE,CAAC;;yDACzI;AAK7B;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;+CACP;AAKnB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,2CAA2C,EAAE,CAAC;;4CACpE;AAIhB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCACd,IAAI;8CAAA;AAIhB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCACd,IAAI;8CAAA;AAGhB;IADC,IAAA,0BAAgB,GAAE;sCACP,IAAI;IAEhB,oDAAoD;;8CAFpC;AAIhB;IADC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,6BAAW,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;;6CACnC;sBApLb,WAAW;IATvB,IAAA,gBAAM,GAAE;IACT,qEAAqE;IACrE,kDAAkD;;IACjD,IAAA,eAAK,EAAC,mBAAmB,EAAE,CAAC,OAAoB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IACxF,uEAAuE;;IACtE,IAAA,eAAK,EAAC,mBAAmB,EAAE,CAAC,OAAoB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5G,IAAA,yBAAU,EAAC;QACV,WAAW,EAAE,2HAA2H;KACzI,CAAC;GACW,WAAW,CAoMvB","sourcesContent":["/**\n * ChatSession — Board 와 결합되는 AI 협력 세션.\n *\n * 한 보드에 여러 세션 가능 — DB 레벨 unique 제약 없음.\n *\n * 향후 폭증할 세션 분화 차원 (예: thread / 사용자별 / 컨텍스트별 design vs ops /\n * archived / forked from clone / agent별) 을 미리 차단하지 않기 위해 구조 자체는 1:N\n * 으로 열어둠. 단일 세션 정책 (현재 UX) 은 app 레벨 idempotent 로직 — `startBoardAISession`\n * 이 boardId 매칭 시 기존 세션 반환, `chatSessionByBoard` 가 첫 매칭 반환.\n *\n * 미래에 다중 세션 활성화 시 discriminator 컬럼 (kind / name / threadId / creatorId 등)\n * 을 그때 추가. 마이그레이션 부담 최소화 — 현재는 unique 만 풀어 자리 마련.\n *\n * board-service 의 Board 가 owning side 가 될 예정 (ChatSession FK).\n */\nimport {\n Column,\n CreateDateColumn,\n DeleteDateColumn,\n Entity,\n Index,\n ManyToOne,\n OneToMany,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\nimport { config } from '@things-factory/env'\n\nimport { ChatMessage } from '../chat-message/chat-message.js'\n\nconst ORMCONFIG = config.get('ormconfig', {})\nconst DATABASE_TYPE = ORMCONFIG.type\n\n@Entity()\n// 인덱스는 조회 성능용으로 유지 (chatSessionByBoard 가 [domain, boardId] 로 자주 쿼리).\n// unique 제약은 의도적으로 제거 — 한 보드에 여러 세션을 미래에 허용하기 위함.\n@Index('ix_chat_session_1', (session: ChatSession) => [session.domain, session.boardId])\n// 앵커 조회용 — chatSessionsByAnchor 가 [domain, anchorType, anchorId] 로 질의.\n@Index('ix_chat_session_2', (session: ChatSession) => [session.domain, session.anchorType, session.anchorId])\n@ObjectType({\n description: 'One AI conversation, anchored to what it is about. An anchor may have several — threads, one per person, one per context.'\n})\nexport class ChatSession {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { nullable: true })\n readonly id?: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true })\n domain?: Domain\n\n @RelationId((session: ChatSession) => session.domain)\n domainId?: string\n\n /** Board.id 와 연결. Board 가 owning side 가 될 예정. 1:N — 한 보드에 여러 세션 가능.\n *\n * **앵커 일반화 이후에도 유지되는 하위호환 컬럼**이다(제거하지 말 것). 앵커가 board 인 세션은\n * anchorId 와 함께 이 컬럼에도 같은 값을 쓴다(dual-write) — 기존 조회/인덱스/소비자가 그대로 동작한다. */\n @Column({ nullable: true })\n @Field({\n nullable: true,\n description:\n 'Connected Board id. Multiple sessions per board allowed (future: threads / per-user / contexts). Kept for backward compatibility — board-anchored sessions dual-write this alongside anchorId.'\n })\n boardId?: string\n\n /**\n * 대화가 매달린 대상의 **종류**. 세션은 보드에만 매달리지 않는다 —\n * 저작 대화는 보드에, 운영 대화는 공간(사이트)에 매달린다. 앵커가 대화의 성격과 이력 경계를 정한다.\n *\n * 'board' | 'space' | 'instance' | 'none'. 미설정(기존 데이터)은 boardId 가 있으면 board 로 읽는다 —\n * 백필 마이그레이션 없이 lazy 하위호환(컬럼 추가만).\n */\n @Column({ nullable: true })\n @Field({\n nullable: true,\n description:\n \"What this conversation is anchored to: 'board' | 'figure' (authoring a Figure) | 'application' | 'space' | 'instance' | 'domain' | 'subject' (one subject in a 360, '<axis>:<itemId>') | 'none'. Absent on legacy rows — treated as 'board' when boardId is present.\"\n })\n anchorType?: string\n\n /** 앵커 대상의 id(보드 id · 공간 id · 인스턴스 id). anchorType 과 함께 의미를 갖는다. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Id of the anchored target (board id / space id / instance id).' })\n anchorId?: string\n\n /**\n * 세션의 사용자 정의 이름 (탭 레이블 / 세션 식별 표시용).\n *\n * nullable — 미설정 시 UI 가 fallback 라벨 (\"세션 N\" 또는 createdAt 기반) 사용. 사용자가\n * 다중 세션 운영 시 의미 부여 (예: \"초기 설계\", \"운영 시뮬레이션\", \"사용자A 의 사적 세션\").\n */\n @Column({ nullable: true, length: 200 })\n @Field({ nullable: true, description: 'User-given name of this session (tab label / identification).' })\n name?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'User who created this session.' })\n creator?: User\n\n @RelationId((session: ChatSession) => session.creator)\n creatorId?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'User who last updated this session.' })\n updater?: User\n\n @RelationId((session: ChatSession) => session.updater)\n updaterId?: string\n\n /** 토큰 절감용 — 오래된 메시지를 LLM 으로 압축한 요약. Phase 2 에서 작성. */\n /**\n * 마지막 메시지 시각 — **비정규화**. 목록 정렬(최근 활동순)과 안 읽음 판정의 기준.\n *\n * 왜 비정규화인가: 세션마다 메시지를 집계하면(MAX/COUNT + group by) 목록 한 번에 집계 쿼리가 붙고,\n * 이 프레임워크는 5개 DB 드라이버를 지원해야 한다. 세션 컬럼 하나로 두면 정렬·비교가 값싸게 끝나고\n * 드라이버 차이가 없다. 메시지를 저장할 때 함께 갱신한다(activityPatch).\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Timestamp of the last message in this session (denormalized for list ordering and unread comparison — avoids per-session aggregation across the supported DB drivers).' })\n lastMessageAt?: Date\n\n /**\n * 어느 대화인가 — `twin.board` · `figure.catalog` 같은 모드의 kind (ADR-0072 결정 3).\n *\n * **서버가 적는다.** 클라이언트가 보내는 값이 아니다 — 등록된 모드에서 나온 것만 적힌다(`addressee`\n * 와 같은 결). 한 대상 위에 대화가 둘 설 수 있게 되면서(A17), 세션이 자기가 어느 대화인지 알아야\n * 한다. 몰랐을 때 화면에서 이렇게 나왔다: 모드를 바꿔도 같은 세션이 그대로 남아, 「도형 찾기」로\n * 바꾼 뒤 보낸 말이 앞의 고치기 대화에 붙었다(2026-09-18, Figure 레인 :3300).\n *\n * 값이 없는 세션은 이 칸이 서기 전에 난 것이다. 다음 턴에 요청한 kind 로 한 번 찍고, 찍힌 뒤에는\n * 「요청한 모드와 다르면 거절」 하나만 남는다. 마이그레이션은 만들지 않는다.\n */\n @Column({ type: 'varchar', length: 64, nullable: true })\n @Field({ nullable: true, description: \"Which conversation this session is — the registered mode kind. Written by the server, never sent by the client.\" })\n kind?: string\n\n /**\n * 이 대화를 도메인 안 누구나 읽는가 — ADR-0073 판정 2.\n *\n * `open` 도메인 안이면 누구나 들어와 읽고 말한다. 초대 절차는 없다.\n * `group` owner 가 들인 사람과 owner 만 읽고 말한다(2026-10-24, ADR-0073 결정 3).\n * `private` owner 혼자. 아무도 앉히지 않고, 그룹을 비공개로 바꾸면 들인 사람이 나간다.\n *\n * 새 세션은 **모두** `private` 으로 시작한다(2026-09-30 — `initialVisibility`). 앵커는 무엇에 대한\n * 이야기인지를 말할 뿐 누구에게 보이는지를 정하지 않는다. 칸의 기본값 `open` 은 칸이 서기 전의 옛 행을\n * 그날까지의 규칙대로 읽기 위한 것이고, 새 행은 늘 값을 적는다.\n *\n * owner 가 양방향으로 바꿀 수 있다. 공개로 바꾸기 전에 화면이 한 줄을 먼저 보이고, 공개 대화는 도크가\n * 다르게 그린다.\n *\n * open 이어도 답 행의 문 판정(ADR-0073 판정 1)은 그대로다 — 방에 들어온 것과 값을 읽는 것은 다르다.\n */\n @Column({ type: 'varchar', length: 16, default: 'open' })\n @Field({ description: \"Who may read this conversation: 'open' (anyone in the domain), 'group' (the owner and the people the owner brought in) or 'private' (the owner alone).\" })\n visibility!: string\n\n /** 목록에 보여줄 마지막 메시지 미리보기 — 목록에서 메시지 테이블을 다시 읽지 않기 위해 짧게 저장. */\n @Column({ nullable: true, length: 200 })\n @Field({ nullable: true, description: 'Short preview of the last message, stored so the session list does not need to query messages.' })\n lastMessagePreview?: string\n\n /**\n * 그 미리보기가 지나온 문 — ADR-0073 판정 1.\n *\n * 목록은 답을 **다시 읽는 두 번째 길**이다. 메시지 화면에서만 가리면 같은 문장이 목록에 그대로 뜬다.\n * 문이 둘이고 한쪽이 느슨하면 느슨한 쪽이 이긴다(ADR-0061 결정 1). 미리보기를 저장할 때 그 행의 문을\n * 함께 옮겨 적고, 목록도 같은 관문을 지난다.\n */\n @Column({ type: 'simple-array', nullable: true })\n @Field(() => [String], { nullable: true, description: 'Doors of the message the preview came from — the session list passes the same gate as the conversation.' })\n lastMessageDoors?: string[]\n\n @Column({\n nullable: true,\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({ nullable: true, description: 'Compressed summary of older messages (for token saving).' })\n lastSummary?: string\n\n /**\n * `lastSummary` 가 **어디까지 덮는지** — 이 메시지까지 요약에 반영됐다.\n *\n * 이게 없으면 누적 요약을 할 수 없다: 매 턴 전체를 다시 요약하거나(비용) 이미 요약한 구간을\n * 또 이어 붙인다(중복). 이력에서 이 id 를 못 찾으면(삭제·편집) 덮은 범위를 알 수 없으므로\n * 전부 미반영으로 보고 다시 요약한다 — 빠뜨리는 쪽보다 안전하다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Id of the last message folded into lastSummary — enables incremental summarization instead of re-reading the whole history.' })\n summaryUpToMessageId?: string\n\n /** 사용 모델 식별 — 'anthropic:claude-sonnet-4-6' 등 */\n @Column({ nullable: true })\n @Field({ nullable: true })\n aiClientId?: string\n\n /** The agent that answered the last turn. Every turn's agent is in the AI usage rows. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'The AI agent that answered the last turn.' })\n agentId?: string\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n\n @DeleteDateColumn()\n deletedAt?: Date\n\n // ── 관계 (TypeORM 만, GraphQL 노출은 별도 query 통해 페이징) ──\n @OneToMany(type => ChatMessage, message => message.session)\n messages?: ChatMessage[]\n\n /*\n * ── 보드 패치 관계를 끊었다 (2026-09-10) ──────────────────────────────────\n * 여기 `@OneToMany(PatchEntry)` 가 있었다. 그래서 **대화 세션이 보드 편집기를 알아야** 했고,\n * 세션 계열을 중립 패키지로 옮기려는 순간 `ai-assistant → board-ai → ai-assistant` 순환이\n * 된다.\n *\n * 끊어도 잃는 것이 없다 — 이 관계를 **자료로 읽는 자리가 하나도 없었다**(전 저장소에서\n * `session.patches` 를 읽는 코드 0 건). 유일한 쓰임이 `PatchEntry` 쪽의 역방향 선언 한 줄이었고,\n * TypeORM 은 역방향 없이도 `@ManyToOne` 을 만든다. 패치는 `sessionId` 로 찾는다\n * (`chatPatches` 가 이미 그렇게 한다).\n *\n * 방향은 남는다: **보드 패치가 세션을 안다. 세션은 보드 패치를 모른다.** 종속이 한쪽이면\n * 중립 쪽이 옮겨질 수 있다.\n */\n}\n"]}
1
+ {"version":3,"file":"chat-session.js","sourceRoot":"","sources":["../../../server/service/chat-session/chat-session.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;;;;;;;;GAcG;AACH,qCAWgB;AAChB,+CAAoD;AAEpD,iDAA8C;AAC9C,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;AAW7B,IAAM,WAAW,GAAjB,MAAM,WAAW;CAoMvB,CAAA;AApMY,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;AAIpB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,CAAC;IACzB,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCACjC,cAAM;2CAAA;AAGf;IADC,IAAA,oBAAU,EAAC,CAAC,OAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;;6CACpC;AAYjB;IANC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC;QACL,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,gMAAgM;KACnM,CAAC;;4CACc;AAehB;IANC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC;QACL,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,sQAAsQ;KACzQ,CAAC;;+CACiB;AAKnB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gEAAgE,EAAE,CAAC;;6CACxF;AAUjB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IACvC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,+DAA+D,EAAE,CAAC;;yCAC3F;AAIb;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,CAAC;IACvB,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gCAAgC,EAAE,CAAC;sCAC7E,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,CAAC;IACvB,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,qCAAqC,EAAE,CAAC;sCAClF,gBAAI;4CAAA;AAGd;IADC,IAAA,oBAAU,EAAC,CAAC,OAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;;8CACpC;AAYlB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,wKAAwK,EAAE,CAAC;sCACjM,IAAI;IAEpB;;;;;;;;;;OAUG;;kDAZiB;AAepB;IAFC,IAAA,gBAAM,EAAC,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACtC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,iHAAiH,EAAE,CAAC;;yCAC7I;AAoBb;IAFC,IAAA,gBAAM,EAAC,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IACvC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,wJAAwJ,EAAE,CAAC;;+CAC9J;AAKnB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IACvC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gGAAgG,EAAE,CAAC;;uDAC9G;AAW3B;IAFC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,cAAc,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAChD,IAAA,oBAAK,EAAC,GAAG,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,yGAAyG,EAAE,CAAC;;qDACvI;AAe3B;IAbC,IAAA,gBAAM,EAAC;QACN,QAAQ,EAAE,IAAI;QACd,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,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,0DAA0D,EAAE,CAAC;;gDAC/E;AAWpB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,6HAA6H,EAAE,CAAC;;yDACzI;AAK7B;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;+CACP;AAKnB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,2CAA2C,EAAE,CAAC;;4CACpE;AAIhB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCACd,IAAI;8CAAA;AAIhB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCACd,IAAI;8CAAA;AAGhB;IADC,IAAA,0BAAgB,GAAE;sCACP,IAAI;IAEhB,oDAAoD;;8CAFpC;AAIhB;IADC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,6BAAW,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;;6CACnC;sBApLb,WAAW;IATvB,IAAA,gBAAM,GAAE;IACT,qEAAqE;IACrE,kDAAkD;;IACjD,IAAA,eAAK,EAAC,mBAAmB,EAAE,CAAC,OAAoB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IACxF,uEAAuE;;IACtE,IAAA,eAAK,EAAC,mBAAmB,EAAE,CAAC,OAAoB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5G,IAAA,yBAAU,EAAC;QACV,WAAW,EAAE,2HAA2H;KACzI,CAAC;GACW,WAAW,CAoMvB","sourcesContent":["/**\n * ChatSession — Board 와 결합되는 AI 협력 세션.\n *\n * 한 보드에 여러 세션 가능 — DB 레벨 unique 제약 없음.\n *\n * 향후 폭증할 세션 분화 차원 (예: thread / 사용자별 / 컨텍스트별 design vs ops /\n * archived / forked from clone / agent별) 을 미리 차단하지 않기 위해 구조 자체는 1:N\n * 으로 열어둠. 단일 세션 정책 (현재 UX) 은 app 레벨 idempotent 로직 — `startBoardAISession`\n * 이 boardId 매칭 시 기존 세션 반환, `chatSessionByBoard` 가 첫 매칭 반환.\n *\n * 미래에 다중 세션 활성화 시 discriminator 컬럼 (kind / name / threadId / creatorId 등)\n * 을 그때 추가. 마이그레이션 부담 최소화 — 현재는 unique 만 풀어 자리 마련.\n *\n * board-service 의 Board 가 owning side 가 될 예정 (ChatSession FK).\n */\nimport {\n Column,\n CreateDateColumn,\n DeleteDateColumn,\n Entity,\n Index,\n ManyToOne,\n OneToMany,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\nimport { config } from '@things-factory/env'\n\nimport { ChatMessage } from '../chat-message/chat-message.js'\n\nconst ORMCONFIG = config.get('ormconfig', {})\nconst DATABASE_TYPE = ORMCONFIG.type\n\n@Entity()\n// 인덱스는 조회 성능용으로 유지 (chatSessionByBoard 가 [domain, boardId] 로 자주 쿼리).\n// unique 제약은 의도적으로 제거 — 한 보드에 여러 세션을 미래에 허용하기 위함.\n@Index('ix_chat_session_1', (session: ChatSession) => [session.domain, session.boardId])\n// 앵커 조회용 — chatSessionsByAnchor 가 [domain, anchorType, anchorId] 로 질의.\n@Index('ix_chat_session_2', (session: ChatSession) => [session.domain, session.anchorType, session.anchorId])\n@ObjectType({\n description: 'One AI conversation, anchored to what it is about. An anchor may have several — threads, one per person, one per context.'\n})\nexport class ChatSession {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { nullable: true })\n readonly id?: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true })\n domain?: Domain\n\n @RelationId((session: ChatSession) => session.domain)\n domainId?: string\n\n /** Board.id 와 연결. Board 가 owning side 가 될 예정. 1:N — 한 보드에 여러 세션 가능.\n *\n * **앵커 일반화 이후에도 유지되는 하위호환 컬럼**이다(제거하지 말 것). 앵커가 board 인 세션은\n * anchorId 와 함께 이 컬럼에도 같은 값을 쓴다(dual-write) — 기존 조회/인덱스/소비자가 그대로 동작한다. */\n @Column({ nullable: true })\n @Field({\n nullable: true,\n description:\n 'Connected Board id. Multiple sessions per board allowed (future: threads / per-user / contexts). Kept for backward compatibility — board-anchored sessions dual-write this alongside anchorId.'\n })\n boardId?: string\n\n /**\n * 대화가 매달린 대상의 **종류**. 세션은 보드에만 매달리지 않는다 —\n * 저작 대화는 보드에, 운영 대화는 공간(사이트)에 매달린다. 앵커가 대화의 성격과 이력 경계를 정한다.\n *\n * 'board' | 'space' | 'instance' | 'none'. 미설정(기존 데이터)은 boardId 가 있으면 board 로 읽는다 —\n * 백필 마이그레이션 없이 lazy 하위호환(컬럼 추가만).\n */\n @Column({ nullable: true })\n @Field({\n nullable: true,\n description:\n \"What this conversation is anchored to: 'board' | 'figure' (authoring a Figure) | 'application' | 'space' | 'instance' | 'domain' | 'subject' (one subject in a 360, '<axis>:<itemId>') | 'none'. Absent on legacy rows — treated as 'board' when boardId is present.\"\n })\n anchorType?: string\n\n /** 앵커 대상의 id(보드 id · 공간 id · 인스턴스 id). anchorType 과 함께 의미를 갖는다. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Id of the anchored target (board id / space id / instance id).' })\n anchorId?: string\n\n /**\n * 세션의 사용자 정의 이름 (탭 레이블 / 세션 식별 표시용).\n *\n * nullable — 미설정 시 UI 가 fallback 라벨 (\"세션 N\" 또는 createdAt 기반) 사용. 사용자가\n * 다중 세션 운영 시 의미 부여 (예: \"초기 설계\", \"운영 시뮬레이션\", \"사용자A 의 사적 세션\").\n */\n @Column({ nullable: true, length: 200 })\n @Field({ nullable: true, description: 'User-given name of this session (tab label / identification).' })\n name?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'User who created this session.' })\n creator?: User\n\n @RelationId((session: ChatSession) => session.creator)\n creatorId?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'User who last updated this session.' })\n updater?: User\n\n @RelationId((session: ChatSession) => session.updater)\n updaterId?: string\n\n /** 토큰 절감용 — 오래된 메시지를 LLM 으로 압축한 요약. Phase 2 에서 작성. */\n /**\n * 마지막 메시지 시각 — **비정규화**. 목록 정렬(최근 활동순)과 안 읽음 판정의 기준.\n *\n * 왜 비정규화인가: 세션마다 메시지를 집계하면(MAX/COUNT + group by) 목록 한 번에 집계 쿼리가 붙고,\n * 이 프레임워크는 5개 DB 드라이버를 지원해야 한다. 세션 컬럼 하나로 두면 정렬·비교가 값싸게 끝나고\n * 드라이버 차이가 없다. 메시지를 저장할 때 함께 갱신한다(activityPatch).\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Timestamp of the last message in this session (denormalized for list ordering and unread comparison — avoids per-session aggregation across the supported DB drivers).' })\n lastMessageAt?: Date\n\n /**\n * 어느 대화인가 — `twin.board` · `figure.catalog` 같은 모드의 kind (ADR-0072 결정 3).\n *\n * **서버가 적는다.** 클라이언트가 보내는 값이 아니다 — 등록된 모드에서 나온 것만 적힌다(`addressee`\n * 와 같은 결). 한 대상 위에 대화가 둘 설 수 있게 되면서(A17), 세션이 자기가 어느 대화인지 알아야\n * 한다. 몰랐을 때 화면에서 이렇게 나왔다: 모드를 바꿔도 같은 세션이 그대로 남아, 「도형 찾기」로\n * 바꾼 뒤 보낸 말이 앞의 고치기 대화에 붙었다(2026-09-18, Figure 레인 :3300).\n *\n * 값이 없는 세션은 이 칸이 서기 전에 난 것이다. 다음 턴에 요청한 kind 로 한 번 찍고, 찍힌 뒤에는\n * 「요청한 모드와 다르면 거절」 하나만 남는다. 마이그레이션은 만들지 않는다.\n */\n @Column({ length: 64, nullable: true })\n @Field({ nullable: true, description: \"Which conversation this session is — the registered mode kind. Written by the server, never sent by the client.\" })\n kind?: string\n\n /**\n * 이 대화를 도메인 안 누구나 읽는가 — ADR-0073 판정 2.\n *\n * `open` 도메인 안이면 누구나 들어와 읽고 말한다. 초대 절차는 없다.\n * `group` owner 가 들인 사람과 owner 만 읽고 말한다(2026-10-24, ADR-0073 결정 3).\n * `private` owner 혼자. 아무도 앉히지 않고, 그룹을 비공개로 바꾸면 들인 사람이 나간다.\n *\n * 새 세션은 **모두** `private` 으로 시작한다(2026-09-30 — `initialVisibility`). 앵커는 무엇에 대한\n * 이야기인지를 말할 뿐 누구에게 보이는지를 정하지 않는다. 칸의 기본값 `open` 은 칸이 서기 전의 옛 행을\n * 그날까지의 규칙대로 읽기 위한 것이고, 새 행은 늘 값을 적는다.\n *\n * owner 가 양방향으로 바꿀 수 있다. 공개로 바꾸기 전에 화면이 한 줄을 먼저 보이고, 공개 대화는 도크가\n * 다르게 그린다.\n *\n * open 이어도 답 행의 문 판정(ADR-0073 판정 1)은 그대로다 — 방에 들어온 것과 값을 읽는 것은 다르다.\n */\n @Column({ length: 16, default: 'open' })\n @Field({ description: \"Who may read this conversation: 'open' (anyone in the domain), 'group' (the owner and the people the owner brought in) or 'private' (the owner alone).\" })\n visibility!: string\n\n /** 목록에 보여줄 마지막 메시지 미리보기 — 목록에서 메시지 테이블을 다시 읽지 않기 위해 짧게 저장. */\n @Column({ nullable: true, length: 200 })\n @Field({ nullable: true, description: 'Short preview of the last message, stored so the session list does not need to query messages.' })\n lastMessagePreview?: string\n\n /**\n * 그 미리보기가 지나온 문 — ADR-0073 판정 1.\n *\n * 목록은 답을 **다시 읽는 두 번째 길**이다. 메시지 화면에서만 가리면 같은 문장이 목록에 그대로 뜬다.\n * 문이 둘이고 한쪽이 느슨하면 느슨한 쪽이 이긴다(ADR-0061 결정 1). 미리보기를 저장할 때 그 행의 문을\n * 함께 옮겨 적고, 목록도 같은 관문을 지난다.\n */\n @Column({ type: 'simple-array', nullable: true })\n @Field(() => [String], { nullable: true, description: 'Doors of the message the preview came from — the session list passes the same gate as the conversation.' })\n lastMessageDoors?: string[]\n\n @Column({\n nullable: true,\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({ nullable: true, description: 'Compressed summary of older messages (for token saving).' })\n lastSummary?: string\n\n /**\n * `lastSummary` 가 **어디까지 덮는지** — 이 메시지까지 요약에 반영됐다.\n *\n * 이게 없으면 누적 요약을 할 수 없다: 매 턴 전체를 다시 요약하거나(비용) 이미 요약한 구간을\n * 또 이어 붙인다(중복). 이력에서 이 id 를 못 찾으면(삭제·편집) 덮은 범위를 알 수 없으므로\n * 전부 미반영으로 보고 다시 요약한다 — 빠뜨리는 쪽보다 안전하다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Id of the last message folded into lastSummary — enables incremental summarization instead of re-reading the whole history.' })\n summaryUpToMessageId?: string\n\n /** 사용 모델 식별 — 'anthropic:claude-sonnet-4-6' 등 */\n @Column({ nullable: true })\n @Field({ nullable: true })\n aiClientId?: string\n\n /** The agent that answered the last turn. Every turn's agent is in the AI usage rows. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'The AI agent that answered the last turn.' })\n agentId?: string\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n\n @DeleteDateColumn()\n deletedAt?: Date\n\n // ── 관계 (TypeORM 만, GraphQL 노출은 별도 query 통해 페이징) ──\n @OneToMany(type => ChatMessage, message => message.session)\n messages?: ChatMessage[]\n\n /*\n * ── 보드 패치 관계를 끊었다 (2026-09-10) ──────────────────────────────────\n * 여기 `@OneToMany(PatchEntry)` 가 있었다. 그래서 **대화 세션이 보드 편집기를 알아야** 했고,\n * 세션 계열을 중립 패키지로 옮기려는 순간 `ai-assistant → board-ai → ai-assistant` 순환이\n * 된다.\n *\n * 끊어도 잃는 것이 없다 — 이 관계를 **자료로 읽는 자리가 하나도 없었다**(전 저장소에서\n * `session.patches` 를 읽는 코드 0 건). 유일한 쓰임이 `PatchEntry` 쪽의 역방향 선언 한 줄이었고,\n * TypeORM 은 역방향 없이도 `@ManyToOne` 을 만든다. 패치는 `sessionId` 로 찾는다\n * (`chatPatches` 가 이미 그렇게 한다).\n *\n * 방향은 남는다: **보드 패치가 세션을 안다. 세션은 보드 패치를 모른다.** 종속이 한쪽이면\n * 중립 쪽이 옮겨질 수 있다.\n */\n}\n"]}
@@ -54,7 +54,7 @@ tslib_1.__decorate([
54
54
  tslib_1.__metadata("design:type", String)
55
55
  ], ChatSessionParticipant.prototype, "userId", void 0);
56
56
  tslib_1.__decorate([
57
- (0, typeorm_1.Column)({ type: 'varchar', length: 16, default: 'member' }),
57
+ (0, typeorm_1.Column)({ length: 16, default: 'member' }),
58
58
  (0, type_graphql_1.Field)({ description: 'Role of this participant: owner | member | viewer.' }),
59
59
  tslib_1.__metadata("design:type", String)
60
60
  ], ChatSessionParticipant.prototype, "role", void 0);
@@ -1 +1 @@
1
- {"version":3,"file":"chat-session-participant.js","sourceRoot":"","sources":["../../../server/service/chat-session-participant/chat-session-participant.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ChatSessionParticipant — 채팅 세션의 참여 사용자.\n *\n * 한 ChatSession 에 여러 사용자가 참여할 수 있고, 각자 role (owner / member / viewer 등)\n * 을 가짐. 다자간 채팅 / 권한 분화 / \"내 세션 목록\" / 멤버 표시 등의 기반.\n *\n * 현재 visibility 정책 (도메인 내 모두 공개) 은 그대로 유지 — 본 엔티티는 영속/추적\n * 데이터 만들고, \"참여자만 접근\" 같은 enforcement 는 정책 굳을 때 별도 phase 에서 도입.\n *\n * 자동 참여 vs 명시적 초대 정책도 미정 — 일단 세션 creator 만 자동 'owner' 로 등록.\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'\n\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\nimport { ChatSession } from '../chat-session/chat-session.js'\n\n@Entity('chat_session_participants')\n@Index('ix_chat_session_participant_0', ['domain', 'session', 'user'], {\n unique: true,\n where: '\"deleted_at\" IS NULL'\n})\n@Index('ix_chat_session_participant_1', ['domain', 'user'], {\n where: '\"deleted_at\" IS NULL'\n})\n@ObjectType({\n description:\n 'Participant of a ChatSession — links a User to a session with a role. Foundation for multi-user chat / per-user filtering / member display.'\n})\nexport class ChatSessionParticipant {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique participant id.' })\n readonly id?: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'Domain to which this participant belongs.' })\n domain?: Domain\n\n @RelationId((p: ChatSessionParticipant) => p.domain)\n domainId?: string\n\n @ManyToOne(type => ChatSession, { onDelete: 'CASCADE' })\n @Field(type => ChatSession, { nullable: true, description: 'The ChatSession this participation belongs to.' })\n session?: ChatSession\n\n @RelationId((p: ChatSessionParticipant) => p.session)\n sessionId?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'The user participating in the session.' })\n user?: User\n\n @RelationId((p: ChatSessionParticipant) => p.user)\n userId?: string\n\n /**\n * 참여자의 역할.\n * - 'owner': 세션 만든 사람. rename / delete 권한.\n * - 'member': 일반 참여자. 메시지 송수신.\n * - 'viewer': 읽기 전용 (미래 권한 정책 활성화 시).\n *\n * 시작은 'owner' / 'member' 둘로 충분. 더 세분화 필요해지면 enum 확장.\n */\n @Column({ type: 'varchar', length: 16, default: 'member' })\n @Field({ description: 'Role of this participant: owner | member | viewer.' })\n role!: string\n\n /** 마지막 활동 시각 — presence indicator. nullable. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Last seen / activity timestamp (presence indicator).' })\n lastSeenAt?: Date\n\n /**\n * 이 참여자가 **읽은 지점**. 세션의 마지막 활동 시각과 비교해 안 읽음을 판정한다.\n *\n * `lastSeenAt`(접속 표시)과 다른 개념이다 — 창을 열어 두고 화면을 안 볼 수 있고, 접속이 끊긴 뒤에도\n * 읽은 지점은 남는다. 두 값을 섞어 쓰면 \"봤는데 안 읽음\" 또는 그 반대가 된다.\n * nullable — 한 번도 읽지 않은 참여자(초대 직후)는 세션의 모든 활동이 안 읽음이다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Point up to which this participant has read the session. Distinct from lastSeenAt (presence): a window can be open without being read, and the read point survives disconnection. Null means nothing read yet.' })\n lastReadAt?: Date\n\n /**\n * 이 사용자가 **자기 목록에서 숨긴 시각**. 숨김은 **나에게만** 적용된다 — 남의 목록·대화 내용은 그대로다.\n *\n * 왜 삭제와 나누는가: 협의는 여럿의 기록이므로 지우는 것은 **만든 사람**만 할 수 있다(되돌릴 수 없다).\n * 그 권한이 없는 사람에게도 \"내 목록을 정리할\" 방법은 있어야 하고, 그것이 숨김이다.\n *\n * 숨긴 **뒤에 새 활동이 생기면 다시 나타난다** — 아직 참여자인 대화에서 조용히 잘려 나가는 것이\n * 목록이 한 줄 늘어나는 것보다 나쁘다. 완전히 빠지려면 대화에서 나가야 한다(별개 기능).\n * nullable — 값이 없으면 숨기지 않은 상태.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'When this user hid the session from their own list. Per-user only; the session and other participants are unaffected. Activity newer than this timestamp makes it reappear.' })\n hiddenAt?: Date\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'User who created this participant record (typically session owner who invited).' })\n creator?: User\n\n @RelationId((p: ChatSessionParticipant) => p.creator)\n creatorId?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'Last user to update this participant record (e.g., role change).' })\n updater?: User\n\n @RelationId((p: ChatSessionParticipant) => p.updater)\n updaterId?: string\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'Joined-at timestamp.' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'Last update timestamp (role change / lastSeenAt bump).' })\n updatedAt?: Date\n\n @DeleteDateColumn()\n @Field({ nullable: true, description: 'Soft delete (= left session) timestamp.' })\n deletedAt?: Date\n}\n"]}
1
+ {"version":3,"file":"chat-session-participant.js","sourceRoot":"","sources":["../../../server/service/chat-session-participant/chat-session-participant.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ChatSessionParticipant — 채팅 세션의 참여 사용자.\n *\n * 한 ChatSession 에 여러 사용자가 참여할 수 있고, 각자 role (owner / member / viewer 등)\n * 을 가짐. 다자간 채팅 / 권한 분화 / \"내 세션 목록\" / 멤버 표시 등의 기반.\n *\n * 현재 visibility 정책 (도메인 내 모두 공개) 은 그대로 유지 — 본 엔티티는 영속/추적\n * 데이터 만들고, \"참여자만 접근\" 같은 enforcement 는 정책 굳을 때 별도 phase 에서 도입.\n *\n * 자동 참여 vs 명시적 초대 정책도 미정 — 일단 세션 creator 만 자동 'owner' 로 등록.\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'\n\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\nimport { ChatSession } from '../chat-session/chat-session.js'\n\n@Entity('chat_session_participants')\n@Index('ix_chat_session_participant_0', ['domain', 'session', 'user'], {\n unique: true,\n where: '\"deleted_at\" IS NULL'\n})\n@Index('ix_chat_session_participant_1', ['domain', 'user'], {\n where: '\"deleted_at\" IS NULL'\n})\n@ObjectType({\n description:\n 'Participant of a ChatSession — links a User to a session with a role. Foundation for multi-user chat / per-user filtering / member display.'\n})\nexport class ChatSessionParticipant {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique participant id.' })\n readonly id?: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'Domain to which this participant belongs.' })\n domain?: Domain\n\n @RelationId((p: ChatSessionParticipant) => p.domain)\n domainId?: string\n\n @ManyToOne(type => ChatSession, { onDelete: 'CASCADE' })\n @Field(type => ChatSession, { nullable: true, description: 'The ChatSession this participation belongs to.' })\n session?: ChatSession\n\n @RelationId((p: ChatSessionParticipant) => p.session)\n sessionId?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'The user participating in the session.' })\n user?: User\n\n @RelationId((p: ChatSessionParticipant) => p.user)\n userId?: string\n\n /**\n * 참여자의 역할.\n * - 'owner': 세션 만든 사람. rename / delete 권한.\n * - 'member': 일반 참여자. 메시지 송수신.\n * - 'viewer': 읽기 전용 (미래 권한 정책 활성화 시).\n *\n * 시작은 'owner' / 'member' 둘로 충분. 더 세분화 필요해지면 enum 확장.\n */\n @Column({ length: 16, default: 'member' })\n @Field({ description: 'Role of this participant: owner | member | viewer.' })\n role!: string\n\n /** 마지막 활동 시각 — presence indicator. nullable. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Last seen / activity timestamp (presence indicator).' })\n lastSeenAt?: Date\n\n /**\n * 이 참여자가 **읽은 지점**. 세션의 마지막 활동 시각과 비교해 안 읽음을 판정한다.\n *\n * `lastSeenAt`(접속 표시)과 다른 개념이다 — 창을 열어 두고 화면을 안 볼 수 있고, 접속이 끊긴 뒤에도\n * 읽은 지점은 남는다. 두 값을 섞어 쓰면 \"봤는데 안 읽음\" 또는 그 반대가 된다.\n * nullable — 한 번도 읽지 않은 참여자(초대 직후)는 세션의 모든 활동이 안 읽음이다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Point up to which this participant has read the session. Distinct from lastSeenAt (presence): a window can be open without being read, and the read point survives disconnection. Null means nothing read yet.' })\n lastReadAt?: Date\n\n /**\n * 이 사용자가 **자기 목록에서 숨긴 시각**. 숨김은 **나에게만** 적용된다 — 남의 목록·대화 내용은 그대로다.\n *\n * 왜 삭제와 나누는가: 협의는 여럿의 기록이므로 지우는 것은 **만든 사람**만 할 수 있다(되돌릴 수 없다).\n * 그 권한이 없는 사람에게도 \"내 목록을 정리할\" 방법은 있어야 하고, 그것이 숨김이다.\n *\n * 숨긴 **뒤에 새 활동이 생기면 다시 나타난다** — 아직 참여자인 대화에서 조용히 잘려 나가는 것이\n * 목록이 한 줄 늘어나는 것보다 나쁘다. 완전히 빠지려면 대화에서 나가야 한다(별개 기능).\n * nullable — 값이 없으면 숨기지 않은 상태.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'When this user hid the session from their own list. Per-user only; the session and other participants are unaffected. Activity newer than this timestamp makes it reappear.' })\n hiddenAt?: Date\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'User who created this participant record (typically session owner who invited).' })\n creator?: User\n\n @RelationId((p: ChatSessionParticipant) => p.creator)\n creatorId?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'Last user to update this participant record (e.g., role change).' })\n updater?: User\n\n @RelationId((p: ChatSessionParticipant) => p.updater)\n updaterId?: string\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'Joined-at timestamp.' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'Last update timestamp (role change / lastSeenAt bump).' })\n updatedAt?: Date\n\n @DeleteDateColumn()\n @Field({ nullable: true, description: 'Soft delete (= left session) timestamp.' })\n deletedAt?: Date\n}\n"]}
@@ -0,0 +1 @@
1
+ export declare const HELP_CATEGORY = "help-documents";
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP_CATEGORY = void 0;
4
+ /* The help tools' category - every conversation carries it (mode-categories.ts), so it is named apart from the tools. */
5
+ exports.HELP_CATEGORY = 'help-documents';
6
+ //# sourceMappingURL=help-keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"help-keys.js","sourceRoot":"","sources":["../../../server/service/help-documents/help-keys.ts"],"names":[],"mappings":";;;AAAA,yHAAyH;AAC5G,QAAA,aAAa,GAAG,gBAAgB,CAAA","sourcesContent":["/* The help tools' category - every conversation carries it (mode-categories.ts), so it is named apart from the tools. */\nexport const HELP_CATEGORY = 'help-documents'\n"]}
@@ -0,0 +1,38 @@
1
+ import { type ToolCategory } from '@things-factory/ai-client-base';
2
+ /** Where a part opens in the application - the help page at that part. */
3
+ export declare const helpLink: (topic: string, section?: string) => string;
4
+ export declare function searchHelpDocuments(args: {
5
+ query?: string;
6
+ limit?: number;
7
+ }, context?: any): {
8
+ found: number;
9
+ parts: any[];
10
+ message: string;
11
+ } | {
12
+ found: number;
13
+ parts: {
14
+ topic: string;
15
+ section: string;
16
+ title: string;
17
+ summary: string;
18
+ link: string;
19
+ }[];
20
+ message?: undefined;
21
+ };
22
+ export declare function readHelpDocument(args: {
23
+ topic?: string;
24
+ section?: string;
25
+ }, context?: any): {
26
+ found: boolean;
27
+ message: string;
28
+ } | {
29
+ link: string;
30
+ topic: string;
31
+ section: string;
32
+ title: string;
33
+ documentTitle: string;
34
+ text: string;
35
+ found: boolean;
36
+ message?: undefined;
37
+ };
38
+ export declare const helpCategory: ToolCategory;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.helpCategory = exports.helpLink = void 0;
4
+ exports.searchHelpDocuments = searchHelpDocuments;
5
+ exports.readHelpDocument = readHelpDocument;
6
+ /*
7
+ * The application's help documents, for the assistant (ADR-0181) - the same index the screen's `?` asks
8
+ * (@things-factory/help). How to do a piece of work, which screen it is on, what a word means: the model finds the part
9
+ * and reads it, rather than the mode's prompt carrying the manual.
10
+ *
11
+ * The help is no one's secret - every screen's `?` opens it for whoever is there - so the tools reach no door. Whether
12
+ * the person may open the screen a part names is still that screen's own door.
13
+ */
14
+ const ai_client_base_1 = require("@things-factory/ai-client-base");
15
+ const help_1 = require("@things-factory/help");
16
+ const help_keys_js_1 = require("./help-keys.js");
17
+ /** Where a part opens in the application - the help page at that part. */
18
+ const helpLink = (topic, section = '') => `help?topic=${encodeURIComponent(topic)}${section ? `&section=${encodeURIComponent(section)}` : ''}`;
19
+ exports.helpLink = helpLink;
20
+ const languageOf = (context) => context?.state?.lng || context?.state?.language || undefined;
21
+ function searchHelpDocuments(args, context) {
22
+ const query = String(args?.query ?? '').trim();
23
+ if (!query)
24
+ return { found: 0, parts: [], message: 'name what to look for.' };
25
+ const parts = (0, help_1.searchHelp)(query, { lang: languageOf(context), limit: Math.min(Math.max(Number(args?.limit) || 5, 1), 10) }).map(hit => ({
26
+ topic: hit.topic,
27
+ section: hit.section,
28
+ title: hit.documentTitle === hit.title ? hit.title : `${hit.documentTitle} › ${hit.title}`,
29
+ summary: hit.summary,
30
+ link: (0, exports.helpLink)(hit.topic, hit.section)
31
+ }));
32
+ return parts.length ? { found: parts.length, parts } : { found: 0, parts: [], message: 'the help has nothing on this - say so; do not answer from general knowledge as if it were this application.' };
33
+ }
34
+ function readHelpDocument(args, context) {
35
+ const topic = String(args?.topic ?? '').trim();
36
+ const section = String(args?.section ?? '').trim();
37
+ const part = topic ? (0, help_1.readHelp)(topic, section, languageOf(context)) : null;
38
+ if (!part)
39
+ return { found: false, message: `no help part ${topic}${section ? `#${section}` : ''} - search again with searchHelp.` };
40
+ return { found: true, ...part, link: (0, exports.helpLink)(part.topic, part.section) };
41
+ }
42
+ const searchSpec = {
43
+ kind: 'read',
44
+ name: 'searchHelp',
45
+ doors: [],
46
+ description: "Search this application's help and manuals (how to do a piece of work, which screen and menu it is on, what a term means). " +
47
+ 'Returns the matching parts with a one-line summary and a link; read one with readHelp.',
48
+ schema: {
49
+ type: 'object',
50
+ properties: {
51
+ query: { type: 'string', description: 'The words to look for, in the language of the help - a screen or menu name, a task, a term ("지출결의 지급", "휴가 신청").' },
52
+ limit: { type: 'number', description: 'How many parts, 1-10. Default 5.' }
53
+ },
54
+ required: ['query']
55
+ },
56
+ builder: (args, context) => searchHelpDocuments(args, context)
57
+ };
58
+ const readSpec = {
59
+ kind: 'read',
60
+ name: 'readHelp',
61
+ doors: [],
62
+ description: 'Read one part of the help (topic and section from searchHelp), or the whole document when section is left out.',
63
+ schema: {
64
+ type: 'object',
65
+ properties: {
66
+ topic: { type: 'string', description: 'The topic from searchHelp ("office/guide-employee").' },
67
+ section: { type: 'string', description: 'The section from searchHelp ("leave"). Leave out for the whole document.' }
68
+ },
69
+ required: ['topic']
70
+ },
71
+ builder: (args, context) => readHelpDocument(args, context)
72
+ };
73
+ exports.helpCategory = {
74
+ name: help_keys_js_1.HELP_CATEGORY,
75
+ description: "the application's help and manuals",
76
+ guidance: [
77
+ '- When the person asks how to do something in this application, where a screen or menu is, what the steps are, or what a term on a screen means, call searchHelp, then readHelp on the part that answers it - before you answer.',
78
+ '- Answer from what readHelp returned, in your own short words, and end with the part as a link: [its title](its link). Never invent a menu, a button or a step the help does not name.',
79
+ '- When searchHelp finds nothing, say the help does not cover it; then answer only what your other tools show.',
80
+ '- The help says how the application works; figures and records still come from the other tools.'
81
+ ].join('\n'),
82
+ specs: [searchSpec, readSpec]
83
+ };
84
+ (0, ai_client_base_1.registerToolCategory)(exports.helpCategory);
85
+ //# sourceMappingURL=help-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"help-tools.js","sourceRoot":"","sources":["../../../server/service/help-documents/help-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The application's help documents, for the assistant (ADR-0181) - the same index the screen's `?` asks\n * (@things-factory/help). How to do a piece of work, which screen it is on, what a word means: the model finds the part\n * and reads it, rather than the mode's prompt carrying the manual.\n *\n * The help is no one's secret - every screen's `?` opens it for whoever is there - so the tools reach no door. Whether\n * the person may open the screen a part names is still that screen's own door.\n */\nimport { registerToolCategory, type ToolCategory, type ToolSpec } from '@things-factory/ai-client-base'\nimport { readHelp, searchHelp } from '@things-factory/help'\n\nimport { HELP_CATEGORY } from './help-keys.js'\n\n/** Where a part opens in the application - the help page at that part. */\nexport const helpLink = (topic: string, section = '') => `help?topic=${encodeURIComponent(topic)}${section ? `&section=${encodeURIComponent(section)}` : ''}`\n\nconst languageOf = (context: any): string | undefined => context?.state?.lng || context?.state?.language || undefined\n\nexport function searchHelpDocuments(args: { query?: string; limit?: number }, context?: any) {\n const query = String(args?.query ?? '').trim()\n if (!query) return { found: 0, parts: [], message: 'name what to look for.' }\n const parts = searchHelp(query, { lang: languageOf(context), limit: Math.min(Math.max(Number(args?.limit) || 5, 1), 10) }).map(hit => ({\n topic: hit.topic,\n section: hit.section,\n title: hit.documentTitle === hit.title ? hit.title : `${hit.documentTitle} › ${hit.title}`,\n summary: hit.summary,\n link: helpLink(hit.topic, hit.section)\n }))\n return parts.length ? { found: parts.length, parts } : { found: 0, parts: [], message: 'the help has nothing on this - say so; do not answer from general knowledge as if it were this application.' }\n}\n\nexport function readHelpDocument(args: { topic?: string; section?: string }, context?: any) {\n const topic = String(args?.topic ?? '').trim()\n const section = String(args?.section ?? '').trim()\n const part = topic ? readHelp(topic, section, languageOf(context)) : null\n if (!part) return { found: false, message: `no help part ${topic}${section ? `#${section}` : ''} - search again with searchHelp.` }\n return { found: true, ...part, link: helpLink(part.topic, part.section) }\n}\n\nconst searchSpec = {\n kind: 'read',\n name: 'searchHelp',\n doors: [],\n description:\n \"Search this application's help and manuals (how to do a piece of work, which screen and menu it is on, what a term means). \" +\n 'Returns the matching parts with a one-line summary and a link; read one with readHelp.',\n schema: {\n type: 'object',\n properties: {\n query: { type: 'string', description: 'The words to look for, in the language of the help - a screen or menu name, a task, a term (\"지출결의 지급\", \"휴가 신청\").' },\n limit: { type: 'number', description: 'How many parts, 1-10. Default 5.' }\n },\n required: ['query']\n },\n builder: (args: any, context: any) => searchHelpDocuments(args, context)\n} as ToolSpec\n\nconst readSpec = {\n kind: 'read',\n name: 'readHelp',\n doors: [],\n description: 'Read one part of the help (topic and section from searchHelp), or the whole document when section is left out.',\n schema: {\n type: 'object',\n properties: {\n topic: { type: 'string', description: 'The topic from searchHelp (\"office/guide-employee\").' },\n section: { type: 'string', description: 'The section from searchHelp (\"leave\"). Leave out for the whole document.' }\n },\n required: ['topic']\n },\n builder: (args: any, context: any) => readHelpDocument(args, context)\n} as ToolSpec\n\nexport const helpCategory: ToolCategory = {\n name: HELP_CATEGORY,\n description: \"the application's help and manuals\",\n guidance: [\n '- When the person asks how to do something in this application, where a screen or menu is, what the steps are, or what a term on a screen means, call searchHelp, then readHelp on the part that answers it - before you answer.',\n '- Answer from what readHelp returned, in your own short words, and end with the part as a link: [its title](its link). Never invent a menu, a button or a step the help does not name.',\n '- When searchHelp finds nothing, say the help does not cover it; then answer only what your other tools show.',\n '- The help says how the application works; figures and records still come from the other tools.'\n ].join('\\n'),\n specs: [searchSpec, readSpec]\n}\n\nregisterToolCategory(helpCategory)\n"]}
@@ -11,6 +11,7 @@ import './chat-session/session-purge.js';
11
11
  import './chat-session/attachment-route.js';
12
12
  import { EntryProposalResolver } from './data-entry/entry-resolver.js';
13
13
  import './workspace/workspace-tools.js';
14
+ import './help-documents/help-tools.js';
14
15
  import './people/index.js';
15
16
  export * from './types.js';
16
17
  export * from './chat-session-resolver.js';
@@ -18,6 +18,7 @@ require("./chat-session/session-purge.js");
18
18
  require("./chat-session/attachment-route.js");
19
19
  const entry_resolver_js_1 = require("./data-entry/entry-resolver.js");
20
20
  require("./workspace/workspace-tools.js");
21
+ require("./help-documents/help-tools.js");
21
22
  /* Who works here, and the card that brings one of them into a conversation - every conversation carries both. */
22
23
  require("./people/index.js");
23
24
  const index_js_4 = require("./unattended-run/index.js");