dsh-telegram-multiagent 1.4.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
@@ -132,6 +132,12 @@ MIT.
132
132
  файл пишет сам отправитель, там заголовок и есть его подпись. В обоих случаях это
133
133
  учёт, а не удостоверение личности.
134
134
 
135
+ И третье следствие того же различия: **команды (`/goal`) из ящика не принимаются вовсе** —
136
+ они живут только в канале a2a, где отправитель подписывает файл сам. Положив `/goal` в
137
+ ящик, вы получите обычное письмо с этим текстом: ни выполнения, ни отказа. Право ставить
138
+ цель поимённое, и проверять его по имени файла, которое задаёт посторонний процесс,
139
+ означало бы раздать это право владельцу каталога.
140
+
135
141
  **Чтение писем из системного почтового ящика** (`spoolDir`). Письмо старше 12 часов
136
142
  подаётся с явной пометкой возраста — иначе двухсуточное письмо толкает отвечать на
137
143
  устаревшее. Больше пяти писем подаются одной пачкой, а не по одному. Ящик не задан —
package/cordis.patch.yml CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-telegram-multiagent",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Канал Telegram для DeepSeek Harness: один модуль на машину обслуживает несколько агентов, у каждого свой бот, свой файл токена и свой список разрешённых. Метка отправителя неподделываема, память владельца и координатора слита в одну, режим доставки переключается файлом вне кода, цели управляются из канала.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -11,6 +11,7 @@
11
11
  "test/stend-imena.mjs",
12
12
  "test/stend-yashchika-shiny.mjs",
13
13
  "test/test-goal-control.mjs",
14
+ "test/stend-pobudki.mjs",
14
15
  "cordis.patch.yml",
15
16
  "README.md"
16
17
  ],
