dsh-telegram-multiagent 1.3.0 → 1.4.1

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.md CHANGED
@@ -1,123 +1,172 @@
1
1
  # dsh-telegram-multiagent
2
2
 
3
- A Telegram channel for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): message
4
- your agent from a phone, get answers back, keep one conversation per chat.
3
+ Канал Telegram для платформы DeepSeek Harness. Один модуль на машину обслуживает
4
+ несколько агентов: у каждого свой бот, свой файл токена и свой список тех, кому он
5
+ отвечает. Агенты не видят переписку друг друга.
5
6
 
6
- The harness ships no messenger channel — there is no channel abstraction in the product at all. This
7
- plugin builds one out of the two ends the harness does give you:
7
+ ## Установка
8
8
 
9
- ```
10
- in: ctx.agents.create/resume(...) → agent.send(message)
11
- out: ctx.on('session/event', ...) → send to Telegram
12
- ```
9
+ npm install dsh-telegram-multiagent
13
10
 
14
- One chat = one session = one agent. Sessions live independently, the way the core intends.
11
+ Строка подключения кладётся в пресет агента (см. «Куда класть строку» ниже):
15
12
 
16
- ## Install
13
+ # внутри каталога профиля ($DSH_HOME/profiles/<имя>)
17
14
 
18
- ```bash
19
- # inside your profile directory ($DSH_HOME/profiles/<name>)
20
- pnpm add dsh-telegram-multiagent
21
- ```
15
+ ## Настройки
22
16
 
