@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
@@ -4,8 +4,8 @@
4
4
  * Owns bounded JSONL runtime evidence files, previous-log preservation, and profile-aware log paths without becoming routing state
5
5
  */
6
6
  import { copyFileSync, existsSync, mkdirSync, statSync, writeFileSync, appendFileSync, } from "node:fs";
7
- import { dirname } from "node:path";
8
- import { resolveAgentDir, resolveTelegramProfileTempFilePath, } from "./paths.js";
7
+ import { basename, dirname, join } from "node:path";
8
+ import { resolveAgentDir, resolveTelegramProfileTempFilePath, resolveTelegramRuntimeLogPath, resolveTelegramPreviousSharedRuntimeLogPath, } from "./paths.js";
9
9
  import { withTelegramFileTransaction } from "./locks.js";
10
10
  import * as Status from "./status.js";
11
11
  const DEFAULT_MAX_LOG_BYTES = 5 * 1024 * 1024;
@@ -27,15 +27,19 @@ function safeJsonLine(value) {
27
27
  });
28
28
  }
29
29
  export function createTelegramRuntimeJsonlLog(options = {}) {
30
- const resolvePath = () => typeof options.path === "function"
31
- ? options.path()
32
- : (options.path ?? getTelegramRuntimeLogPath());
30
+ const shared = options.sharedProfiles ? { ...options.sharedProfiles } : undefined;
31
+ const pathSource = options.path, previousSource = options.previousPath;
32
+ const canReset = options.canReset, commitReset = options.commitReset;
33
+ const resolvePath = () => typeof pathSource === "function"
34
+ ? pathSource()
35
+ : (pathSource ?? getTelegramRuntimeLogPath());
33
36
  const resolvePreviousPath = () => {
34
- if (typeof options.previousPath === "function")
35
- return options.previousPath();
36
- if (options.previousPath)
37
- return options.previousPath;
38
- return resolvePath().replace(/\.jsonl$/u, "._prev.jsonl");
37
+ if (typeof previousSource === "function")
38
+ return previousSource();
39
+ if (previousSource)
40
+ return previousSource;
41
+ return shared ? join(dirname(resolvePath()), "logs", `${basename(resolvePath(), ".jsonl")}._prev.jsonl`)
42
+ : resolvePath().replace(/\.jsonl$/u, "._prev.jsonl");
39
43
  };
40
44
  const maxBytes = options.maxBytes ?? DEFAULT_MAX_LOG_BYTES;
41
45
  const getNowMs = options.getNowMs ?? Date.now;
@@ -43,6 +47,15 @@ export function createTelegramRuntimeJsonlLog(options = {}) {
43
47
  let pending = Promise.resolve();
44
48
  let appendScheduled = false;
45
49
  let queuedAppends = [];
50
+ const getProfile = () => shared?.getProfileName() ?? "default";
51
+ const transactionPath = (path) => shared ? join(dirname(path), "runtime", `${basename(path)}.transaction`) : `${path}.transaction`;
52
+ const captureRotation = (path, previousPath, profile) => {
53
+ if (!shared)
54
+ return undefined;
55
+ const authority = shared.captureAuthority();
56
+ return () => authority?.() === true && resolvePath() === path && resolvePreviousPath() === previousPath && getProfile() === profile;
57
+ };
58
+ const scopeStorageKey = (path, profile) => shared ? JSON.stringify([path, profile]) : path;
46
59
  const ensureParent = (path) => {
47
60
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
48
61
  };
@@ -52,42 +65,64 @@ export function createTelegramRuntimeJsonlLog(options = {}) {
52
65
  mkdirSync(dirname(previousPath), { recursive: true, mode: 0o700 });
53
66
  copyFileSync(path, previousPath);
54
67
  };
55
- const writeResetLocked = (path, previousPath, reason, scope) => {
56
- ensureParent(path);
57
- preserveCurrentLog(path, previousPath);
58
- writeFileSync(path, safeJsonLine({
68
+ const writeResetLocked = (path, previousPath, reason, scope, profile, isCurrent) => {
69
+ const line = safeJsonLine({
59
70
  at: getNowMs(),
60
71
  kind: "reset",
61
72
  reason,
62
73
  scope,
63
74
  previousPath,
64
- }) + "\n", { mode: 0o600 });
75
+ ...(shared ? { profile } : {}),
76
+ }) + "\n";
77
+ if (shared && !isCurrent?.())
78
+ return false;
79
+ ensureParent(path);
80
+ preserveCurrentLog(path, previousPath);
81
+ if (shared && !isCurrent?.())
82
+ return false;
83
+ writeFileSync(path, line, { mode: 0o600 });
84
+ return true;
65
85
  };
66
- const writeReset = (reason, scope) => {
67
- if (options.canReset && !options.canReset())
86
+ const writeReset = (reason, scope, path, profile) => {
87
+ const previousPath = resolvePreviousPath();
88
+ const isCurrent = captureRotation(path, previousPath, profile);
89
+ if (canReset && !canReset())
68
90
  return false;
69
- const path = resolvePath();
70
91
  let didReset = false;
71
- withTelegramFileTransaction(`${path}.transaction`, () => {
92
+ withTelegramFileTransaction(transactionPath(path), () => {
72
93
  const commit = () => {
73
- writeResetLocked(path, resolvePreviousPath(), reason, scope);
74
- didReset = true;
94
+ if (shared && !isCurrent?.())
95
+ return;
96
+ if (shared) {
97
+ const line = safeJsonLine({ at: getNowMs(), kind: "reset", reason, scope, profile }) + "\n";
98
+ if (!isCurrent?.())
99
+ return;
100
+ ensureParent(path);
101
+ if (existsSync(path) && statSync(path).size + Buffer.byteLength(line) > maxBytes &&
102
+ !writeResetLocked(path, previousPath, "max-bytes", { maxBytes }, profile, isCurrent))
103
+ return;
104
+ if (!isCurrent?.())
105
+ return;
106
+ appendFileSync(path, line, { mode: 0o600 });
107
+ didReset = true;
108
+ }
109
+ else
110
+ didReset = writeResetLocked(path, previousPath, reason, scope);
75
111
  };
76
- if (options.commitReset) {
77
- options.commitReset(commit);
112
+ if (commitReset) {
113
+ commitReset(commit);
78
114
  }
79
115
  else {
80
116
  commit();
81
117
  }
82
118
  });
83
- return didReset;
119
+ return didReset && (!shared || isCurrent?.() === true);
84
120
  };
85
- const appendLine = (line) => {
86
- queuedAppends.push({
87
- path: resolvePath(),
88
- previousPath: resolvePreviousPath(),
89
- line,
90
- });
121
+ const appendEvent = (event) => {
122
+ const path = resolvePath(), previousPath = resolvePreviousPath(), profile = getProfile();
123
+ const rotationCurrent = captureRotation(path, previousPath, profile);
124
+ const line = safeJsonLine({ kind: "event", ...event, ...(shared ? { profile } : {}) }) + "\n";
125
+ queuedAppends.push({ path, previousPath, line, ...(shared ? { profile, rotationCurrent } : {}) });
91
126
  if (appendScheduled)
92
127
  return;
93
128
  appendScheduled = true;
@@ -101,14 +136,14 @@ export function createTelegramRuntimeJsonlLog(options = {}) {
101
136
  const key = `${entry.path}\u0000${entry.previousPath}`;
102
137
  const group = groups.get(key);
103
138
  if (group)
104
- group.lines.push(entry.line);
139
+ group.entries.push(entry);
105
140
  else
106
- groups.set(key, { ...entry, lines: [entry.line] });
141
+ groups.set(key, { ...entry, entries: [entry] });
107
142
  }
108
143
  for (const group of groups.values()) {
109
144
  try {
110
145
  ensureParent(group.path);
111
- withTelegramFileTransaction(`${group.path}.transaction`, () => {
146
+ withTelegramFileTransaction(transactionPath(group.path), () => {
112
147
  let currentSize = existsSync(group.path)
113
148
  ? statSync(group.path).size
114
149
  : 0;
@@ -122,28 +157,32 @@ export function createTelegramRuntimeJsonlLog(options = {}) {
122
157
  chunk = "";
123
158
  chunkBytes = 0;
124
159
  };
125
- const rotate = () => {
126
- if (options.canReset && !options.canReset())
160
+ const rotate = (entry) => {
161
+ if (canReset && !canReset())
162
+ return false;
163
+ if (shared && !entry.rotationCurrent?.())
127
164
  return false;
128
165
  let rotated = false;
129
166
  const commit = () => {
130
- writeResetLocked(group.path, group.previousPath, "max-bytes", { maxBytes });
131
- rotated = true;
167
+ if (shared && !entry.rotationCurrent?.())
168
+ return;
169
+ rotated = writeResetLocked(group.path, group.previousPath, "max-bytes", { maxBytes }, entry.profile, entry.rotationCurrent);
132
170
  };
133
- if (options.commitReset)
134
- options.commitReset(commit);
171
+ if (commitReset)
172
+ commitReset(commit);
135
173
  else
136
174
  commit();
137
175
  if (rotated)
138
176
  currentSize = statSync(group.path).size;
139
177
  return rotated;
140
178
  };
141
- for (const line of group.lines) {
179
+ for (const entry of group.entries) {
180
+ const line = entry.line;
142
181
  const lineBytes = Buffer.byteLength(line);
143
182
  if (currentSize + chunkBytes > 0 &&
144
183
  currentSize + chunkBytes + lineBytes > maxBytes) {
145
184
  flushChunk();
146
- rotate();
185
+ rotate(entry);
147
186
  }
148
187
  chunk += line;
149
188
  chunkBytes += lineBytes;
@@ -161,10 +200,10 @@ export function createTelegramRuntimeJsonlLog(options = {}) {
161
200
  return {
162
201
  getPath: resolvePath,
163
202
  reset(reason, scope) {
164
- const path = resolvePath();
203
+ const path = resolvePath(), profile = getProfile();
165
204
  try {
166
- if (writeReset(reason, scope)) {
167
- scopeKeys.set(path, scope ? safeJsonLine(scope) : undefined);
205
+ if (writeReset(reason, scope, path, profile)) {
206
+ scopeKeys.set(scopeStorageKey(path, profile), scope ? safeJsonLine(scope) : undefined);
168
207
  }
169
208
  }
170
209
  catch {
@@ -172,12 +211,12 @@ export function createTelegramRuntimeJsonlLog(options = {}) {
172
211
  }
173
212
  },
174
213
  resetIfScopeChanged(nextScopeKey, reason, scope) {
175
- const path = resolvePath();
176
- if (scopeKeys.get(path) === nextScopeKey)
214
+ const path = resolvePath(), profile = getProfile(), key = scopeStorageKey(path, profile);
215
+ if (scopeKeys.get(key) === nextScopeKey)
177
216
  return;
178
217
  try {
179
- if (writeReset(reason, scope))
180
- scopeKeys.set(path, nextScopeKey);
218
+ if (writeReset(reason, scope, path, profile))
219
+ scopeKeys.set(key, nextScopeKey);
181
220
  }
182
221
  catch {
183
222
  // Diagnostics must never break Telegram runtime behavior.
@@ -185,7 +224,7 @@ export function createTelegramRuntimeJsonlLog(options = {}) {
185
224
  },
186
225
  record(event) {
187
226
  try {
188
- appendLine(safeJsonLine({ kind: "event", ...event }) + "\n");
227
+ appendEvent(event);
189
228
  }
190
229
  catch {
191
230
  // Diagnostics must never break Telegram runtime behavior.
@@ -193,19 +232,22 @@ export function createTelegramRuntimeJsonlLog(options = {}) {
193
232
  },
194
233
  };
195
234
  }
196
- export function createTelegramRuntimeDiagnosticsRuntime() {
235
+ export function createTelegramRuntimeDiagnosticsRuntime(options = {}) {
236
+ const sharedFile = options.sharedFile === true;
197
237
  let getBotToken = () => undefined;
198
238
  let getProfileName = () => undefined;
199
239
  let canReset = () => false;
200
240
  let commitReset = (_commit) => false;
241
+ let captureAuthority = () => undefined;
201
242
  let statusPorts;
202
- let requestSnapshotPersist = () => { };
203
243
  const events = Status.createTelegramRuntimeEventRecorder({
204
244
  getBotToken: () => getBotToken(),
205
245
  });
206
246
  const jsonl = createTelegramRuntimeJsonlLog({
207
- path: () => getTelegramRuntimeLogPath(undefined, getProfileName()),
208
- previousPath: () => getTelegramPreviousRuntimeLogPath(undefined, getProfileName()),
247
+ path: () => sharedFile ? resolveTelegramRuntimeLogPath() : getTelegramRuntimeLogPath(undefined, getProfileName()),
248
+ previousPath: () => sharedFile ? resolveTelegramPreviousSharedRuntimeLogPath()
249
+ : getTelegramPreviousRuntimeLogPath(undefined, getProfileName()),
250
+ ...(sharedFile ? { sharedProfiles: { getProfileName: () => getProfileName(), captureAuthority: () => captureAuthority() } } : {}),
209
251
  canReset: () => canReset(),
210
252
  commitReset: (commit) => commitReset(commit),
211
253
  });
@@ -216,11 +258,34 @@ export function createTelegramRuntimeDiagnosticsRuntime() {
216
258
  jsonl.record(latestEvent);
217
259
  requestSnapshotPersist();
218
260
  };
219
- const persistCurrentSnapshot = async () => {
220
- if (!statusPorts)
221
- return;
222
- await statusPorts.persistSnapshot(Status.createTelegramStatusSnapshot(statusPorts.getStatusState()));
261
+ const captureSnapshotScope = () => {
262
+ const ports = statusPorts;
263
+ if (!ports)
264
+ return undefined;
265
+ if (!ports.session)
266
+ return () => statusPorts === ports;
267
+ const session = ports.session, ctx = session.get(), generation = session.getGeneration();
268
+ if (ctx === undefined || !session.isCurrent(ctx, generation))
269
+ return undefined;
270
+ return () => statusPorts === ports && session.isCurrent(ctx, generation);
223
271
  };
272
+ const requestSnapshotPersist = Status.createTelegramRuntimeDiagnosticsSnapshotScheduler({
273
+ ...options.snapshotTimer,
274
+ captureScope: captureSnapshotScope,
275
+ async persistSnapshot(isCurrent) {
276
+ const ports = statusPorts;
277
+ // Projection may acquire journal guards or recover a snapshot: fence before reading it.
278
+ if (!ports || !isCurrent())
279
+ return;
280
+ const snapshot = Status.createTelegramStatusSnapshot(ports.getStatusState());
281
+ if (!isCurrent())
282
+ return;
283
+ await ports.persistSnapshot(snapshot);
284
+ },
285
+ recordError(error) {
286
+ events.record("telegram", error, { phase: "runtime-diagnostics-snapshot-persist" });
287
+ },
288
+ });
224
289
  const updateRuntimeLogScope = function (reason) {
225
290
  if (!statusPorts)
226
291
  return;
@@ -238,21 +303,17 @@ export function createTelegramRuntimeDiagnosticsRuntime() {
238
303
  getProfileName = ports.getProfileName;
239
304
  canReset = ports.canReset;
240
305
  commitReset = ports.commitReset;
306
+ captureAuthority = ports.captureAuthority ?? (() => undefined);
241
307
  },
242
308
  bindStatus(ports) {
243
309
  statusPorts = ports;
244
- requestSnapshotPersist =
245
- Status.createTelegramRuntimeDiagnosticsSnapshotScheduler({
246
- persistSnapshot: persistCurrentSnapshot,
247
- recordError(error) {
248
- events.record("telegram", error, {
249
- phase: "runtime-diagnostics-snapshot-persist",
250
- });
251
- },
252
- });
310
+ if (ports.session)
311
+ void requestSnapshotPersist.suspend();
253
312
  },
313
+ onSessionStart: requestSnapshotPersist.resume,
314
+ onSessionShutdown: requestSnapshotPersist.suspend,
254
315
  updateStatus(ctx, error) {
255
- if (!statusPorts)
316
+ if (!statusPorts || statusPorts.session?.isCurrent(ctx) === false)
256
317
  return;
257
318
  statusPorts.updateStatus(ctx, error);
258
319
  updateRuntimeLogScope("status-scope-change");
@@ -260,11 +321,7 @@ export function createTelegramRuntimeDiagnosticsRuntime() {
260
321
  getStatusLines(options) {
261
322
  if (!statusPorts)
262
323
  return [];
263
- void persistCurrentSnapshot().catch((error) => {
264
- recordRuntimeEvent("telegram", error, {
265
- phase: "status-snapshot-persist",
266
- });
267
- });
324
+ requestSnapshotPersist();
268
325
  return Status.buildTelegramBridgeStatusLines(statusPorts.getStatusState(), options);
269
326
  },
270
327
  scheduleSnapshotPersist() {
@@ -61,9 +61,16 @@ export interface TelegramReplyToMessage {
61
61
  }
62
62
  export interface TelegramSticker {
63
63
  file_id: string;
64
+ is_video?: boolean;
65
+ is_animated?: boolean;
64
66
  }
65
67
  export interface TelegramMediaMessage {
66
68
  message_id: number;
69
+ chat?: {
70
+ id: number;
71
+ type?: string;
72
+ username?: string;
73
+ };
67
74
  from?: TelegramMessageUser;
68
75
  forward_origin?: TelegramMessageForwardOrigin;
69
76
  forward_from?: TelegramMessageUser;
@@ -125,9 +132,29 @@ export interface TelegramMediaGroupControllerOptions {
125
132
  clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
126
133
  }
127
134
  export type TelegramAttachmentKind = "photo" | "document" | "video" | "audio" | "voice" | "animation" | "sticker";
135
+ /** Where an inbound attachment came from; the download layer turns it into a scoped file name. */
136
+ export interface TelegramAttachmentSource {
137
+ kind: string;
138
+ messageId: number;
139
+ index?: number;
140
+ /** Sender-chosen file name, kept as a trailing readable segment. */
141
+ userFileName?: string;
142
+ chat?: {
143
+ id: number;
144
+ type?: string;
145
+ username?: string;
146
+ };
147
+ /** Explicit source scope, e.g. a Guest Mode peer whose message IDs are not the bot chat's. */
148
+ scope?: string;
149
+ }
128
150
  export interface TelegramFileInfo {
129
151
  file_id: string;
130
152
  fileName: string;
153
+ /** Present only when the sender chose the name. */
154
+ userFileName?: string;
155
+ /** Position inside a rich message. */
156
+ index?: number;
157
+ source?: TelegramAttachmentSource;
131
158
  mimeType?: string;
132
159
  kind: TelegramAttachmentKind;
133
160
  isImage: boolean;
@@ -147,20 +174,16 @@ export interface DownloadedTelegramMessageFile {
147
174
  kind?: TelegramAttachmentKind;
148
175
  }
149
176
  export interface DownloadTelegramMessageFilesDeps {
150
- downloadFile: (fileId: string, fileName: string) => Promise<string>;
177
+ downloadFile: (fileId: string, fileName: string, source?: TelegramAttachmentSource) => Promise<string>;
151
178
  }
152
- export declare function guessExtensionFromMime(mimeType: string | undefined, fallback: string): string;
153
179
  export declare function guessMediaType(path: string): string | undefined;
154
180
  export declare function extractTelegramMessageText(message: TelegramMediaMessage): string;
155
181
  export declare function extractTelegramForwardContextText(message: TelegramMediaMessage, allowedUserId?: number): string;
156
- export declare function extractTelegramReplyContextText(message: TelegramMediaMessage): string;
157
182
  export declare function buildTelegramReplyContextBlock(message: TelegramMediaMessage, replyFiles?: Pick<DownloadedTelegramFile, "path">[], replyOutputs?: readonly string[]): string;
158
- export declare function appendTelegramReplyContext(text: string, replyContext: string): string;
159
183
  export declare function extractTelegramMessagePromptText(message: TelegramMediaMessage): string;
160
184
  export declare function extractTelegramMessagesText(messages: TelegramMediaMessage[]): string;
161
185
  export declare function extractTelegramMessagesPromptText(messages: TelegramMediaMessage[]): string;
162
186
  export declare function extractFirstTelegramMessageText(messages: TelegramMediaMessage[]): string;
163
- export declare function hasTelegramMessagePromptContent(message: TelegramMediaMessage): boolean;
164
187
  export declare function hasTelegramMessagesPromptContent(messages: TelegramMediaMessage[]): boolean;
165
188
  export declare function collectTelegramMessageIds(messages: TelegramMediaMessage[]): number[];
166
189
  export declare function getTelegramMediaGroupKey(message: TelegramMediaGroupMessage): string | undefined;
@@ -6,7 +6,7 @@
6
6
  import { basename, dirname } from "node:path";
7
7
  const TELEGRAM_MEDIA_GROUP_DEBOUNCE_MS = 1200;
8
8
  const TELEGRAM_REPLY_CONTEXT_MAX_LENGTH = 1000;
9
- export function guessExtensionFromMime(mimeType, fallback) {
9
+ function guessExtensionFromMime(mimeType, fallback) {
10
10
  if (!mimeType)
11
11
  return fallback;
12
12
  const normalized = mimeType.toLowerCase();
@@ -162,7 +162,7 @@ export function extractTelegramForwardContextText(message, allowedUserId) {
162
162
  return "";
163
163
  return `from: ${origin}`;
164
164
  }
165
- export function extractTelegramReplyContextText(message) {
165
+ function extractTelegramReplyContextText(message) {
166
166
  const quoted = (extractTelegramRichMessageText(message.reply_to_message?.rich_message) ||
167
167
  message.reply_to_message?.text ||
168
168
  message.reply_to_message?.caption ||
@@ -195,7 +195,7 @@ export function buildTelegramReplyContextBlock(message, replyFiles = [], replyOu
195
195
  }
196
196
  return "";
197
197
  }
198
- export function appendTelegramReplyContext(text, replyContext) {
198
+ function appendTelegramReplyContext(text, replyContext) {
199
199
  if (!replyContext)
200
200
  return text;
201
201
  return text ? `${text}\n\n${replyContext}` : `_\n\n${replyContext}`;
@@ -216,7 +216,7 @@ export function extractTelegramMessagesPromptText(messages) {
216
216
  export function extractFirstTelegramMessageText(messages) {
217
217
  return messages.map(extractTelegramMessageText).find(Boolean) ?? "";
218
218
  }
219
- export function hasTelegramMessagePromptContent(message) {
219
+ function hasTelegramMessagePromptContent(message) {
220
220
  return (!!extractTelegramMessageText(message) ||
221
221
  (Array.isArray(message.photo) && message.photo.length > 0) ||
222
222
  !!message.document ||
@@ -422,7 +422,7 @@ export async function downloadTelegramMessageFiles(messages, deps) {
422
422
  const downloaded = [];
423
423
  for (const file of collectTelegramFileInfos(messages)) {
424
424
  downloaded.push({
425
- path: await deps.downloadFile(file.file_id, file.fileName),
425
+ path: await deps.downloadFile(file.file_id, file.fileName, file.source),
426
426
  fileName: file.fileName,
427
427
  isImage: file.isImage,
428
428
  mimeType: file.mimeType,
@@ -463,6 +463,7 @@ function collectTelegramRichBlockFileInfos(blocks, messageId) {
463
463
  files.push({
464
464
  file_id: photo.file_id,
465
465
  fileName: `photo-${messageId}-${mediaIndex}.jpg`,
466
+ index: mediaIndex,
466
467
  mimeType: "image/jpeg",
467
468
  kind: "photo",
468
469
  isImage: true,
@@ -489,6 +490,8 @@ function collectTelegramRichBlockFileInfos(blocks, messageId) {
489
490
  const fallbackExtension = kind === "voice" ? ".ogg" : kind === "audio" ? ".mp3" : ".mp4";
490
491
  files.push({
491
492
  file_id: fileId,
493
+ index: mediaIndex,
494
+ ...(typeof fileName === "string" ? { userFileName: fileName } : {}),
492
495
  fileName: typeof fileName === "string"
493
496
  ? fileName
494
497
  : `${kind}-${messageId}-${mediaIndex}${guessExtensionFromMime(typeof mimeType === "string" ? mimeType : undefined, fallbackExtension)}`,
@@ -512,6 +515,7 @@ function collectTelegramRichBlockFileInfos(blocks, messageId) {
512
515
  export function collectTelegramFileInfos(messages) {
513
516
  const files = [];
514
517
  for (const message of messages) {
518
+ const firstOfMessage = files.length;
515
519
  files.push(...collectTelegramRichBlockFileInfos(message.rich_message?.blocks, message.message_id));
516
520
  if (Array.isArray(message.photo) && message.photo.length > 0) {
517
521
  const photo = [...message.photo]
@@ -533,6 +537,7 @@ export function collectTelegramFileInfos(messages) {
533
537
  files.push({
534
538
  file_id: message.document.file_id,
535
539
  fileName,
540
+ ...(message.document.file_name ? { userFileName: message.document.file_name } : {}),
536
541
  mimeType: message.document.mime_type,
537
542
  kind: "document",
538
543
  isImage: isImageMimeType(message.document.mime_type),
@@ -544,6 +549,7 @@ export function collectTelegramFileInfos(messages) {
544
549
  files.push({
545
550
  file_id: message.video.file_id,
546
551
  fileName,
552
+ ...(message.video.file_name ? { userFileName: message.video.file_name } : {}),
547
553
  mimeType: message.video.mime_type,
548
554
  kind: "video",
549
555
  isImage: false,
@@ -556,6 +562,7 @@ export function collectTelegramFileInfos(messages) {
556
562
  files.push({
557
563
  file_id: message.audio.file_id,
558
564
  fileName,
565
+ ...(message.audio.file_name ? { userFileName: message.audio.file_name } : {}),
559
566
  mimeType: message.audio.mime_type,
560
567
  kind: "audio",
561
568
  isImage: false,
@@ -577,20 +584,31 @@ export function collectTelegramFileInfos(messages) {
577
584
  files.push({
578
585
  file_id: message.animation.file_id,
579
586
  fileName,
587
+ ...(message.animation.file_name ? { userFileName: message.animation.file_name } : {}),
580
588
  mimeType: message.animation.mime_type,
581
589
  kind: "animation",
582
590
  isImage: false,
583
591
  });
584
592
  }
585
593
  if (message.sticker) {
594
+ // Sticker format is defined by Bot API flags, not a documented MIME field.
595
+ const video = message.sticker.is_video === true;
596
+ const animated = message.sticker.is_animated === true;
597
+ const extension = video ? ".webm" : animated ? ".tgs" : ".webp";
586
598
  files.push({
587
599
  file_id: message.sticker.file_id,
588
- fileName: `sticker-${message.message_id}.webp`,
589
- mimeType: "image/webp",
600
+ fileName: `sticker-${message.message_id}${extension}`,
601
+ mimeType: video ? "video/webm" : animated ? "application/x-tgsticker" : "image/webp",
590
602
  kind: "sticker",
591
- isImage: true,
603
+ isImage: !video && !animated,
592
604
  });
593
605
  }
606
+ for (const file of files.slice(firstOfMessage)) {
607
+ file.source = { kind: file.kind, messageId: message.message_id,
608
+ ...(file.index !== undefined ? { index: file.index } : {}),
609
+ ...(file.userFileName ? { userFileName: file.userFileName } : {}),
610
+ ...(message.chat ? { chat: message.chat } : {}) };
611
+ }
594
612
  }
595
613
  const seenFileIds = new Set();
596
614
  return files.filter((file) => {
@@ -177,7 +177,6 @@ export interface TelegramModelMenuRuntime<TModel extends MenuModel = MenuModel>
177
177
  export declare const TELEGRAM_MODEL_PAGE_SIZE = 6;
178
178
  export declare const MODEL_MENU_TITLE = "<b>\uD83E\uDD16 Choose a model:</b>";
179
179
  export declare const MODEL_PAGE_MENU_TITLE = "<b>Choose a page:</b>";
180
- export declare const MODEL_DETAIL_MENU_TITLE = "<b>\uD83E\uDD16 Model:</b>";
181
180
  export declare function formatScopedModelButtonText<TModel extends MenuModel = MenuModel>(entry: ScopedTelegramModel<TModel>, currentModel: TModel | undefined): string;
182
181
  export declare function formatStatusButtonLabel(label: string, value: string): string;
183
182
  export declare function getModelMenuItems<TModel extends MenuModel = MenuModel>(state: TelegramModelMenuState<TModel>): ScopedTelegramModel<TModel>[];
@@ -195,21 +194,11 @@ export declare function buildTelegramModelMenuStateRuntime<TContext extends Tele
195
194
  export declare function applyTelegramModelScopeSelection(state: TelegramModelMenuState, value: string | undefined): TelegramMenuMutationResult;
196
195
  export declare function applyTelegramModelPageSelection(state: TelegramModelMenuState, value: string | undefined): TelegramMenuMutationResult;
197
196
  export declare function getTelegramModelSelection<TModel extends MenuModel = MenuModel>(state: TelegramModelMenuState<TModel>, value: string | undefined): TelegramMenuSelectionResult<TModel>;
198
- export declare function applyTelegramModelDetailSelection(state: TelegramModelMenuState, value: string | undefined): TelegramMenuMutationResult;
199
- export declare function getTelegramSelectedDetailModel<TModel extends MenuModel = MenuModel>(state: TelegramModelMenuState<TModel>): TelegramMenuSelectionResult<TModel>;
200
- export declare function isTelegramModelScoped(state: TelegramModelMenuState, model: MenuModel): boolean;
201
- export declare function focusTelegramModelListPage(state: TelegramModelMenuState, model: MenuModel, pageSize?: number): void;
202
- export declare function setTelegramModelScope(state: TelegramModelMenuState, model: MenuModel, enabled: boolean): {
203
- patterns: string[];
204
- enabled: boolean;
205
- };
206
197
  export declare function buildTelegramModelCallbackPlan<TModel extends MenuModel = MenuModel>(params: BuildTelegramModelCallbackPlanParams<TModel>): TelegramModelCallbackPlan<TModel>;
207
198
  export declare function openTelegramModelMenu<TModel extends MenuModel = MenuModel>(deps: TelegramModelMenuOpenDeps<TModel>): Promise<void>;
208
199
  export declare function handleTelegramModelMenuCallbackAction<TModel extends MenuModel = MenuModel>(callbackQueryId: string, params: BuildTelegramModelCallbackPlanParams<TModel>, deps: TelegramModelMenuCallbackDeps<TModel>): Promise<boolean>;
209
200
  export declare function getTelegramModelMenuPage(state: TelegramModelMenuState, pageSize: number): TelegramModelMenuPage;
210
201
  export declare function buildModelMenuReplyMarkup(state: TelegramModelMenuState, currentModel: MenuModel | undefined, pageSize: number): TelegramReplyMarkup;
211
- export declare function buildModelDetailMenuReplyMarkup(state: TelegramModelMenuState, currentModel: MenuModel | undefined): TelegramReplyMarkup;
212
- export declare function buildModelDetailMenuText(state: TelegramModelMenuState): string;
213
202
  export declare function buildModelPageMenuReplyMarkup(state: TelegramModelMenuState, pageSize: number): TelegramReplyMarkup;
214
203
  export declare function buildTelegramModelPageMenuRenderPayload(state: TelegramModelMenuState): TelegramMenuRenderPayload;
215
204
  export declare function buildTelegramModelMenuRenderPayload(state: TelegramModelMenuState, activeModel: MenuModel | undefined): TelegramMenuRenderPayload;
@@ -11,7 +11,7 @@ export const TELEGRAM_MODEL_PAGE_SIZE = 6;
11
11
  const TELEGRAM_MODEL_PAGE_PICKER_ROW_SIZE = 4;
12
12
  export const MODEL_MENU_TITLE = "<b>🤖 Choose a model:</b>";
13
13
  export const MODEL_PAGE_MENU_TITLE = "<b>Choose a page:</b>";
14
- export const MODEL_DETAIL_MENU_TITLE = "<b>🤖 Model:</b>";
14
+ const MODEL_DETAIL_MENU_TITLE = "<b>🤖 Model:</b>";
15
15
  function truncateTelegramButtonLabel(label, maxLength = 56) {
16
16
  return label.length <= maxLength
17
17
  ? label
@@ -249,7 +249,7 @@ export function getTelegramModelSelection(state, value) {
249
249
  return { kind: "missing" };
250
250
  return { kind: "selected", selection };
251
251
  }
252
- export function applyTelegramModelDetailSelection(state, value) {
252
+ function applyTelegramModelDetailSelection(state, value) {
253
253
  const index = Number(value);
254
254
  if (!Number.isFinite(index))
255
255
  return "invalid";
@@ -261,7 +261,7 @@ export function applyTelegramModelDetailSelection(state, value) {
261
261
  state.mode = "model-detail";
262
262
  return "changed";
263
263
  }
264
- export function getTelegramSelectedDetailModel(state) {
264
+ function getTelegramSelectedDetailModel(state) {
265
265
  const indexedSelection = getTelegramModelSelection(state, state.selectedModelIndex?.toString());
266
266
  if (indexedSelection.kind === "selected") {
267
267
  if (!state.selectedModelKey)
@@ -279,11 +279,11 @@ export function getTelegramSelectedDetailModel(state) {
279
279
  }
280
280
  return indexedSelection;
281
281
  }
282
- export function isTelegramModelScoped(state, model) {
282
+ function isTelegramModelScoped(state, model) {
283
283
  const key = getCanonicalModelId(model);
284
284
  return state.scopedModels.some((entry) => getCanonicalModelId(entry.model) === key);
285
285
  }
286
- export function focusTelegramModelListPage(state, model, pageSize = TELEGRAM_MODEL_PAGE_SIZE) {
286
+ function focusTelegramModelListPage(state, model, pageSize = TELEGRAM_MODEL_PAGE_SIZE) {
287
287
  const key = getCanonicalModelId(model).toLowerCase();
288
288
  const index = getModelMenuItems(state).findIndex((entry) => getCanonicalModelId(entry.model).toLowerCase() === key);
289
289
  state.page = index < 0 ? 0 : Math.floor(index / pageSize);
@@ -293,7 +293,7 @@ function formatScopedModelPattern(entry) {
293
293
  const key = getCanonicalModelId(entry.model);
294
294
  return entry.thinkingLevel ? `${key}:${entry.thinkingLevel}` : key;
295
295
  }
296
- export function setTelegramModelScope(state, model, enabled) {
296
+ function setTelegramModelScope(state, model, enabled) {
297
297
  const key = getCanonicalModelId(model);
298
298
  const lowerKey = key.toLowerCase();
299
299
  const scopedModelPatterns = state.scopedModelPatterns ?? [];
@@ -583,7 +583,7 @@ export function buildModelMenuReplyMarkup(state, currentModel, pageSize) {
583
583
  ]));
584
584
  return { inline_keyboard: rows };
585
585
  }
586
- export function buildModelDetailMenuReplyMarkup(state, currentModel) {
586
+ function buildModelDetailMenuReplyMarkup(state, currentModel) {
587
587
  const selection = getTelegramSelectedDetailModel(state);
588
588
  if (selection.kind !== "selected") {
589
589
  return {
@@ -617,7 +617,7 @@ export function buildModelDetailMenuReplyMarkup(state, currentModel) {
617
617
  ],
618
618
  };
619
619
  }
620
- export function buildModelDetailMenuText(state) {
620
+ function buildModelDetailMenuText(state) {
621
621
  const selection = getTelegramSelectedDetailModel(state);
622
622
  if (selection.kind !== "selected")
623
623
  return MODEL_DETAIL_MENU_TITLE;