@llblab/pi-kit 0.27.6 → 0.28.0

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 (182) hide show
  1. package/BACKLOG.md +2 -5
  2. package/CHANGELOG.md +5 -0
  3. package/README.md +2 -2
  4. package/node_modules/@llblab/pi-telegram/AGENTS.md +10 -8
  5. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -9
  6. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
  7. package/node_modules/@llblab/pi-telegram/LICENSE +21 -0
  8. package/node_modules/@llblab/pi-telegram/README.md +15 -5
  9. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
  10. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +5 -7
  11. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +5 -1
  12. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +6 -1
  13. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +4 -2
  14. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +54 -56
  15. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +206 -139
  16. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -2
  17. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +181 -25
  18. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.d.ts +4 -1
  19. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +28 -4
  20. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +52 -56
  21. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +78 -246
  22. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +1 -3
  23. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +23 -26
  24. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +0 -5
  25. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +5 -5
  26. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +0 -23
  27. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +15 -15
  28. package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +0 -17
  29. package/node_modules/@llblab/pi-telegram/dist/lib/config.js +12 -14
  30. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +0 -4
  31. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +2 -5
  32. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +255 -61
  33. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -19
  34. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +0 -2
  35. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +2 -2
  36. package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +177 -16
  37. package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +1115 -249
  38. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.d.ts +0 -2
  39. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.js +2 -2
  40. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +1 -1
  41. package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +100 -12
  42. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +380 -14
  43. package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +30 -12
  44. package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +129 -72
  45. package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +28 -5
  46. package/node_modules/@llblab/pi-telegram/dist/lib/media.js +26 -8
  47. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +0 -11
  48. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +8 -8
  49. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +0 -18
  50. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +18 -18
  51. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +4 -4
  52. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +12 -7
  53. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +1 -1
  54. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +1 -1
  55. package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +1 -0
  56. package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -3
  57. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +0 -1
  58. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +1 -1
  59. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +1 -8
  60. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +1 -6
  61. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +4 -21
  62. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +0 -8
  63. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +5 -5
  64. package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +50 -7
  65. package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +155 -17
  66. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +1 -0
  67. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +2 -1
  68. package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +0 -5
  69. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -5
  70. package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +0 -3
  71. package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +3 -3
  72. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.d.ts +0 -1
  73. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.js +1 -1
  74. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +3 -15
  75. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +69 -8
  76. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +29 -9
  77. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +104 -33
  78. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +0 -4
  79. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -1
  80. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +73 -8
  81. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +2206 -307
  82. package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +0 -2
  83. package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +1 -1
  84. package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +44 -9
  85. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +149 -27
  86. package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +0 -5
  87. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +5 -4
  88. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -3
  89. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +34 -23
  90. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +1 -0
  91. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +18 -21
  92. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +32 -5
  93. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +191 -7
  94. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +2 -0
  95. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +1 -1
  96. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +291 -59
  97. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +1647 -369
  98. package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +0 -1
  99. package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -1
  100. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +184 -28
  101. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +1032 -114
  102. package/node_modules/@llblab/pi-telegram/dist/lib/wire.d.ts +12 -0
  103. package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +21 -0
  104. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +25 -17
  105. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +95 -20
  106. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.d.ts +20 -0
  107. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +103 -0
  108. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +43 -8
  109. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +148 -52
  110. package/node_modules/@llblab/pi-telegram/dist/package.json +6 -6
  111. package/node_modules/@llblab/pi-telegram/dist/skills/generated-control-surface/SKILL.md +3 -3
  112. package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
  113. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
  114. package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
  115. package/node_modules/@llblab/pi-telegram/docs/architecture.md +152 -34
  116. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  117. package/node_modules/@llblab/pi-telegram/docs/delivery.md +1 -1
  118. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
  119. package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
  120. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +125 -19
  121. package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
  122. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
  123. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  124. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +5 -9
  125. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -0
  126. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +5 -2
  127. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +243 -224
  128. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +178 -31
  129. package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +30 -8
  130. package/node_modules/@llblab/pi-telegram/lib/bus.ts +110 -334
  131. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +29 -29
  132. package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +5 -5
  133. package/node_modules/@llblab/pi-telegram/lib/commands.ts +15 -15
  134. package/node_modules/@llblab/pi-telegram/lib/config.ts +12 -17
  135. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +2 -8
  136. package/node_modules/@llblab/pi-telegram/lib/extension.ts +257 -72
  137. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +0 -22
  138. package/node_modules/@llblab/pi-telegram/lib/inbound.ts +2 -2
  139. package/node_modules/@llblab/pi-telegram/lib/journal.ts +1182 -326
  140. package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +2 -2
  141. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +1 -1
  142. package/node_modules/@llblab/pi-telegram/lib/locks.ts +402 -23
  143. package/node_modules/@llblab/pi-telegram/lib/logging.ts +151 -101
  144. package/node_modules/@llblab/pi-telegram/lib/media.ts +51 -9
  145. package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +8 -8
  146. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +18 -18
  147. package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +11 -0
  148. package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +2 -2
  149. package/node_modules/@llblab/pi-telegram/lib/menu.ts +5 -1
  150. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +1 -1
  151. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +1 -10
  152. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +5 -24
  153. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +5 -5
  154. package/node_modules/@llblab/pi-telegram/lib/paths.ts +177 -17
  155. package/node_modules/@llblab/pi-telegram/lib/pi.ts +5 -2
  156. package/node_modules/@llblab/pi-telegram/lib/polling.ts +5 -5
  157. package/node_modules/@llblab/pi-telegram/lib/preview.ts +3 -3
  158. package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +1 -1
  159. package/node_modules/@llblab/pi-telegram/lib/queue.ts +67 -9
  160. package/node_modules/@llblab/pi-telegram/lib/recovery.ts +104 -44
  161. package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
  162. package/node_modules/@llblab/pi-telegram/lib/routing.ts +1955 -375
  163. package/node_modules/@llblab/pi-telegram/lib/sections.ts +1 -1
  164. package/node_modules/@llblab/pi-telegram/lib/status.ts +156 -36
  165. package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
  166. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +48 -33
  167. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +24 -21
  168. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +267 -9
  169. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +3 -1
  170. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1697 -488
  171. package/node_modules/@llblab/pi-telegram/lib/turns.ts +1 -1
  172. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1072 -151
  173. package/node_modules/@llblab/pi-telegram/lib/wire.ts +28 -0
  174. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +97 -47
  175. package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +147 -0
  176. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +188 -89
  177. package/node_modules/@llblab/pi-telegram/package.json +6 -6
  178. package/node_modules/@llblab/pi-telegram/scripts/audit-exports.mjs +100 -0
  179. package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +80 -43
  180. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +3 -3
  181. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +3 -3
  182. package/package.json +2 -2
