@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
|
-
|
|
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
|
|
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