package/src/client.js CHANGED
File without changes
package/src/index.js CHANGED
@@ -221,21 +221,57 @@ export function apply(ctx, config = {}) {
221
221
  // 🔴 НИКАКИХ ТИХИХ ВЫХОДОВ: каждая ветка что-то пишет в журнал. Ровно на этом
222
222
  // обжглись сегодня — `agent.inbox?.hasPending` при недоступном inbox даёт
223
223
  // undefined, условие «всё хорошо» проходит, и отказ маскируется под успех.
224
+ // ── ЧЕТЫРЕ ИСХОДА ВМЕСТО ДВУХ (обосновано замером 01.09.2026).
225
+ // Прежняя редакция знала только «забрано» и «не забрали», и второе печаталось
226
+ // бедой. Замер за сутки: 48 писем принято, 45 строк «не забрали», побудок за всё
227
+ // время НОЛЬ, а настоящая задержка доставки — медиана 10 с. То есть беда
228
+ // объявлялась при штатной работе, 94 раза из 100.
229
+ //
230
+ // Две причины, обе исправлены здесь:
231
+ // 1) проверка «забрано ли» стояла ПОСЛЕ проверки занятости, и при занятом агенте
232
+ // очередь не смотрелась вовсе — выйти из цикла было структурно нельзя, пока
233
+ // агент работает. Теперь состояние очереди проверяется ПЕРВЫМ и от занятости
234
+ // не зависит: забрано — значит забрано, кто бы чем ни был занят;
235
+ // 2) «агент был занят весь срок» и «агент свободен, а письмо висит» — разные
236
+ // новости. Первое — сведение о штатном состоянии, второе — отказ механизма.
237
+ // Раньше обе давали одну красную строку, и настоящий отказ утонул бы в шуме.
238
+ //
239
+ // 🔴 ГДЕ НЕ ПРИМЕНЯЕТСЯ И ЧЕГО НЕ ЛОВИТ:
240
+ // • это наблюдение, а не доставка: письмо доходит и без побудки, платформа
241
+ // заберёт его на следующем ходе. Молчание механизма не означает потери;
242
+ // • «занят все проверки» не доказывает, что письмо доставлено — только что
243
+ // будить было некого. Момент забора виден лишь при выходе по пустой очереди;
244
+ // • очередь, опустевшая уже на первой проверке, неотличима от «письмо вклеено
245
+ // в идущий ход» — поэтому такой случай помечается отдельно, а не молчит.
246
+ //
247
+ // 🔴 НИКАКИХ ТИХИХ ВЫХОДОВ: каждая ветка что-то пишет в журнал.
224
248
  function nudgeUntilClaimed(agent, tag, budgetMs = 120000) {
225
249
  void (async () => {
226
- const deadline = Date.now() + budgetMs;
227
- let woke = 0;
250
+ const nachato = Date.now();
251
+ const deadline = nachato + budgetMs;
252
+ let woke = 0; // сколько раз будили
253
+ let proverok = 0; // сколько проходов сделали
254
+ let zanyat = 0; // из них пришлось на занятого агента
255
+ let videliOchered = false; // видели ли очередь непустой хоть раз
228
256
  while (Date.now() < deadline) {
229
257
  await new Promise((r) => setTimeout(r, 500));
258
+ proverok += 1;
230
259
  if (typeof agent?.inbox?.hasPending !== 'boolean') {
231
260
  log(`${tag} 🔴 очередь агента недоступна — добивать побудку нечем`);
232
261
  return;
233
262
  }
234
- if (agent.status === 'running') continue;
263
+ // Забрано ли вопрос о ПИСЬМЕ, а не об агенте. Спрашиваем всегда.
235
264
  if (!agent.inbox.hasPending) {
236
- if (woke) log(`${tag} сообщение забрано после ${woke} повторных побудок`);
265
+ const sek = ((Date.now() - nachato) / 1000).toFixed(1);
266
+ const primech = videliOchered
267
+ ? ''
268
+ : ' (очередь была пуста уже на первой проверке — вероятно, письмо вклеено в идущий ход)';
269
+ log(`${tag} сообщение забрано за ${sek} с: побудок ${woke}, проверок ${proverok}${primech}`);
237
270
  return;
238
271
  }
272
+ videliOchered = true;
273
+ // А вот будить — вопрос об АГЕНТЕ: занятого будить незачем и нечем.
274
+ if (agent.status === 'running') { zanyat += 1; continue; }
239
275
  if (typeof agent.wakeDriver !== 'function') {
240
276
  log(`${tag} 🔴 сообщение висит в очереди, а wakeDriver недоступен`);
241
277
  return;
@@ -243,7 +279,11 @@ export function apply(ctx, config = {}) {
243
279
  agent.wakeDriver();
244
280
  woke += 1;
245
281
  }
246
- log(`${tag} 🔴 за ${budgetMs} мс сообщение так и не забрали (побудок: ${woke})`);
282
+ if (proverok > 0 && zanyat === proverok) {
283
+ log(`${tag} письмо ещё в очереди спустя ${budgetMs} мс: агент был занят все ${zanyat} проверок из ${proverok} — будить было некого, состояние штатное`);
284
+ } else {
285
+ log(`${tag} 🔴 за ${budgetMs} мс сообщение так и не забрали: агент был свободен ${proverok - zanyat} проверок из ${proverok}, побудок ${woke} — побудка не помогла`);
286
+ }
247
287
  })();
248
288
  }
249
289
 
package/test/extract.mjs CHANGED
File without changes
@@ -64,7 +64,17 @@ if (Array.isArray(SPISOK_IMEN) && SPISOK_IMEN.length) {
64
64
  // 2. Ветка отказа чужому: используется who_, а НЕ who (та затенена msg.from).
65
65
  const alertLine = s.split('\n').find((l) => l.includes('Чужой написал'));
66
66
  t('тревога о чужом использует who_', !!alertLine && /\$\{who_\}/.test(alertLine), String(alertLine));
67
- t('тревога о чужом без клички соседа', !!alertLine && !/Петрович/.test(alertLine), String(alertLine));
67
+ // 🔴 ИСКОМОЕ ИМЯ ДАННЫМИ, А НЕ ЛИТЕРАЛОМ. Прежняя редакция несла кличку
68
+ // соседа прямо здесь: проверка была верной, но сама публиковала то, что искала —
69
+ // файл уезжает в пакет. Теперь кличка берётся из списка вне пакета; нет списка —
70
+ // СЛЕПОТА, а не молчаливое «чисто» (у постороннего проверять нечем по построению).
71
+ const netKlichki = (line) => !SPISOK_IMEN.some((im) => line.includes(im));
72
+ if (Array.isArray(SPISOK_IMEN) && SPISOK_IMEN.length) {
73
+ t('тревога о чужом без клички соседа', !!alertLine && netKlichki(alertLine), String(alertLine));
74
+ } else {
75
+ console.log('--- кличка не проверена в строке тревоги: списка имён нет');
76
+ slep += 1;
77
+ }
68
78
 
69
79
  // 3. Затенение существует — значит who_ не блажь: проверяем фактом по файлу.
70
80
  t('внутри ветки чужого who действительно затенена',
@@ -74,7 +84,12 @@ t('внутри ветки чужого who действительно зате
74
84
  // 4. Приветствие /start.
75
85
  const hi = s.split('\n').find((l) => l.includes('на связи. Пишите задачу'));
76
86
  t('приветствие использует who_', !!hi && /\$\{who_\}/.test(hi), String(hi));
77
- t('приветствие без клички соседа', !!hi && !/Петрович/.test(hi), String(hi));
87
+ if (Array.isArray(SPISOK_IMEN) && SPISOK_IMEN.length) {
88
+ t('приветствие без клички соседа', !!hi && netKlichki(hi), String(hi));
89
+ } else {
90
+ console.log('--- кличка не проверена в приветствии: списка имён нет');
91
+ slep += 1;
92
+ }
78
93
 
79
94
  // 5. Поведение подстановки — проверяем ИСПОЛНЕНИЕМ, а не глазом.
80
95
  // 🔴 Проверки 5-6 требуют объявления who_ и строки тревоги. Нет их — стенд обязан
@@ -0,0 +1,142 @@
1
+ // Стенд различения исходов побудки в модуле связи.
2
+ // Текст функции берётся ИЗ файла предмета и ИСПОЛНЯЕТСЯ на подставных агентах.
3
+ // Ни платформа, ни живая очередь не задействованы ни на одном шаге.
4
+ //
5
+ // Зачем стенд: механизм печатал беду при штатной работе — «не забрали» в 45 случаях
6
+ // из 48 за сутки, при нулевом числе побудок. Здесь проверяется, что четыре исхода
7
+ // различаются и что беда осталась только там, где механизм действительно не сработал.
8
+ import fs from 'node:fs';
9
+
10
+ const SRC = process.argv[2] || new URL('../src/index.js', import.meta.url).pathname;
11
+ let s;
12
+ try { s = fs.readFileSync(SRC, 'utf-8'); }
13
+ catch (e) { console.log(`СЛЕПОТА: не читается ${SRC} — ${e.code || e.message}`); process.exit(2); }
14
+
15
+ let ok = 0, fail = 0, slep = 0;
16
+ const t = (n, c, syr) => { if (c) { ok++; console.log(`ok ${n}`); }
17
+ else { fail++; console.log(`FAIL ${n}\n сырьё: ${syr ?? '(не задано)'}`); } };
18
+ const sl = (n, p) => { slep++; console.log(`СЛЕПОТА ${n}: ${p}`); };
19
+
20
+ // ── извлечение функции ИЗ предмета ───────────────────────────────────────────
21
+ function vynut(imya) {
22
+ const i = s.indexOf(` function ${imya}(`);
23
+ if (i < 0) return null;
24
+ const j = s.indexOf('\n }\n', i);
25
+ return j < 0 ? null : s.slice(i, j + 5);
26
+ }
27
+ const txt = vynut('nudgeUntilClaimed');
28
+ if (!txt) {
29
+ sl('извлечение', 'функция nudgeUntilClaimed в предмете не найдена');
30
+ console.log(`ИТОГО: сошлось ${ok}, расхождений ${fail}, слепот ${slep}`);
31
+ process.exit(2);
32
+ }
33
+
34
+ // ── 1. порядок проверок: очередь спрашивается ДО занятости ───────────────────
35
+ const iPending = txt.indexOf('!agent.inbox.hasPending');
36
+ const iRunning = txt.indexOf("agent.status === 'running'");
37
+ t('проверка «забрано ли» стоит ДО проверки занятости',
38
+ iPending > 0 && iRunning > 0 && iPending < iRunning,
39
+ `hasPending@${iPending} running@${iRunning}`);
40
+
41
+ // ── 2. счётчики объявлены ────────────────────────────────────────────────────
42
+ for (const im of ['proverok', 'zanyat']) {
43
+ t(`счётчик ${im} объявлен`, new RegExp(`let ${im} = 0`).test(txt), `в тексте: ${txt.includes(im)}`);
44
+ }
45
+
46
+ // ── исполнение на подставных агентах ─────────────────────────────────────────
47
+ const mk = () => new Function('log', `${txt}\n return nudgeUntilClaimed;`);
48
+ const BUDGET = 1600; // 3 прохода по 500 мс
49
+ const ZHDAT = BUDGET + 900;
50
+
51
+ async function progon(agent) {
52
+ const stroki = [];
53
+ let fn;
54
+ try { fn = mk()((m) => stroki.push(String(m))); }
55
+ catch (e) { return { stroki: [], oshibka: e.message }; }
56
+ fn(agent, '[проба]', BUDGET);
57
+ await new Promise((r) => setTimeout(r, ZHDAT));
58
+ return { stroki, oshibka: null };
59
+ }
60
+
61
+ // счётчик обращений к очереди — им проверяем, что проверки реально шли
62
+ const agentZanyat = (pending) => {
63
+ let n = 0;
64
+ return { status: 'running', get inbox() { return { get hasPending() { n++; return pending; } }; },
65
+ wakeDriver() {}, get _n() { return n; } };
66
+ };
67
+ const agentSvoboden = (pendingFn, wake) => ({
68
+ status: 'idle', inbox: { get hasPending() { return pendingFn(); } }, wakeDriver: wake,
69
+ });
70
+
71
+ const R = {};
72
+ R.zanyat = await progon(agentZanyat(true));
73
+ R.zabrano = await progon(agentSvoboden(((c) => () => ++c <= 2)(0), () => {}));
74
+ R.srazu = await progon(agentSvoboden(() => false, () => {}));
75
+ let vzyato = 0, zn = 0;
76
+ R.svoboden = await progon(agentSvoboden(() => true, () => { vzyato++; }));
77
+ R.netOch = await progon({ status: 'idle', inbox: { hasPending: undefined }, wakeDriver: () => {} });
78
+ R.netWake = await progon({ status: 'idle', inbox: { hasPending: true }, wakeDriver: null });
79
+
80
+ const odna = (r) => (r.stroki.length === 1 ? r.stroki[0] : `(строк ${r.stroki.length})`);
81
+
82
+ // ── 3. занят весь срок — СВЕДЕНИЕ, не беда ───────────────────────────────────
83
+ t('занят весь срок: строка есть и она НЕ беда',
84
+ R.zanyat.stroki.length === 1 && !/🔴/.test(odna(R.zanyat)), odna(R.zanyat));
85
+ t('занят весь срок: названо ЧИСЛО проверок',
86
+ /занят все \d+ проверок из \d+/.test(odna(R.zanyat)), odna(R.zanyat));
87
+ t('занят весь срок: сказано, что состояние штатное',
88
+ /штатн/.test(odna(R.zanyat)), odna(R.zanyat));
89
+
90
+ // ── 4. письмо забрано ────────────────────────────────────────────────────────
91
+ t('забрано: строка есть и она НЕ беда',
92
+ R.zabrano.stroki.length === 1 && !/🔴/.test(odna(R.zabrano)), odna(R.zabrano));
93
+ t('забрано: названо время и число проверок',
94
+ /забрано за [\d.]+ с: побудок \d+, проверок \d+/.test(odna(R.zabrano)), odna(R.zabrano));
95
+ t('забрано после побудок: побудок больше нуля',
96
+ /побудок [1-9]/.test(odna(R.zabrano)), odna(R.zabrano));
97
+ t('забрано после побудок: пометки про первую проверку НЕТ',
98
+ !/пуста уже на первой/.test(odna(R.zabrano)), odna(R.zabrano));
99
+
100
+ // ── 5. очередь пуста сразу — помечается отдельно ─────────────────────────────
101
+ t('пусто с первой проверки: случай помечен вслух',
102
+ /пуста уже на первой проверке/.test(odna(R.srazu)), odna(R.srazu));
103
+ t('пусто с первой проверки: это НЕ беда',
104
+ !/🔴/.test(odna(R.srazu)), odna(R.srazu));
105
+
106
+ // ── 6. свободен, а письмо висит — БЕДА (контроль зрячести) ───────────────────
107
+ t('свободен и висит: строка помечена бедой',
108
+ /🔴/.test(odna(R.svoboden)), odna(R.svoboden));
109
+ t('свободен и висит: названо, сколько проверок агент был свободен',
110
+ /свободен \d+ проверок из \d+/.test(odna(R.svoboden)), odna(R.svoboden));
111
+ t('свободен и висит: побудки реально делались', vzyato > 0, `вызовов wakeDriver: ${vzyato}`);
112
+
113
+ // ── 7. отказы устройства — беда ──────────────────────────────────────────────
114
+ t('очередь недоступна: беда с причиной',
115
+ /🔴/.test(odna(R.netOch)) && /очередь агента недоступна/.test(odna(R.netOch)), odna(R.netOch));
116
+ t('wakeDriver недоступен: беда с причиной',
117
+ /🔴/.test(odna(R.netWake)) && /wakeDriver недоступен/.test(odna(R.netWake)), odna(R.netWake));
118
+
119
+ // ── 8. ни один исход не молчит ───────────────────────────────────────────────
120
+ // ── 8а. ЗАНЯТ, но очередь опустела: письмо забрано, и это видно ──────────────
121
+ // Ровно тот случай, ради которого проверка очереди вынесена вперёд: при прежнем
122
+ // порядке занятость закрывала выход, и забранное письмо объявлялось незабранным.
123
+ R.zanyatNoZabrano = await progon({
124
+ status: 'running',
125
+ inbox: { get hasPending() { return ++zn <= 2; } },
126
+ wakeDriver() {},
127
+ });
128
+ t('занят, но очередь опустела: сказано ЗАБРАНО, а не «занят весь срок»',
129
+ /забрано за/.test(odna(R.zanyatNoZabrano)) && !/занят все/.test(odna(R.zanyatNoZabrano)),
130
+ odna(R.zanyatNoZabrano));
131
+
132
+ const molchat = Object.entries(R).filter(([, r]) => r.stroki.length === 0).map(([k]) => k);
133
+ t('ни один из семи исходов не молчит', molchat.length === 0, `молчали: ${molchat.join(', ') || '—'}`);
134
+
135
+ // ── итог ПЕРЕД канарейкой ────────────────────────────────────────────────────
136
+ console.log(`ИТОГО: сошлось ${ok}, расхождений ${fail}, слепот ${slep}`);
137
+ const ZHDYOM = 19;
138
+ if (ok + fail + slep !== ZHDYOM) {
139
+ console.log(`🔴 КАНАРЕЙКА: проверок ${ok + fail + slep}, а стенд состоит из ${ZHDYOM}`);
140
+ process.exit(2);
141
+ }
142
+ process.exit(fail ? 1 : (slep ? 2 : 0));
File without changes
File without changes