23
- Then add one row to your **agent preset** (`agent.cordis.yml`), not to the profile patch layer —
24
- see "Where to put the row" below:
17
+ | Поле | Обязательно | Что делает |
18
+ |---|---|---|
19
+ | `tokenFile` | одно из трёх | Путь к файлу с токеном бота. Предпочтительно: секрет принадлежит машине, настройка несёт только путь. |
20
+ | `token` / `tokenEnv` | одно из трёх | Сам токен либо имя переменной окружения. Для отладки. |
21
+ | `appDir` | да | Каталог, где установлена платформа этого агента. Пакеты платформы разрешаются **отсюда**, а не от места самого плагина — см. «Почему `appDir`». |
22
+ | `agentName` | нет | Подпись в строках журнала. Полезно, когда на машине несколько ботов. |
23
+ | `allowedUsers` | нет | Числовые идентификаторы тех, кому можно говорить с агентом. **Пусто означает «всем»** — а у агента обычно есть настоящий доступ к машине, так что задайте. |
24
+ | `workspace` | нет | Рабочий каталог, передаваемый сессии агента. |
25
+ | `preset` | нет | Пресет агента для сессий, созданных этим каналом. |
26
+ | `provider` / `model` | нет | Запасной вариант, если в установке нет службы модели по умолчанию. |
27
+ | `a2aDir` | нет | Каталог файлового канала между агентами (`in/`, `out/`). Не задан — канала нет. |
28
+ | `a2aSession` | нет | Идентификатор сессии для этого канала. По умолчанию `a2a`. |
29
+ | `spoolDir` | нет | Системный почтовый ящик агента. Не задан — ящик не читается. |
30
+ | `transcribeCommand` | нет | Внешняя команда для голосовых: `<команда> <файл> auto` → расшифровка в stdout. Не задана — голос вежливо отклоняется. |
31
+ | `goalUsers` | нет | Кому можно ставить цель из Telegram. **Пусто — команда отвергается для всех.** Намеренно не наследуется от `allowedUsers`: говорить с агентом и запускать ему платный автономный цикл — разные права. |
32
+ | `goalA2ASenders` | нет | Кому можно ставить цель из канала между агентами. Отправитель называет себя первой строкой файла `From: <имя>`. Это учёт, а не удостоверение личности. |
33
+
34
+
35
+ 🔴 **Предел: 3 постановки цели за скользящий час.** Считается по отметкам состоявшихся
36
+ постановок, отдельно на каждый канал. Исчерпан — команда отвергается со словами «зовите
37
+ человека». СНЯТИЕ цели при исчерпанном пределе разрешено: предел бережёт от разгона, а не
38
+ запирает возможность остановиться.
39
+
40
+ Предел не защищает от намеренного действия агента с правами администратора: такой агент
41
+ снимет и счётчик, и сам предел. Защита здесь от разгона и случайности, а не от намерения.
42
+ ## Пять вещей, стоивших нам дня
43
+
44
+ Каждая из них отказывает **выглядя успехом**. В исходниках они помечены комментариями
45
+ рядом с местом; здесь короткая версия.
46
+
47
+ **1. Опрос принадлежит боту, а не монтажу.** Платформа монтирует композицию больше
48
+ одного раза на процесс и снимает лишний монтаж. С одним общим флагом «идёт» вы
49
+ получите два опроса, дерущихся за одного бота, и Telegram оборвёт оба с ошибкой
50
+ `Conflict`. С вежливым «новый монтаж просит старый посторониться» вы не получите
51
+ опроса вовсе — потому что снимут именно новый. Лечение — счёт ссылок: первый монтаж
52
+ запускает опрос, последний снятый его останавливает.
53
+
54
+ **2. Входящее письмо удаляется только после того, как дошло до агента.** Прочитать
55
+ файл и сразу удалить — значит терять сообщение всякий раз, когда передача не удалась.
56
+ А она не удастся, см. (3). Снаружи это неотличимо от «бот меня проигнорировал».
57
+
58
+ **3. Фабрика агентов появляется позже канала.** Первое сообщение может прийти раньше,
59
+ чем платформа её зарегистрировала, и вы получите `no agent factory registered`. Ждите
60
+ платформу, а не предполагайте, что она готова.
61
+
62
+ **4. Сессию, уже лежащую на диске, надо продолжать, а не создавать.** Вызов `create()`
63
+ с существующим идентификатором даёт новую пустую сессию, и переписка начинается с
64
+ чистого листа — при том, что старая цела и лежит рядом.
65
+
66
+ **5. Отказ незнакомцу должен быть громким.** Молчаливый отказ прячет событие
67
+ безопасности: владелец не узнает, что кто-то нашёл его бота.
68
+
69
+ ## Куда класть строку
70
+
71
+ В профиле `web` инструменты общего плана **выключены намеренно**; набор приходит из
72
+ пресета агента. Строка плагина, положенная в слой патча профиля, соберётся без единой
73
+ ошибки, будет числиться смонтированной — и никогда не дойдёт до агента. Кладите строку
74
+ в пресет агента.
75
+
76
+ Пресет читается ещё и **при создании сессии**: правка файла не действует на уже идущую.
77
+ Перезапустите платформу либо смените пресет по умолчанию в настройках (перечитывается на
78
+ ходу, действует для следующей созданной сессии).
79
+
80
+ ## Почему `appDir`
81
+
82
+ Плагин разрешает `@deepseek-ai/dsh-llm` и `@deepseek-ai/dsh-agent` из каталога, который
83
+ вы передали в `appDir`, а не от собственного расположения. Это сделано намеренно: модуль
84
+ должен лежать на машине один раз и обслуживать нескольких агентов, у каждого своя
85
+ установка платформы. Привяжи его жёстко к `node_modules` одного агента — и удаление
86
+ ЭТОГО агента сломает канал всем остальным.
87
+
88
+ ## Заметки о безопасности
89
+
90
+ - Пустой `allowedUsers` означает, что любой нашедший бота говорит с агентом, у которого
91
+ обычно есть доступ к оболочке машины. Задайте его.
92
+ - Отвергнутый незнакомец попадает в журнал и в сообщение владельцу. Молчаливый отказ
93
+ прячет событие безопасности.
94
+ - Токен читается из файла при запуске; настройка несёт путь, а не секрет.
95
+ - Изоляция агентов держится на правах файлов токенов, а не на самом модуле: модуль
96
+ общий, разделяет их файловая система.
97
+
98
+ ## Состояние
99
+
100
+ Написан для собственного парка агентов и работает в бою на нескольких. Платформа молодая
101
+ (`0.1.0-rc`), её интерфейс плагинов меняется — рассчитывайте подстраиваться. Комментарии
102
+ в исходниках русские: они несут причину каждой неочевидной строки.
25
103
 
26
- ```yaml
27
- - insert:
28
- - name: dsh-telegram-multiagent
29
- config:
30
- agentName: my-agent # log label only
31
- tokenFile: /etc/dsh/bot.token # preferred: a file readable by this agent
32
- appDir: /opt/my-agent/app # where THIS agent's harness is installed
33
- workspace: /opt/my-agent/work
34
- allowedUsers: [123456789] # empty = everyone; you do not want that
35
- ```
104
+ MIT.
36
105
 
