@deadragdoll/tellymcp 0.0.7 → 0.0.8

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.
package/README-ru.md CHANGED
@@ -73,6 +73,12 @@ Telegram HITL здесь тоже есть, но он не исчерпывае
73
73
  - partner notes и partner files
74
74
  - tools sync и version checks
75
75
 
76
+ Важно для `Share`:
77
+
78
+ - текущая сессия должна выполнить работу сама
79
+ - target-сессии отправляется только результат
80
+ - исходное поручение не должно пересылаться дальше как новая задача
81
+
76
82
  Полный список MCP tools лучше держать ниже по README и в самом MCP server, а не на первом экране.
77
83
 
78
84
  ## Prerequisites
@@ -121,6 +127,10 @@ tmux attach -t backend
121
127
 
122
128
  Если агентов несколько, лучше запускать каждого в своей tmux-сессии или pane и привязывать отдельно.
123
129
 
130
+ Если tmux pane пересоздан и его `pane_id` изменился, TellyMCP теперь пытается автоматически восстановить актуальный target по сохранённым tmux session/window/pane hints.
131
+
132
+ Если авто-восстановление не удалось, в Telegram придёт operational warning, а не только запись в backend log.
133
+
124
134
  ## Быстрый старт
125
135
 
126
136
  ### 1. Standalone client без шлюза
package/README.md CHANGED
@@ -122,6 +122,10 @@ Use short, meaningful names such as:
122
122
 
123
123
  If you run multiple agents, put each one in its own tmux session or pane and pair them separately.
124
124
 
125
+ If a tmux pane is recreated and its pane id changes, TellyMCP now tries to recover the live pane target automatically from saved tmux session, window, and pane hints.
126
+
127
+ If auto-recovery fails, Telegram sends an operational warning so the problem is visible to the human user, not only in backend logs.
128
+
125
129
  ## Quick start
126
130
 
127
131
  ### Standalone client node
@@ -490,6 +494,8 @@ Current file model:
490
494
  - `vfs/minio` are no longer part of the active Telegram file exchange path
491
495
  - if an agent must send a real local file to a partner, prefer `send_partner_file`
492
496
  over plain `send_partner_note`
497
+ - for `Share`, the current session must do the work itself and send only the result
498
+ - `Share` must not forward the original task into the target session as a new assignment
493
499
 
494
500
  Current presence model:
495
501
 