@@ -12,33 +12,40 @@ import {
12
12
  writeFileSync,
13
13
  appendFileSync,
14
14
  } from "node:fs";
15
- import { dirname } from "node:path";
15
+ import { basename, dirname, join } from "node:path";
16
16
  import {
17
17
  resolveAgentDir,
18
18
  resolveTelegramProfileTempFilePath,
19
+ resolveTelegramRuntimeLogPath,
20
+ resolveTelegramPreviousSharedRuntimeLogPath,
19
21
  } from "./paths.ts";
20
22
  import { withTelegramFileTransaction } from "./locks.ts";
21
23
  import * as Status from "./status.ts";
22
24
 
23
- export type TelegramLogPathInput = string | (() => string);
25
+ type TelegramLogPathInput = string | (() => string);
24
26
 
25
- export interface TelegramRuntimeJsonlEvent {
27
+ interface TelegramRuntimeJsonlEvent {
26
28
  at: number;
27
29
  category: string;
28
30
  message: string;
29
31
  details?: Record<string, unknown>;
30
32
  }
31
33
 
32
- export interface TelegramRuntimeJsonlLogOptions {
34
+ interface TelegramRuntimeJsonlLogOptions {
33
35
  path?: TelegramLogPathInput;
34
36
  previousPath?: TelegramLogPathInput;
35
37
  maxBytes?: number;
36
38
  getNowMs?: () => number;
37
39
  canReset?: () => boolean;
38
40
  commitReset?: (commit: () => void) => boolean;
41
+ /** Explicit shared-file protocol; profile-labelled scope reset is an append, never truncation. */
42
+ sharedProfiles?: {
43
+ getProfileName: () => string | undefined;
44
+ captureAuthority: () => (() => boolean) | undefined;
45
+ };
39
46
  }
40
47
 
41
- export interface TelegramRuntimeJsonlLog {
48
+ interface TelegramRuntimeJsonlLog {
42
49
  getPath: () => string;
43
50
  reset: (reason: string, scope?: Record<string, unknown>) => void;
44
51
  resetIfScopeChanged: (
@@ -88,15 +95,18 @@ function safeJsonLine(value: unknown): string {
88
95
  export function createTelegramRuntimeJsonlLog(
89
96
  options: TelegramRuntimeJsonlLogOptions = {},
90
97
  ): TelegramRuntimeJsonlLog {
98
+ const shared = options.sharedProfiles ? { ...options.sharedProfiles } : undefined;
99
+ const pathSource = options.path, previousSource = options.previousPath;
100
+ const canReset = options.canReset, commitReset = options.commitReset;
91
101
  const resolvePath = () =>
92
- typeof options.path === "function"
93
- ? options.path()
94
- : (options.path ?? getTelegramRuntimeLogPath());
102
+ typeof pathSource === "function"
103
+ ? pathSource()
104
+ : (pathSource ?? getTelegramRuntimeLogPath());
95
105
  const resolvePreviousPath = () => {
96
- if (typeof options.previousPath === "function")
97
- return options.previousPath();
98
- if (options.previousPath) return options.previousPath;
99
- return resolvePath().replace(/\.jsonl$/u, "._prev.jsonl");
106
+ if (typeof previousSource === "function") return previousSource();
107
+ if (previousSource) return previousSource;
108
+ return shared ? join(dirname(resolvePath()), "logs", `${basename(resolvePath(), ".jsonl")}._prev.jsonl`)
109
+ : resolvePath().replace(/\.jsonl$/u, "._prev.jsonl");
100
110
  };
101
111
  const maxBytes = options.maxBytes ?? DEFAULT_MAX_LOG_BYTES;
102
112
  const getNowMs = options.getNowMs ?? Date.now;
@@ -107,7 +117,17 @@ export function createTelegramRuntimeJsonlLog(
107
117
  path: string;
108
118
  previousPath: string;
109
119
  line: string;
120
+ profile?: string;
121
+ rotationCurrent?: () => boolean;
110
122
  }[] = [];
123
+ const getProfile = () => shared?.getProfileName() ?? "default";
124
+ const transactionPath = (path: string) => shared ? join(dirname(path), "runtime", `${basename(path)}.transaction`) : `${path}.transaction`;
125
+ const captureRotation = (path: string, previousPath: string, profile: string): (() => boolean) | undefined => {
126
+ if (!shared) return undefined;
127
+ const authority = shared.captureAuthority();
128
+ return () => authority?.() === true && resolvePath() === path && resolvePreviousPath() === previousPath && getProfile() === profile;
129
+ };
130
+ const scopeStorageKey = (path: string, profile: string) => shared ? JSON.stringify([path, profile]) : path;
111
131
 
112
132
  const ensureParent = (path: string) => {
113
133
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
@@ -124,49 +144,63 @@ export function createTelegramRuntimeJsonlLog(
124
144
  previousPath: string,
125
145
  reason: string,
126
146
  scope?: Record<string, unknown>,
127
- ) => {
147
+ profile?: string,
148
+ isCurrent?: () => boolean,
149
+ ): boolean => {
150
+ const line = safeJsonLine({
151
+ at: getNowMs(),
152
+ kind: "reset",
153
+ reason,
154
+ scope,
155
+ previousPath,
156
+ ...(shared ? { profile } : {}),
157
+ }) + "\n";
158
+ if (shared && !isCurrent?.()) return false;
128
159
  ensureParent(path);
129
160
  preserveCurrentLog(path, previousPath);
130
- writeFileSync(
131
- path,
132
- safeJsonLine({
133
- at: getNowMs(),
134
- kind: "reset",
135
- reason,
136
- scope,
137
- previousPath,
138
- }) + "\n",
139
- { mode: 0o600 },
140
- );
161
+ if (shared && !isCurrent?.()) return false;
162
+ writeFileSync(path, line, { mode: 0o600 });
163
+ return true;
141
164
  };
142
165
 
143
166
  const writeReset = (
144
167
  reason: string,
145
- scope?: Record<string, unknown>,
168
+ scope: Record<string, unknown> | undefined,
169
+ path: string,
170
+ profile: string,
146
171
  ): boolean => {
147
- if (options.canReset && !options.canReset()) return false;
148
- const path = resolvePath();
172
+ const previousPath = resolvePreviousPath();
173
+ const isCurrent = captureRotation(path, previousPath, profile);
174
+ if (canReset && !canReset()) return false;
149
175
  let didReset = false;
150
- withTelegramFileTransaction(`${path}.transaction`, () => {
176
+ withTelegramFileTransaction(transactionPath(path), () => {
151
177
  const commit = () => {
152
- writeResetLocked(path, resolvePreviousPath(), reason, scope);
153
- didReset = true;
178
+ if (shared && !isCurrent?.()) return;
179
+ if (shared) {
180
+ const line = safeJsonLine({ at: getNowMs(), kind: "reset", reason, scope, profile }) + "\n";
181
+ if (!isCurrent?.()) return;
182
+ ensureParent(path);
183
+ if (existsSync(path) && statSync(path).size + Buffer.byteLength(line) > maxBytes &&
184
+ !writeResetLocked(path, previousPath, "max-bytes", { maxBytes }, profile, isCurrent)) return;
185
+ if (!isCurrent?.()) return;
186
+ appendFileSync(path, line, { mode: 0o600 });
187
+ didReset = true;
188
+ } else didReset = writeResetLocked(path, previousPath, reason, scope);
154
189
  };
155
- if (options.commitReset) {
156
- options.commitReset(commit);
190
+ if (commitReset) {
191
+ commitReset(commit);
157
192
  } else {
158
193
  commit();
159
194
  }
160
195
  });
161
- return didReset;
196
+ return didReset && (!shared || isCurrent?.() === true);
162
197
  };
163
198
 
164
- const appendLine = (line: string) => {
165
- queuedAppends.push({
166
- path: resolvePath(),
167
- previousPath: resolvePreviousPath(),
168
- line,
169
- });
199
+ const appendEvent = (event: TelegramRuntimeJsonlEvent) => {
200
+ const path = resolvePath(), previousPath = resolvePreviousPath(), profile = getProfile();
201
+ const rotationCurrent = captureRotation(path, previousPath, profile);
202
+ const line = safeJsonLine({ kind: "event", ...event, ...(shared ? { profile } : {}) }) + "\n";
203
+ queuedAppends.push({ path, previousPath, line, ...(shared ? { profile, rotationCurrent } : {}) });
170
204
  if (appendScheduled) return;
171
205
  appendScheduled = true;
172
206
  pending = pending
@@ -176,18 +210,18 @@ export function createTelegramRuntimeJsonlLog(
176
210
  queuedAppends = [];
177
211
  const groups = new Map<
178
212
  string,
179
- { path: string; previousPath: string; lines: string[] }
213
+ { path: string; previousPath: string; entries: typeof queuedAppends }
180
214
  >();
181
215
  for (const entry of batch) {
182
216
  const key = `${entry.path}\u0000${entry.previousPath}`;
183
217
  const group = groups.get(key);
184
- if (group) group.lines.push(entry.line);
185
- else groups.set(key, { ...entry, lines: [entry.line] });
218
+ if (group) group.entries.push(entry);
219
+ else groups.set(key, { ...entry, entries: [entry] });
186
220
  }
187
221
  for (const group of groups.values()) {
188
222
  try {
189
223
  ensureParent(group.path);
190
- withTelegramFileTransaction(`${group.path}.transaction`, () => {
224
+ withTelegramFileTransaction(transactionPath(group.path), () => {
191
225
  let currentSize = existsSync(group.path)
192
226
  ? statSync(group.path).size
193
227
  : 0;
@@ -200,31 +234,35 @@ export function createTelegramRuntimeJsonlLog(
200
234
  chunk = "";
201
235
  chunkBytes = 0;
202
236
  };
203
- const rotate = (): boolean => {
204
- if (options.canReset && !options.canReset()) return false;
237
+ const rotate = (entry: typeof queuedAppends[number]): boolean => {
238
+ if (canReset && !canReset()) return false;
239
+ if (shared && !entry.rotationCurrent?.()) return false;
205
240
  let rotated = false;
206
241
  const commit = () => {
207
- writeResetLocked(
242
+ if (shared && !entry.rotationCurrent?.()) return;
243
+ rotated = writeResetLocked(
208
244
  group.path,
209
245
  group.previousPath,
210
246
  "max-bytes",
211
247
  { maxBytes },
248
+ entry.profile,
249
+ entry.rotationCurrent,
212
250
  );
213
- rotated = true;
214
251
  };
215
- if (options.commitReset) options.commitReset(commit);
252
+ if (commitReset) commitReset(commit);
216
253
  else commit();
217
254
  if (rotated) currentSize = statSync(group.path).size;
218
255
  return rotated;
219
256
  };
220
- for (const line of group.lines) {
257
+ for (const entry of group.entries) {
258
+ const line = entry.line;
221
259
  const lineBytes = Buffer.byteLength(line);
222
260
  if (
223
261
  currentSize + chunkBytes > 0 &&
224
262
  currentSize + chunkBytes + lineBytes > maxBytes
225
263
  ) {
226
264
  flushChunk();
227
- rotate();
265
+ rotate(entry);
228
266
  }
229
267
  chunk += line;
230
268
  chunkBytes += lineBytes;
@@ -242,27 +280,27 @@ export function createTelegramRuntimeJsonlLog(
242
280
  return {
243
281
  getPath: resolvePath,
244
282
  reset(reason, scope) {
245
- const path = resolvePath();
283
+ const path = resolvePath(), profile = getProfile();
246
284
  try {
247
- if (writeReset(reason, scope)) {
248
- scopeKeys.set(path, scope ? safeJsonLine(scope) : undefined);
285
+ if (writeReset(reason, scope, path, profile)) {
286
+ scopeKeys.set(scopeStorageKey(path, profile), scope ? safeJsonLine(scope) : undefined);
249
287
  }
250
288
  } catch {
251
289
  // Diagnostics must never break Telegram runtime behavior.
252
290
  }
253
291
  },
254
292
  resetIfScopeChanged(nextScopeKey, reason, scope) {
255
- const path = resolvePath();
256
- if (scopeKeys.get(path) === nextScopeKey) return;
293
+ const path = resolvePath(), profile = getProfile(), key = scopeStorageKey(path, profile);
294
+ if (scopeKeys.get(key) === nextScopeKey) return;
257
295
  try {
258
- if (writeReset(reason, scope)) scopeKeys.set(path, nextScopeKey);
296
+ if (writeReset(reason, scope, path, profile)) scopeKeys.set(key, nextScopeKey);
259
297
  } catch {
260
298
  // Diagnostics must never break Telegram runtime behavior.
261
299
  }
262
300
  },
263
301
  record(event) {
264
302
  try {
265
- appendLine(safeJsonLine({ kind: "event", ...event }) + "\n");
303
+ appendEvent(event);
266
304
  } catch {
267
305
  // Diagnostics must never break Telegram runtime behavior.
268
306
  }
@@ -270,7 +308,19 @@ export function createTelegramRuntimeJsonlLog(
270
308
  };
271
309
  }
272
310
 
273
- export interface TelegramRuntimeDiagnosticsRuntime<TContext> {
311
+ interface TelegramRuntimeDiagnosticsStatusPorts<TContext> {
312
+ instanceId: string;
313
+ updateStatus(ctx: TContext, error?: string): void;
314
+ getStatusState(): Status.TelegramBridgeStatusLineState;
315
+ persistSnapshot(snapshot: ReturnType<typeof Status.createTelegramStatusSnapshot>): Promise<void>;
316
+ session?: {
317
+ get(): TContext | undefined;
318
+ getGeneration(): number;
319
+ isCurrent(ctx: TContext, generation?: number): boolean;
320
+ };
321
+ }
322
+
323
+ interface TelegramRuntimeDiagnosticsRuntime<TContext> {
274
324
  events: Status.TelegramRuntimeEventRecorder;
275
325
  recordRuntimeEvent(
276
326
  category: string,
@@ -282,15 +332,11 @@ export interface TelegramRuntimeDiagnosticsRuntime<TContext> {
282
332
  getProfileName(): string | undefined;
283
333
  canReset(): boolean;
284
334
  commitReset(commit: () => void): boolean;
335
+ captureAuthority?: () => (() => boolean) | undefined;
285
336
  }): void;
286
- bindStatus(ports: {
287
- instanceId: string;
288
- updateStatus(ctx: TContext, error?: string): void;
289
- getStatusState(): Status.TelegramBridgeStatusLineState;
290
- persistSnapshot(
291
- snapshot: ReturnType<typeof Status.createTelegramStatusSnapshot>,
292
- ): Promise<void>;
293
- }): void;
337
+ bindStatus(ports: TelegramRuntimeDiagnosticsStatusPorts<TContext>): void;
338
+ onSessionStart(): void;
339
+ onSessionShutdown(): Promise<void>;
294
340
  updateStatus(ctx: TContext, error?: string): void;
295
341
  getStatusLines(options?: Status.TelegramBridgeStatusLineOptions): string[];
296
342
  scheduleSnapshotPersist(): void;
@@ -298,29 +344,25 @@ export interface TelegramRuntimeDiagnosticsRuntime<TContext> {
298
344
 
299
345
  export function createTelegramRuntimeDiagnosticsRuntime<
300
346
  TContext,
301
- >(): TelegramRuntimeDiagnosticsRuntime<TContext> {
347
+ >(options: {
348
+ sharedFile?: boolean;
349
+ snapshotTimer?: Pick<Parameters<typeof Status.createTelegramRuntimeDiagnosticsSnapshotScheduler>[0], "setTimer" | "clearTimer">;
350
+ } = {}): TelegramRuntimeDiagnosticsRuntime<TContext> {
351
+ const sharedFile = options.sharedFile === true;
302
352
  let getBotToken = (): string | undefined => undefined;
303
353
  let getProfileName = (): string | undefined => undefined;
304
354
  let canReset = (): boolean => false;
305
355
  let commitReset = (_commit: () => void): boolean => false;
306
- let statusPorts:
307
- | {
308
- instanceId: string;
309
- updateStatus(ctx: TContext, error?: string): void;
310
- getStatusState(): Status.TelegramBridgeStatusLineState;
311
- persistSnapshot(
312
- snapshot: ReturnType<typeof Status.createTelegramStatusSnapshot>,
313
- ): Promise<void>;
314
- }
315
- | undefined;
316
- let requestSnapshotPersist = (): void => {};
356
+ let captureAuthority = (): (() => boolean) | undefined => undefined;
357
+ let statusPorts: TelegramRuntimeDiagnosticsStatusPorts<TContext> | undefined;
317
358
  const events = Status.createTelegramRuntimeEventRecorder({
318
359
  getBotToken: () => getBotToken(),
319
360
  });
320
361
  const jsonl = createTelegramRuntimeJsonlLog({
321
- path: () => getTelegramRuntimeLogPath(undefined, getProfileName()),
322
- previousPath: () =>
323
- getTelegramPreviousRuntimeLogPath(undefined, getProfileName()),
362
+ path: () => sharedFile ? resolveTelegramRuntimeLogPath() : getTelegramRuntimeLogPath(undefined, getProfileName()),
363
+ previousPath: () => sharedFile ? resolveTelegramPreviousSharedRuntimeLogPath()
364
+ : getTelegramPreviousRuntimeLogPath(undefined, getProfileName()),
365
+ ...(sharedFile ? { sharedProfiles: { getProfileName: () => getProfileName(), captureAuthority: () => captureAuthority() } } : {}),
324
366
  canReset: () => canReset(),
325
367
  commitReset: (commit) => commitReset(commit),
326
368
  });
@@ -334,12 +376,29 @@ export function createTelegramRuntimeDiagnosticsRuntime<
334
376
  if (latestEvent) jsonl.record(latestEvent);
335
377
  requestSnapshotPersist();
336
378
  };
337
- const persistCurrentSnapshot = async (): Promise<void> => {
338
- if (!statusPorts) return;
339
- await statusPorts.persistSnapshot(
340
- Status.createTelegramStatusSnapshot(statusPorts.getStatusState()),
341
- );
379
+ const captureSnapshotScope = (): (() => boolean) | undefined => {
380
+ const ports = statusPorts;
381
+ if (!ports) return undefined;
382
+ if (!ports.session) return () => statusPorts === ports;
383
+ const session = ports.session, ctx = session.get(), generation = session.getGeneration();
384
+ if (ctx === undefined || !session.isCurrent(ctx, generation)) return undefined;
385
+ return () => statusPorts === ports && session.isCurrent(ctx, generation);
342
386
  };
387
+ const requestSnapshotPersist = Status.createTelegramRuntimeDiagnosticsSnapshotScheduler({
388
+ ...options.snapshotTimer,
389
+ captureScope: captureSnapshotScope,
390
+ async persistSnapshot(isCurrent) {
391
+ const ports = statusPorts;
392
+ // Projection may acquire journal guards or recover a snapshot: fence before reading it.
393
+ if (!ports || !isCurrent()) return;
394
+ const snapshot = Status.createTelegramStatusSnapshot(ports.getStatusState());
395
+ if (!isCurrent()) return;
396
+ await ports.persistSnapshot(snapshot);
397
+ },
398
+ recordError(error) {
399
+ events.record("telegram", error, { phase: "runtime-diagnostics-snapshot-persist" });
400
+ },
401
+ });
343
402
  const updateRuntimeLogScope = function (reason: string): void {
344
403
  if (!statusPorts) return;
345
404
  const scope = Status.createTelegramRuntimeLogScope({
@@ -356,31 +415,22 @@ export function createTelegramRuntimeDiagnosticsRuntime<
356
415
  getProfileName = ports.getProfileName;
357
416
  canReset = ports.canReset;
358
417
  commitReset = ports.commitReset;
418
+ captureAuthority = ports.captureAuthority ?? (() => undefined);
359
419
  },
360
420
  bindStatus(ports) {
361
421
  statusPorts = ports;
362
- requestSnapshotPersist =
363
- Status.createTelegramRuntimeDiagnosticsSnapshotScheduler({
364
- persistSnapshot: persistCurrentSnapshot,
365
- recordError(error) {
366
- events.record("telegram", error, {
367
- phase: "runtime-diagnostics-snapshot-persist",
368
- });
369
- },
370
- });
422
+ if (ports.session) void requestSnapshotPersist.suspend();
371
423
  },
424
+ onSessionStart: requestSnapshotPersist.resume,
425
+ onSessionShutdown: requestSnapshotPersist.suspend,
372
426
  updateStatus(ctx, error) {
373
- if (!statusPorts) return;
427
+ if (!statusPorts || statusPorts.session?.isCurrent(ctx) === false) return;
374
428
  statusPorts.updateStatus(ctx, error);
375
429
  updateRuntimeLogScope("status-scope-change");
376
430
  },
377
431
  getStatusLines(options) {
378
432
  if (!statusPorts) return [];
379
- void persistCurrentSnapshot().catch((error) => {
380
- recordRuntimeEvent("telegram", error, {
381
- phase: "status-snapshot-persist",
382
- });
383
- });
433
+ requestSnapshotPersist();
384
434
  return Status.buildTelegramBridgeStatusLines(
385
435
  statusPorts.getStatusState(),
386
436
  options,
@@ -67,10 +67,13 @@ export interface TelegramReplyToMessage {
67
67
 
68
68
  export interface TelegramSticker {
69
69
  file_id: string;
70
+ is_video?: boolean;
71
+ is_animated?: boolean;
70
72
  }
71
73
 
72
74
  export interface TelegramMediaMessage {
73
75
  message_id: number;
76
+ chat?: { id: number; type?: string; username?: string };
74
77
  from?: TelegramMessageUser;
75
78
  forward_origin?: TelegramMessageForwardOrigin;
76
79
  forward_from?: TelegramMessageUser;
@@ -154,9 +157,26 @@ export interface TelegramMediaGroupControllerOptions {
154
157
  export type TelegramAttachmentKind =
155
158
  "photo" | "document" | "video" | "audio" | "voice" | "animation" | "sticker";
156
159
 
160
+ /** Where an inbound attachment came from; the download layer turns it into a scoped file name. */
161
+ export interface TelegramAttachmentSource {
162
+ kind: string;
163
+ messageId: number;
164
+ index?: number;
165
+ /** Sender-chosen file name, kept as a trailing readable segment. */
166
+ userFileName?: string;
167
+ chat?: { id: number; type?: string; username?: string };
168
+ /** Explicit source scope, e.g. a Guest Mode peer whose message IDs are not the bot chat's. */
169
+ scope?: string;
170
+ }
171
+
157
172
  export interface TelegramFileInfo {
158
173
  file_id: string;
159
174
  fileName: string;
175
+ /** Present only when the sender chose the name. */
176
+ userFileName?: string;
177
+ /** Position inside a rich message. */
178
+ index?: number;
179
+ source?: TelegramAttachmentSource;
160
180
  mimeType?: string;
161
181
  kind: TelegramAttachmentKind;
162
182
  isImage: boolean;
@@ -179,10 +199,14 @@ export interface DownloadedTelegramMessageFile {
179
199
  }
180
200
 
181
201
  export interface DownloadTelegramMessageFilesDeps {
182
- downloadFile: (fileId: string, fileName: string) => Promise<string>;
202
+ downloadFile: (
203
+ fileId: string,
204
+ fileName: string,
205
+ source?: TelegramAttachmentSource,
206
+ ) => Promise<string>;
183
207
  }
184
208
 
185
- export function guessExtensionFromMime(
209
+ function guessExtensionFromMime(
186
210
  mimeType: string | undefined,
187
211
  fallback: string,
188
212
  ): string {
@@ -351,7 +375,7 @@ export function extractTelegramForwardContextText(
351
375
  return `from: ${origin}`;
352
376
  }
353
377
 
354
- export function extractTelegramReplyContextText(
378
+ function extractTelegramReplyContextText(
355
379
  message: TelegramMediaMessage,
356
380
  ): string {
357
381
  const quoted = (
@@ -396,7 +420,7 @@ export function buildTelegramReplyContextBlock(
396
420
  return "";
397
421
  }
398
422
 
399
- export function appendTelegramReplyContext(
423
+ function appendTelegramReplyContext(
400
424
  text: string,
401
425
  replyContext: string,
402
426
  ): string {
@@ -437,7 +461,7 @@ export function extractFirstTelegramMessageText(
437
461
  return messages.map(extractTelegramMessageText).find(Boolean) ?? "";
438
462
  }
439
463
 
440
- export function hasTelegramMessagePromptContent(
464
+ function hasTelegramMessagePromptContent(
441
465
  message: TelegramMediaMessage,
442
466
  ): boolean {
443
467
  return (
@@ -712,7 +736,7 @@ export async function downloadTelegramMessageFiles(
712
736
  const downloaded: DownloadedTelegramMessageFile[] = [];
713
737
  for (const file of collectTelegramFileInfos(messages)) {
714
738
  downloaded.push({
715
- path: await deps.downloadFile(file.file_id, file.fileName),
739
+ path: await deps.downloadFile(file.file_id, file.fileName, file.source),
716
740
  fileName: file.fileName,
717
741
  isImage: file.isImage,
718
742
  mimeType: file.mimeType,
@@ -759,6 +783,7 @@ function collectTelegramRichBlockFileInfos(
759
783
  files.push({
760
784
  file_id: photo.file_id,
761
785
  fileName: `photo-${messageId}-${mediaIndex}.jpg`,
786
+ index: mediaIndex,
762
787
  mimeType: "image/jpeg",
763
788
  kind: "photo",
764
789
  isImage: true,
@@ -788,6 +813,8 @@ function collectTelegramRichBlockFileInfos(
788
813
  kind === "voice" ? ".ogg" : kind === "audio" ? ".mp3" : ".mp4";
789
814
  files.push({
790
815
  file_id: fileId,
816
+ index: mediaIndex,
817
+ ...(typeof fileName === "string" ? { userFileName: fileName } : {}),
791
818
  fileName:
792
819
  typeof fileName === "string"
793
820
  ? fileName
@@ -817,6 +844,7 @@ export function collectTelegramFileInfos(
817
844
  ): TelegramFileInfo[] {
818
845
  const files: TelegramFileInfo[] = [];
819
846
  for (const message of messages) {
847
+ const firstOfMessage = files.length;
820
848
  files.push(
821
849
  ...collectTelegramRichBlockFileInfos(
822
850
  message.rich_message?.blocks,
@@ -847,6 +875,7 @@ export function collectTelegramFileInfos(
847
875
  files.push({
848
876
  file_id: message.document.file_id,
849
877
  fileName,
878
+ ...(message.document.file_name ? { userFileName: message.document.file_name } : {}),
850
879
  mimeType: message.document.mime_type,
851
880
  kind: "document",
852
881
  isImage: isImageMimeType(message.document.mime_type),
@@ -862,6 +891,7 @@ export function collectTelegramFileInfos(
862
891
  files.push({
863
892
  file_id: message.video.file_id,
864
893
  fileName,
894
+ ...(message.video.file_name ? { userFileName: message.video.file_name } : {}),
865
895
  mimeType: message.video.mime_type,
866
896
  kind: "video",
867
897
  isImage: false,
@@ -878,6 +908,7 @@ export function collectTelegramFileInfos(
878
908
  files.push({
879
909
  file_id: message.audio.file_id,
880
910
  fileName,
911
+ ...(message.audio.file_name ? { userFileName: message.audio.file_name } : {}),
881
912
  mimeType: message.audio.mime_type,
882
913
  kind: "audio",
883
914
  isImage: false,
@@ -907,20 +938,31 @@ export function collectTelegramFileInfos(
907
938
  files.push({
908
939
  file_id: message.animation.file_id,
909
940
  fileName,
941
+ ...(message.animation.file_name ? { userFileName: message.animation.file_name } : {}),
910
942
  mimeType: message.animation.mime_type,
911
943
  kind: "animation",
912
944
  isImage: false,
913
945
  });
914
946
  }
915
947
  if (message.sticker) {
948
+ // Sticker format is defined by Bot API flags, not a documented MIME field.
949
+ const video = message.sticker.is_video === true;
950
+ const animated = message.sticker.is_animated === true;
951
+ const extension = video ? ".webm" : animated ? ".tgs" : ".webp";
916
952
  files.push({
917
953
  file_id: message.sticker.file_id,
918
- fileName: `sticker-${message.message_id}.webp`,
919
- mimeType: "image/webp",
954
+ fileName: `sticker-${message.message_id}${extension}`,
955
+ mimeType: video ? "video/webm" : animated ? "application/x-tgsticker" : "image/webp",
920
956
  kind: "sticker",
921
- isImage: true,
957
+ isImage: !video && !animated,
922
958
  });
923
959
  }
960
+ for (const file of files.slice(firstOfMessage)) {
961
+ file.source = { kind: file.kind, messageId: message.message_id,
962
+ ...(file.index !== undefined ? { index: file.index } : {}),
963
+ ...(file.userFileName ? { userFileName: file.userFileName } : {}),
964
+ ...(message.chat ? { chat: message.chat } : {}) };
965
+ }
924
966
  }
925
967
  const seenFileIds = new Set<string>();
926
968
  return files.filter((file) => {
@@ -267,7 +267,7 @@ export const TELEGRAM_MODEL_PAGE_SIZE = 6;
267
267
  const TELEGRAM_MODEL_PAGE_PICKER_ROW_SIZE = 4;
268
268
  export const MODEL_MENU_TITLE = "<b>🤖 Choose a model:</b>";
269
269
  export const MODEL_PAGE_MENU_TITLE = "<b>Choose a page:</b>";
270
- export const MODEL_DETAIL_MENU_TITLE = "<b>🤖 Model:</b>";
270
+ const MODEL_DETAIL_MENU_TITLE = "<b>🤖 Model:</b>";
271
271
 
272
272
  function truncateTelegramButtonLabel(label: string, maxLength = 56): string {
273
273
  return label.length <= maxLength
@@ -647,7 +647,7 @@ export function getTelegramModelSelection<TModel extends MenuModel = MenuModel>(
647
647
  return { kind: "selected", selection };
648
648
  }
649
649
 
650
- export function applyTelegramModelDetailSelection(
650
+ function applyTelegramModelDetailSelection(
651
651
  state: TelegramModelMenuState,
652
652
  value: string | undefined,
653
653
  ): TelegramMenuMutationResult {
@@ -661,7 +661,7 @@ export function applyTelegramModelDetailSelection(
661
661
  return "changed";
662
662
  }
663
663
 
664
- export function getTelegramSelectedDetailModel<
664
+ function getTelegramSelectedDetailModel<
665
665
  TModel extends MenuModel = MenuModel,
666
666
  >(state: TelegramModelMenuState<TModel>): TelegramMenuSelectionResult<TModel> {
667
667
  const indexedSelection = getTelegramModelSelection(
@@ -687,7 +687,7 @@ export function getTelegramSelectedDetailModel<
687
687
  return indexedSelection;
688
688
  }
689
689
 
690
- export function isTelegramModelScoped(
690
+ function isTelegramModelScoped(
691
691
  state: TelegramModelMenuState,
692
692
  model: MenuModel,
693
693
  ): boolean {
@@ -697,7 +697,7 @@ export function isTelegramModelScoped(
697
697
  );
698
698
  }
699
699
 
700
- export function focusTelegramModelListPage(
700
+ function focusTelegramModelListPage(
701
701
  state: TelegramModelMenuState,
702
702
  model: MenuModel,
703
703
  pageSize = TELEGRAM_MODEL_PAGE_SIZE,
@@ -715,7 +715,7 @@ function formatScopedModelPattern(entry: ScopedTelegramModel): string {
715
715
  return entry.thinkingLevel ? `${key}:${entry.thinkingLevel}` : key;
716
716
  }
717
717
 
718
- export function setTelegramModelScope(
718
+ function setTelegramModelScope(
719
719
  state: TelegramModelMenuState,
720
720
  model: MenuModel,
721
721
  enabled: boolean,
@@ -1062,7 +1062,7 @@ export function buildModelMenuReplyMarkup(
1062
1062
  return { inline_keyboard: rows };
1063
1063
  }
1064
1064
 
1065
- export function buildModelDetailMenuReplyMarkup(
1065
+ function buildModelDetailMenuReplyMarkup(
1066
1066
  state: TelegramModelMenuState,
1067
1067
  currentModel: MenuModel | undefined,
1068
1068
  ): TelegramReplyMarkup {
@@ -1100,7 +1100,7 @@ export function buildModelDetailMenuReplyMarkup(
1100
1100
  };
1101
1101
  }
1102
1102
 
1103
- export function buildModelDetailMenuText(
1103
+ function buildModelDetailMenuText(
1104
1104
  state: TelegramModelMenuState,
1105
1105
  ): string {
1106
1106
  const selection = getTelegramSelectedDetailModel(state);