37
- ## Configuration
106
+ ## 1.4.0 — причина сетевого отказа и чтение почтового ящика
38
107
 
39
- | Field | Required | What it does |
40
- |---|---|---|
41
- | `tokenFile` | one of these | Path to a file holding the bot token. Preferred: the secret belongs to the machine, the config only carries a path. |
42
- | `token` / `tokenEnv` | one of these | Literal token, or the name of an env var. For debugging. |
43
- | `appDir` | yes | Directory where this agent's harness is installed. Platform packages are resolved from **here**, not from the plugin's own location — see "Why appDir" below. |
44
- | `agentName` | no | Label in log lines. Useful when several bots run on one machine. |
45
- | `allowedUsers` | no | Numeric user ids allowed to talk to the agent. **Empty means everyone** — an agent usually has real access to the machine, so set it. |
46
- | `workspace` | no | Working directory handed to the agent session. |
47
- | `preset` | no | Agent preset to mount for sessions created by this channel. |
48
- | `provider` / `model` | no | Fallback if the deployment has no default model service. |
49
- | `a2aDir` | no | Directory for a file-based agent-to-agent channel (`in/`, `out/`). Omit and no such channel exists. |
50
- | `a2aSession` | no | Session id used for that channel. Default `a2a`. |
51
- | `transcribeCommand` | no | External command for voice messages: `<cmd> <audio-file> auto` → transcript on stdout. Omit and voice is politely refused. |
52
- | `goalUsers` | no | Telegram user ids allowed to run `/goal`. **Empty (default) = the command is refused for everyone.** Not inherited from `allowedUsers` on purpose: talking to an agent and starting a paid autonomous loop are different rights. |
53
- | `goalA2ASenders` | no | Sender names allowed to run `/goal` from the agent-to-agent channel. The sender names itself in the first line of the file (`From: <name>`) — this is bookkeeping, not authentication; see 1.3.0 below. |
54
-
55
- ## Five things that cost us a day
56
-
57
- Each of these fails **while looking like success**. They are commented inline in the source; this is
58
- the short version.
59
-
60
- **1. Polling belongs to the bot, not to the mount.** The harness mounts a composition more than once
61
- per process and unmounts the extra one. With a single shared `running` flag you get two pollers
62
- fighting over one bot and Telegram cuts both with `Conflict`. With a polite "new mount asks the old
63
- one to step aside" you get *no* poller at all — because the new mount is the one that gets unmounted.
64
- The fix is reference counting: the first mount starts polling, the last unmount stops it.
65
-
66
- **2. Delete the incoming message only after it reached the agent.** Reading a file and unlinking it
67
- immediately loses the message whenever the handoff fails — and the handoff *will* fail, see (3).
68
- From the outside that is indistinguishable from "the bot ignored me".
69
-
70
- **3. The agent factory appears later than the channel.** The first message can arrive before the
71
- harness has registered it, and you get `no agent factory registered`. Wait for the platform instead
72
- of assuming it is ready.
73
-
74
- **4. A session already on disk must be resumed, not created.** Calling `create()` with an existing
75
- session id makes the persistence layer abort *every* turn with an id-collision error. Externally:
76
- "accepted the message and went quiet" — the turn honestly starts and dies in milliseconds. It works
77
- until the first restart, which is what makes it nasty. Use `resume()` when the session exists.
78
-
79
- **5. `ctx.get()` returns nothing *quietly* while a service is still starting.** cordis hands back
80
- `undefined` without throwing until the provider's fiber is active, so "this service is not in the
81
- build" and "this service is thirty milliseconds away" arrive as the same value. Code written as
82
- `const x = ctx.get('x'); if (x) {...}` then takes the "feature absent" branch and says nothing — the
83
- agent is assembled, answers, and never mentions what it lost. This bit us three times in three days:
84
- the agent factory (3), session persistence (4, the whole resume block was skipped), and the agent
85
- preset — where a neighbouring agent's toolset dropped from 33 tools to 3 with no error and no log
86
- line. Wait for the service with a stated deadline, and when the deadline passes, say so *and name
87
- the consequence*. One helper for all of them: a fix applied to the instance instead of the class
88
- guarantees a relapse, and the relapse looks like a new illness.
89
-
90
- ## Where to put the row
91
-
92
- In the `web` profile the common-plane tools are **disabled on purpose**; the toolset comes from the
93
- agent preset. A plugin row placed in the profile patch layer composes without a single error, is
94
- listed as mounted — and never reaches the agent. Put the row in the agent preset.
95
-
96
- The preset is also picked up when a **session is created**: editing the file does not affect a
97
- running session. Restart the platform, or change the default preset in settings (hot-reloaded, takes
98
- effect for the next created session).
99
-
100
- ## Why `appDir`
101
-
102
- The plugin resolves `@deepseek-ai/dsh-llm` and `@deepseek-ai/dsh-agent` from the directory you pass
103
- in `appDir`, not from its own location. That is deliberate: the module is meant to live once on a
104
- machine and serve several agents, each with its own harness installation. Hard-linking it to one
105
- agent's `node_modules` would mean that removing *that* agent breaks the channel for everybody else.
106
-
107
- ## Security notes
108
-
109
- - `allowedUsers` empty means anyone who finds the bot talks to an agent that usually has shell access
110
- to the machine. Set it.
111
- - A rejected stranger is logged and reported to the owner. A silent refusal hides a security event.
112
- - The token is read from a file at startup; the config carries a path, not a secret.
113
-
114
- ## Status
115
-
116
- Written for our own fleet and running in production on several agents. The harness is young
117
- (`0.1.0-rc`) and its plugin API moves; expect to adapt. Inline comments are currently in Russian —
118
- they carry the reasoning behind each non-obvious line, and a translation is welcome.
108
+ **Ломающих изменений НЕТ.** Проверено сравнением состава: из 1.3.0 не пропало ни одного
109
+ файла, добавилось два стенда. Пути внутри пакета прежние.
119
110
 