package/VERSION.md CHANGED
@@ -4,6 +4,35 @@ Public, user-facing release notes for published versions of `@deadragdoll/tellym
4
4
 
5
5
  For detailed engineering history, refactors, and internal development notes, see [CHANGELOG.md](CHANGELOG.md).
6
6
 
7
+ ## 0.0.8
8
+
9
+ ### Added
10
+ - Unified logging model based on `pino`:
11
+ - pretty console output by default
12
+ - optional JSONL file sink for Alloy or other collectors
13
+ - `LOG_FILE_ENABLED=true`
14
+ - `LOG_FILE_PATH=.tellymcp/log.jsonl`
15
+ - Better tmux recovery behavior:
16
+ - when a saved pane target becomes stale after tmux recreation, TellyMCP now tries to recover the live pane automatically from stored tmux session/window/pane hints
17
+ - if auto-recovery fails, Telegram sends a clear operational warning instead of leaving the problem only in logs
18
+ - Stronger `Share` execution guidance:
19
+ - the current session must do the work itself
20
+ - it must send only the result
21
+ - it must not forward the original task to the target session as a new assignment
22
+
23
+ ### Changed
24
+ - Runtime identity and service labels now use `tellymcp` naming consistently instead of older `telegram-human-mcp` tags.
25
+ - MCP server metadata now reports the current package version and `tellymcp` service name.
26
+ - Logging config is now simpler:
27
+ - one console logging model
28
+ - one optional JSON file sink
29
+ - optional `LogFeed` buffer for UI diagnostics
30
+
31
+ ### Fixed
32
+ - Stale tmux pane ids like `%1 -> %2` no longer require manual user understanding before the service can try to wake the session again.
33
+ - Broken tmux nudge targets are now visible to the user in Telegram, not only in backend logs.
34
+ - `Share` inbox instructions are now explicit enough to reduce the chance that one agent re-delegates the task back into the collaboration graph.
35
+
7
36
  ## 0.0.3
8
37
 
9
38
  ### Added
@@ -19,6 +19,7 @@ const proxyFetch_1 = require("./proxyFetch");
19
19
  const client_1 = require("../tmux/client");
20
20
  const versionHandshake_1 = require("../../lib/version/versionHandshake");
21
21
  const LOCAL_INDEX_FILE_NAME = "LOCAL_INDEX.md";
22
+ const TMUX_NUDGE_FAILURE_NOTICE_COOLDOWN_MS = 5 * 60 * 1000;
22
23
  function trimTrailingSlashes(value) {
23
24
  return value.replace(/\/+$/u, "");
24
25
  }
@@ -306,6 +307,7 @@ class TelegramTransport {
306
307
  screenshotMessageMenu;
307
308
  waiters = new Map();
308
309
  tmuxNudgeDebounceTimers = new Map();
310
+ tmuxNudgeFailureNoticeAt = new Map();
309
311
  pendingRenames = new Map();
310
312
  pendingBroadcasts = new Map();
311
313
  pendingPartnerNotes = new Map();
@@ -2585,22 +2587,117 @@ class TelegramTransport {
2585
2587
  return;
2586
2588
  }
2587
2589
  await this.sendTypingForSession(sessionId);
2588
- await (0, client_1.sendTmuxLiteralLine)(this.config.tmux, session.tmuxTarget, input.message);
2590
+ let tmuxTarget = session.tmuxTarget;
2591
+ try {
2592
+ await (0, client_1.sendTmuxLiteralLine)(this.config.tmux, tmuxTarget, input.message);
2593
+ }
2594
+ catch (error) {
2595
+ if ((0, client_1.isTmuxTargetInvalidError)(error)) {
2596
+ const recoveredTarget = await this.tryRecoverTmuxTarget(sessionId, session);
2597
+ if (recoveredTarget) {
2598
+ tmuxTarget = recoveredTarget;
2599
+ await (0, client_1.sendTmuxLiteralLine)(this.config.tmux, recoveredTarget, input.message);
2600
+ }
2601
+ else {
2602
+ await this.notifyTmuxTargetInvalid(sessionId, session, error);
2603
+ throw error;
2604
+ }
2605
+ }
2606
+ else {
2607
+ throw error;
2608
+ }
2609
+ }
2589
2610
  const lastTmuxNudgeAt = new Date(nowMs).toISOString();
2590
2611
  await this.sessionStore.setSession({
2591
2612
  ...session,
2613
+ tmuxTarget,
2614
+ ...(tmuxTarget ? { tmuxPaneId: tmuxTarget } : {}),
2592
2615
  lastTmuxNudgeAt,
2593
2616
  });
2617
+ this.tmuxNudgeFailureNoticeAt.delete(sessionId);
2594
2618
  this.logger.info("tmux nudge sent", {
2595
2619
  sessionId,
2596
2620
  reason: input.reason,
2597
2621
  message: input.message,
2598
2622
  tmuxSessionName: session.tmuxSessionName,
2599
- tmuxTarget: session.tmuxTarget,
2623
+ tmuxTarget,
2600
2624
  inboxCount,
2601
2625
  lastTmuxNudgeAt,
2602
2626
  });
2603
2627
  }
2628
+ async tryRecoverTmuxTarget(sessionId, session) {
2629
+ const recoveredTarget = await (0, client_1.resolveTmuxTargetFromHint)(this.config.tmux, {
2630
+ tmuxSessionName: session.tmuxSessionName,
2631
+ tmuxWindowName: session.tmuxWindowName,
2632
+ tmuxWindowIndex: session.tmuxWindowIndex,
2633
+ tmuxPaneId: session.tmuxPaneId,
2634
+ tmuxPaneIndex: session.tmuxPaneIndex,
2635
+ tmuxTarget: session.tmuxTarget,
2636
+ });
2637
+ if (!recoveredTarget || recoveredTarget === session.tmuxTarget) {
2638
+ return recoveredTarget;
2639
+ }
2640
+ await this.sessionStore.setSession({
2641
+ ...session,
2642
+ tmuxTarget: recoveredTarget,
2643
+ tmuxPaneId: recoveredTarget,
2644
+ updatedAt: new Date().toISOString(),
2645
+ });
2646
+ this.logger.warn("tmux target auto-recovered", {
2647
+ sessionId,
2648
+ previousTmuxTarget: session.tmuxTarget,
2649
+ recoveredTmuxTarget: recoveredTarget,
2650
+ tmuxSessionName: session.tmuxSessionName,
2651
+ tmuxWindowName: session.tmuxWindowName,
2652
+ tmuxWindowIndex: session.tmuxWindowIndex,
2653
+ tmuxPaneIndex: session.tmuxPaneIndex,
2654
+ });
2655
+ return recoveredTarget;
2656
+ }
2657
+ async notifyTmuxTargetInvalid(sessionId, session, error) {
2658
+ const binding = await this.bindingStore.getBinding(sessionId);
2659
+ if (!binding) {
2660
+ return;
2661
+ }
2662
+ const nowMs = Date.now();
2663
+ const lastNoticeAt = this.tmuxNudgeFailureNoticeAt.get(sessionId);
2664
+ if (lastNoticeAt &&
2665
+ nowMs - lastNoticeAt < TMUX_NUDGE_FAILURE_NOTICE_COOLDOWN_MS) {
2666
+ return;
2667
+ }
2668
+ this.tmuxNudgeFailureNoticeAt.set(sessionId, nowMs);
2669
+ const sessionLabel = session.label ?? sessionId;
2670
+ const tmuxTarget = session.tmuxTarget ?? "unknown";
2671
+ const errorMessage = error instanceof Error ? error.message : String(error);
2672
+ try {
2673
+ await this.sendNotification({
2674
+ sessionId,
2675
+ sessionLabel: "TellyMCP",
2676
+ recipient: {
2677
+ telegramChatId: binding.telegramChatId,
2678
+ telegramUserId: binding.telegramUserId,
2679
+ },
2680
+ message: [
2681
+ `⚠️ Автоматический tmux nudge для сессии ${sessionLabel} не сработал.`,
2682
+ `Сохранённый tmux target больше недействителен: ${tmuxTarget}`,
2683
+ `Ошибка: ${errorMessage}`,
2684
+ "Обычно это значит, что pane/window/session был пересоздан.",
2685
+ "Перепривяжи tmux target для этой сессии.",
2686
+ ].join("\n"),
2687
+ });
2688
+ }
2689
+ catch (notifyError) {
2690
+ this.logger.warn("Failed to deliver tmux target failure notification", {
2691
+ sessionId,
2692
+ tmuxTarget,
2693
+ telegramChatId: binding.telegramChatId,
2694
+ telegramUserId: binding.telegramUserId,
2695
+ notifyError: notifyError instanceof Error
2696
+ ? (notifyError.stack ?? notifyError.message)
2697
+ : String(notifyError),
2698
+ });
2699
+ }
2700
+ }
2604
2701
  async sendTypingForSession(sessionId) {
2605
2702
  const binding = await this.bindingStore.getBinding(sessionId);
2606
2703
  if (!binding) {
@@ -6454,7 +6551,7 @@ class TelegramTransport {
6454
6551
  telegramUserId: input.principal.telegramUserId,
6455
6552
  sourceTelegramMessageId: input.sourceTelegramMessageId,
6456
6553
  text: [
6457
- "Пользователь просит текущую сессию подготовить сообщение для другой сессии.",
6554
+ "Пользователь просит текущую сессию выполнить работу и отправить результат другой сессии.",
6458
6555
  `Маршрут отправки: ${sourceLabel} -> ${targetLabel}`,
6459
6556
  `Тип: ${input.kind}`,
6460
6557
  `Кратко: ${input.summary}`,
@@ -6466,6 +6563,9 @@ class TelegramTransport {
6466
6563
  "Содержимое для отправки:",
6467
6564
  input.message,
6468
6565
  "",
6566
+ "Не пересылай это как новую задачу в target-сессию.",
6567
+ "Сначала выполни работу в текущей сессии сам.",
6568
+ "Через send_partner_note или send_partner_file отправляй только результат, а не исходное поручение.",
6469
6569
  "Не используй linked partner для отправки. Передай target_session_id явно в send_partner_note.",
6470
6570
  "После подготовки обязательно используй send_partner_note.",
6471
6571
  "Задача не завершена, пока send_partner_note не отработал успешно.",
@@ -10,6 +10,8 @@ exports.listXchangeFiles = listXchangeFiles;
10
10
  exports.deleteXchangeFile = deleteXchangeFile;
11
11
  exports.readWorkspaceFile = readWorkspaceFile;
12
12
  exports.isTmuxUnavailableError = isTmuxUnavailableError;
13
+ exports.isTmuxTargetInvalidError = isTmuxTargetInvalidError;
14
+ exports.resolveTmuxTargetFromHint = resolveTmuxTargetFromHint;
13
15
  exports.getTmuxWindowHeight = getTmuxWindowHeight;
14
16
  exports.captureTmuxPaneRange = captureTmuxPaneRange;
15
17
  exports.captureVisibleTmuxPane = captureVisibleTmuxPane;
@@ -189,6 +191,79 @@ function isTmuxUnavailableError(error) {
189
191
  message.includes("ENOENT") ||
190
192
  message.includes("tmux is unavailable"));
191
193
  }
194
+ function isTmuxTargetInvalidError(error) {
195
+ const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
196
+ return (message.includes("can't find pane") ||
197
+ message.includes("can't find window") ||
198
+ message.includes("can't find session"));
199
+ }
200
+ async function listTmuxPanes(config) {
201
+ const { stdout } = await execFileOutputAsync("tmux", buildTmuxArgs(config, [
202
+ "list-panes",
203
+ "-a",
204
+ "-F",
205
+ "#{session_name}\t#{window_name}\t#{window_index}\t#{pane_id}\t#{pane_index}",
206
+ ]));
207
+ return stdout
208
+ .split("\n")
209
+ .map((line) => line.trim())
210
+ .filter((line) => line.length > 0)
211
+ .map((line) => {
212
+ const [sessionName = "", windowName = "", windowIndexRaw = "", paneId = "", paneIndexRaw = ""] = line.split("\t");
213
+ return {
214
+ sessionName,
215
+ windowName,
216
+ windowIndex: Number.parseInt(windowIndexRaw, 10),
217
+ paneId,
218
+ paneIndex: Number.parseInt(paneIndexRaw, 10),
219
+ };
220
+ })
221
+ .filter((pane) => pane.sessionName &&
222
+ pane.paneId &&
223
+ Number.isFinite(pane.windowIndex) &&
224
+ Number.isFinite(pane.paneIndex));
225
+ }
226
+ async function resolveTmuxTargetFromHint(config, hint) {
227
+ const panes = await listTmuxPanes(config);
228
+ const byPaneId = hint.tmuxPaneId
229
+ ? panes.find((pane) => pane.paneId === hint.tmuxPaneId)
230
+ : null;
231
+ if (byPaneId) {
232
+ return byPaneId.paneId;
233
+ }
234
+ const exactMatch = panes.find((pane) => {
235
+ if (!hint.tmuxSessionName) {
236
+ return false;
237
+ }
238
+ if (pane.sessionName !== hint.tmuxSessionName) {
239
+ return false;
240
+ }
241
+ if (typeof hint.tmuxWindowIndex === "number" &&
242
+ pane.windowIndex !== hint.tmuxWindowIndex) {
243
+ return false;
244
+ }
245
+ if (typeof hint.tmuxPaneIndex === "number" &&
246
+ pane.paneIndex !== hint.tmuxPaneIndex) {
247
+ return false;
248
+ }
249
+ if (hint.tmuxWindowName &&
250
+ pane.windowName !== hint.tmuxWindowName) {
251
+ return false;
252
+ }
253
+ return true;
254
+ });
255
+ if (exactMatch) {
256
+ return exactMatch.paneId;
257
+ }
258
+ const fallbackBySessionAndPane = panes.find((pane) => {
259
+ if (!hint.tmuxSessionName || typeof hint.tmuxPaneIndex !== "number") {
260
+ return false;
261
+ }
262
+ return (pane.sessionName === hint.tmuxSessionName &&
263
+ pane.paneIndex === hint.tmuxPaneIndex);
264
+ });
265
+ return fallbackBySessionAndPane?.paneId ?? null;
266
+ }
192
267
  async function getTmuxWindowHeight(config, target) {
193
268
  const { stdout: heightRaw } = await execFileOutputAsync("tmux", buildTmuxArgs(config, ["display-message", "-p", "-t", target, "#{window_height}"]));
194
269
  const height = Number.parseInt(heightRaw.trim(), 10);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deadragdoll/tellymcp",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "TellyMCP - Telegram control plane for MCP-connected coding agents",
5
5
  "main": "dist/services/features/telegram-mcp/runtime.service.js",
6
6
  "bin": {