120
- MIT.
111
+ пропало: ничего
112
+ добавилось: test/stend-imena.mjs, test/stend-yashchika-shiny.mjs
113
+ изменилось: src/index.js, test/test-goal-control.mjs, cordis.patch.yml, README.md
114
+
115
+ **Причина сетевого отказа печатается рядом с сообщением.** Раньше `fetch failed` скрывал
116
+ причину: обрыв соединения, истёкшее ожидание и ненайденное имя выглядели одинаково, и по
117
+ журналу нельзя было понять, что чинить. Теперь рядом печатается `e.cause`. Если причины
118
+ нет — пишется «не указана», а не пустое место: пустое читается как «причины нет» и
119
+ возвращает ту же слепоту.
120
+
121
+ 🔴 **У двух каналов РАЗНЫЕ способы назвать отправителя, и путать их дорого:**
122
+
123
+ канал a2a (каталог a2aDir) — отправитель из заголовка первой строки: From: <имя>
124
+ почтовый ящик (spoolDir) — отправитель из ИМЕНИ ФАЙЛА: ГГГГММДДTЧЧММССZ-<имя>-<хвост>
125
+
126
+ Положив в ящик письмо с заголовком `From:`, вы получите отправителем то, что стоит
127
+ в имени файла, а сам заголовок останется в тексте письма. Ошибки не будет, отказа
128
+ тоже — просто отправитель окажется не тем, кого вы назвали. Проверено пробой.
129
+
130
+ Причина разницы: в ящик кладёт посторонний процесс правами каталога, и имя файла —
131
+ единственное, что он не может подделать незаметно для владельца ящика. В канале a2a
132
+ файл пишет сам отправитель, там заголовок и есть его подпись. В обоих случаях это
133
+ учёт, а не удостоверение личности.
134
+
135
+ И третье следствие того же различия: **команды (`/goal`) из ящика не принимаются вовсе** —
136
+ они живут только в канале a2a, где отправитель подписывает файл сам. Положив `/goal` в
137
+ ящик, вы получите обычное письмо с этим текстом: ни выполнения, ни отказа. Право ставить
138
+ цель поимённое, и проверять его по имени файла, которое задаёт посторонний процесс,
139
+ означало бы раздать это право владельцу каталога.
140
+
141
+ **Чтение писем из системного почтового ящика** (`spoolDir`). Письмо старше 12 часов
142
+ подаётся с явной пометкой возраста — иначе двухсуточное письмо толкает отвечать на
143
+ устаревшее. Больше пяти писем подаются одной пачкой, а не по одному. Ящик не задан —
144
+ не читается, молчаливого умолчания нет.
145
+
146
+ **Блок управления целями появился в `cordis.patch.yml`.** Раньше `goalUsers` и
147
+ `goalA2ASenders` были описаны в README, но отсутствовали в патче монтажа — то есть
148
+ настройка была недоступна тому, кто ставил пакет по инструкции.
149
+
150
+ ### Проверка, что работает
151
+
152
+ Стенды едут вместе с кодом:
153
+
154
+ node test/test-goal-control.mjs управление целями из канала
155
+ node test/stend-imena.mjs метки отправителя и обезличенность
156
+ node test/stend-yashchika-shiny.mjs чтение почтового ящика
157
+
158
+ Коды: 0 — сошлось, 1 — расхождение, 2 — проверить нечем (слепота).
159
+ 🔴 Код 2 не означает «всё хорошо»: он означает, что часть проверок не состоялась. Чаще
160
+ всего это отсутствие необязательных зависимостей или файлов настроек, которых у только
161
+ что установленного модуля ещё нет.
162
+
163
+ ### Чего 1.4.0 НЕ делает
164
+
165
+ - не хранит переписку — она живёт в журнале сессии платформы;
166
+ - не гарантирует доставку при недоступной сети: повторяет опрос и пишет причину отказа в
167
+ журнал, но письмо в этот момент не уходит;
168
+ - не проверяет, что пишущий в канал между агентами — тот, за кого себя выдаёт: метка
169
+ отправителя берётся из имени файла, а право положить файл раздаётся правами каталога.
121
170
 
122
171
  ## 1.1.0 — кто спросил, кому отвечено, и кто это видит
123
172
 
package/cordis.patch.yml CHANGED
@@ -17,13 +17,14 @@
17
17
  # Empty means everyone can talk to an agent that usually has shell
18
18
  # access. Set it.
19
19
  allowedUsers: []
20
- # Goal control from the channel (1.3.0). BOTH lists are empty by
21
- # default, which means the /goal command is refused for everyone —
22
- # deliberately: starting an autonomous goal loop costs money, so the
23
- # right is granted by name, never inherited from allowedUsers.
24
- # Telegram user ids allowed to run /goal:
20
+ # 🔴 УПРАВЛЕНИЕ ЦЕЛЯМИ ИЗ КАНАЛА. Оба списка пусты по умолчанию — это
21
+ # значит, что команда постановки цели отвергается для ВСЕХ. Так задумано:
22
+ # запуск автономного цикла стоит денег, поэтому право даётся поимённо и
23
+ # НИКОГДА не наследуется от allowedUsers. Говорить с агентом и запускать
24
+ # ему платный цикл разные права.
25
+ # Кому можно ставить цель из Telegram (числовые идентификаторы):
25
26
  goalUsers: []
26
- # Names allowed to run /goal from the agent-to-agent channel. The
27
- # sender names itself in the first line of the file: "From: <name>".
28
- # This is bookkeeping, not authenticationsee README.
27
+ # Кому можно ставить цель из служебного канала между агентами.
28
+ # Отправитель называет себя сам, первой строкой файла: "From: <имя>".
29
+ # Это учёт, а не удостоверение личности см. README.
29
30
  goalA2ASenders: []
package/package.json CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "dsh-telegram-multiagent",
3
- "version": "1.3.0",
4
- "description": "Telegram channel for DeepSeek Harness one shared module serving several agents, with unforgeable sender marks, merged owner+coordinator memory, a delivery mode you switch in a file outside the code, and goal control from the channel for agents whose loop runs outside the platform.",
3
+ "version": "1.4.1",
4
+ "description": "Канал Telegram для DeepSeek Harness: один модуль на машину обслуживает несколько агентов, у каждого свой бот, свой файл токена и свой список разрешённых. Метка отправителя неподделываема, память владельца и координатора слита в одну, режим доставки переключается файлом вне кода, цели управляются из канала.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "files": [
8
- "src",
9
- "test",
8
+ "src/index.js",
9
+ "src/client.js",
10
+ "test/extract.mjs",
11
+ "test/stend-imena.mjs",
12
+ "test/stend-yashchika-shiny.mjs",
13
+ "test/test-goal-control.mjs",
14
+ "test/stend-pobudki.mjs",
10
15
  "cordis.patch.yml",
11
16
  "README.md"
12
17
  ],
package/src/client.js CHANGED
File without changes