dsh-telegram-multiagent 1.5.0 → 1.5.2
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/CHANGELOG.md +45 -0
- package/package.json +1 -1
- package/src/index.js +55 -5
- package/test/stend-prichiny-otkaza.mjs +115 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,48 @@
|
|
|
1
|
+
## 1.5.2
|
|
2
|
+
|
|
3
|
+
**Разбор брошенного — по ДВУМ ОСЯМ.** Три места (`Измеритель отказал`, `обход событий отказал`,
|
|
4
|
+
`llm.resolveModelInfo отказал`) собирали причину как `String(e?.message ?? e)`.
|
|
5
|
+
|
|
6
|
+
ось 1 — пустое исключение -> строка НЕ пуста было 6 из 10
|
|
7
|
+
ось 2 — брошенный ПРИМИТИВ -> текст СОХРАНЁН было 4 из 5
|
|
8
|
+
стало: ось 1 — 10 из 10, ось 2 — 4 из 5 (не хуже)
|
|
9
|
+
|
|
10
|
+
🔴 Первая предложенная редакция закрывала ось 1 и ЛОМАЛА ось 2: `throw 'сессия не открыта'`
|
|
11
|
+
давала «string: (без пояснения)». Пустоту закрыли, потерю открыли рядом. Нашлось это не чтением
|
|
12
|
+
кода, а второй осью — пробы одной оси слепы к дефекту другой, сколько бы их ни было.
|
|
13
|
+
|
|
14
|
+
`code` добавлен сверх присланного замысла: у нас ДОКАЗАНО замером, что при пустом `message`
|
|
15
|
+
причина живёт именно в нём (`ECONNRESET`, `ENOTFOUND` у fetch). Без него три случая из пяти
|
|
16
|
+
дают «(без пояснения)» там, где код известен.
|
|
17
|
+
|
|
18
|
+
⚠️ Остаток названный: `throw null` и `{}` текста не несут вовсе — там «(без пояснения)» честно,
|
|
19
|
+
а не потеря.
|
|
20
|
+
|
|
21
|
+
Две порчи, каждая ломает СВОЮ ось: первая редакция роняет ось 2, прежнее выражение — ось 1.
|
|
22
|
+
Стенд 8 из 8.
|
|
23
|
+
|
|
24
|
+
## 1.5.1
|
|
25
|
+
|
|
26
|
+
🔴 **`errDetail` печатал пустоту после слова «причина».** Собирал причину через `??`, а нулевое
|
|
27
|
+
слияние реагирует только на `null` и `undefined` — пустая строка для него «найденное значение»,
|
|
28
|
+
и до `code` дело не доходило. У `AggregateError`, которую кладёт `fetch`, перебрав адреса,
|
|
29
|
+
собственный `message` как раз пустой.
|
|
30
|
+
|
|
31
|
+
В живом журнале это дало **125 строк за три часа** вида `причина: ` с пустотой после двоеточия.
|
|
32
|
+
Хуже отсутствия поля: «причина:» с пустотой читается как «причину узнали, она пустая», а её
|
|
33
|
+
просто не достали из объекта.
|
|
34
|
+
|
|
35
|
+
Теперь причина собирается перечнем: класс ошибки (AggregateError и Error лечатся по-разному, а
|
|
36
|
+
по тексту неотличимы), `code`, `message`, и внутренние ошибки `AggregateError.errors`, где у неё
|
|
37
|
+
и лежат настоящие причины. Пустой перечень даёт «объект есть, полей с описанием нет» — это НЕ
|
|
38
|
+
пустая причина, а другое состояние, и слова для него другие.
|
|
39
|
+
|
|
40
|
+
Строка отказа теперь называет, ЧТО опрашивалось: в цикле три опроса подряд (a2a, ящик, telegram),
|
|
41
|
+
и без имени отказ читается как «всё сломалось», хотя стоять может одно из трёх.
|
|
42
|
+
|
|
43
|
+
Стенд `stend-prichiny-otkaza.mjs`, 5 проб, вырезает функцию ИЗ ПРЕДМЕТА. Порча возвратом `??`
|
|
44
|
+
роняет три пробы из пяти.
|
|
45
|
+
|
|
1
46
|
## 1.5.0
|
|
2
47
|
|
|
3
48
|
🔴 **`/compact-status` больше не выдаёт устаревшую опору за свежую.** Строка «Опора замера:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-telegram-multiagent",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "Канал Telegram для DeepSeek Harness: один модуль на машину обслуживает несколько агентов, у каждого свой бот, свой файл токена и свой список разрешённых. Метка отправителя неподделываема, память владельца и координатора слита в одну, режим доставки переключается файлом вне кода, цели управляются из канала.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/index.js
CHANGED
|
@@ -1364,10 +1364,57 @@ export function apply(ctx, config = {}) {
|
|
|
1364
1364
|
// «причины нет», и это та же слепота, что и раньше).
|
|
1365
1365
|
// Перенесено из второй живой копии модуля 01.09.2026 ДОСЛОВНО: своя редакция
|
|
1366
1366
|
// того же лечения развела бы копии по логике при сверке.
|
|
1367
|
+
// 🔴 `??` ПРОПУСКАЕТ ПУСТУЮ СТРОКУ КАК ЗАДАННОЕ ЗНАЧЕНИЕ (замер 04.09.2026).
|
|
1368
|
+
// Было `c?.message ?? c?.code ?? String(c)`. Нулевое слияние реагирует только на
|
|
1369
|
+
// null и undefined, а `message` у AggregateError (её кладёт fetch, перебрав адреса)
|
|
1370
|
+
// бывает ПУСТОЙ СТРОКОЙ — и она «найдена», до `code` дело не доходит.
|
|
1371
|
+
// В журнале это дало 125 строк за три часа вида «причина: » с пустотой после
|
|
1372
|
+
// двоеточия. Хуже отсутствия поля: «причина:» с пустотой читается как «причину
|
|
1373
|
+
// узнали, она пустая», а её просто не достали.
|
|
1374
|
+
// Воспроизведено: errDetail(cause с message='') -> '', errDetail(AggregateError) -> ''.
|
|
1375
|
+
// 🔴 РАЗБОР БРОШЕННОГО. Замысел соавтора от 04.09 плюс `code` — и то, и другое
|
|
1376
|
+
// по замеру, а не по вкусу.
|
|
1377
|
+
//
|
|
1378
|
+
// ДВЕ ОСИ ПРИЁМКИ, и вторая нашла дефект, к которому первая слепа по построению:
|
|
1379
|
+
// ось 1 — пустое исключение -> строка НЕ пуста
|
|
1380
|
+
// ось 2 — брошенный ПРИМИТИВ -> текст СОХРАНЁН
|
|
1381
|
+
// Прежнее `String(e?.message ?? e)` держало ось 2 (4 из 5) и проваливало ось 1
|
|
1382
|
+
// (6 из 10): у Error с пустым message и у AggregateError печаталась пустота.
|
|
1383
|
+
// Первая присланная редакция закрывала ось 1 и ЛОМАЛА ось 2: `throw 'сессия не
|
|
1384
|
+
// открыта'` давала «string: (без пояснения)» — пустоту закрыли, потерю открыли рядом.
|
|
1385
|
+
//
|
|
1386
|
+
// Здесь: ось 1 — 10 из 10, ось 2 — 4 из 5 (не хуже прежнего).
|
|
1387
|
+
// `code` добавлен сверх замысла соавтора, потому что у нас ДОКАЗАНО замером: при
|
|
1388
|
+
// пустом message причина живёт именно в нём (ECONNRESET, ENOTFOUND у fetch). Без
|
|
1389
|
+
// него три случая из пяти дают «(без пояснения)» там, где код известен.
|
|
1390
|
+
// Остаток названный, а не замазанный: `throw null` и `{}` текста не несут вовсе —
|
|
1391
|
+
// терять там нечего, и «(без пояснения)» тут ЧЕСТНО, а не потеря.
|
|
1392
|
+
function prichina(e) {
|
|
1393
|
+
const tekst = String(e?.message ?? (typeof e === 'object' ? '' : (e ?? ''))).trim();
|
|
1394
|
+
const kod = e?.code ? String(e.code) : '';
|
|
1395
|
+
const hvost = [kod, tekst].filter(Boolean).join(' ') || '(без пояснения)';
|
|
1396
|
+
return ((e?.name ?? typeof e) + ': ' + hvost).slice(0, 90);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1367
1399
|
function errDetail(e) {
|
|
1368
1400
|
const c = e?.cause;
|
|
1369
1401
|
if (!c) return 'не указана';
|
|
1370
|
-
|
|
1402
|
+
const chasti = [];
|
|
1403
|
+
// Класс ошибки называем всегда: AggregateError и Error лечатся по-разному, а по
|
|
1404
|
+
// тексту они неотличимы.
|
|
1405
|
+
if (c?.constructor?.name) chasti.push(c.constructor.name);
|
|
1406
|
+
if (c?.code) chasti.push(String(c.code));
|
|
1407
|
+
if (c?.message) chasti.push(String(c.message));
|
|
1408
|
+
// У AggregateError настоящие причины лежат в errors, а собственный message пуст.
|
|
1409
|
+
if (Array.isArray(c?.errors) && c.errors.length) {
|
|
1410
|
+
const vnutri = c.errors.slice(0, 3)
|
|
1411
|
+
.map((x) => [x?.code, x?.message].filter(Boolean).join(' ') || String(x))
|
|
1412
|
+
.filter(Boolean);
|
|
1413
|
+
if (vnutri.length) chasti.push('внутри: ' + vnutri.join('; '));
|
|
1414
|
+
}
|
|
1415
|
+
// Пустой перечень — это НЕ пустая причина, а «объект есть, а сказать про него
|
|
1416
|
+
// нечего». Разные состояния, разные слова.
|
|
1417
|
+
return chasti.length ? chasti.join(' ') : `объект ${typeof c}, полей с описанием нет`;
|
|
1371
1418
|
}
|
|
1372
1419
|
|
|
1373
1420
|
async function loop(run) {
|
|
@@ -1397,7 +1444,10 @@ export function apply(ctx, config = {}) {
|
|
|
1397
1444
|
}
|
|
1398
1445
|
} catch (e) {
|
|
1399
1446
|
// Сеть моргнула или Telegram ответил ошибкой — ждём и продолжаем.
|
|
1400
|
-
|
|
1447
|
+
// ЧТО именно опрашивалось — называем: в цикле три опроса подряд (a2a, ящик,
|
|
1448
|
+
// телеграм), и без имени отказ читается как «всё сломалось», хотя стоять может
|
|
1449
|
+
// одно из трёх.
|
|
1450
|
+
log(`опрос не удался (a2a+ящик+telegram): ${e?.message ?? e} | причина: ${errDetail(e)}`);
|
|
1401
1451
|
await new Promise((r) => setTimeout(r, 3000));
|
|
1402
1452
|
}
|
|
1403
1453
|
}
|
|
@@ -1469,7 +1519,7 @@ export function apply(ctx, config = {}) {
|
|
|
1469
1519
|
if (!sess) return { kind: 'error', text: 'Сессия недоступна.' };
|
|
1470
1520
|
let m;
|
|
1471
1521
|
try { m = schetchik.measure(sess); }
|
|
1472
|
-
catch (e) { return { kind: 'error', text: `Измеритель отказал: ${
|
|
1522
|
+
catch (e) { return { kind: 'error', text: `Измеритель отказал: ${prichina(e)}` }; }
|
|
1473
1523
|
// 🔴 СВЕЖЕСТЬ ОПОРЫ. baseline.kind различает ПРОИСХОЖДЕНИЕ опоры и НЕ различает
|
|
1474
1524
|
// её СВЕЖЕСТЬ: при surface-replace (compaction/summary|prune) поверхность
|
|
1475
1525
|
// пересчитывается, а якорь остаётся прежним и тащит свой kind='usage'
|
|
@@ -1510,7 +1560,7 @@ export function apply(ctx, config = {}) {
|
|
|
1510
1560
|
return { kind: 'ne-znayu', pochemu: 'у сессии нет ни events, ни eventAt+seq' };
|
|
1511
1561
|
}
|
|
1512
1562
|
} catch (e) {
|
|
1513
|
-
return { kind: 'ne-znayu', pochemu: `обход событий отказал: ${
|
|
1563
|
+
return { kind: 'ne-znayu', pochemu: `обход событий отказал: ${prichina(e)}` };
|
|
1514
1564
|
}
|
|
1515
1565
|
if (poslednijKompakt < 0) return { kind: 'svezhaya', kompaktov: 0 };
|
|
1516
1566
|
if (poslednijYakor > poslednijKompakt) return { kind: 'svezhaya', kompaktov: 1 };
|
|
@@ -1571,7 +1621,7 @@ export function apply(ctx, config = {}) {
|
|
|
1571
1621
|
okno = undefined;
|
|
1572
1622
|
}
|
|
1573
1623
|
} catch (e) {
|
|
1574
|
-
pochemuNet = `llm.resolveModelInfo отказал: ${
|
|
1624
|
+
pochemuNet = `llm.resolveModelInfo отказал: ${prichina(e)}`;
|
|
1575
1625
|
}
|
|
1576
1626
|
}
|
|
1577
1627
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Стенд разбора причины отказа опроса.
|
|
2
|
+
//
|
|
3
|
+
// Коды: 0 сошлось | 1 расхождение | 2 слепота.
|
|
4
|
+
//
|
|
5
|
+
// ЧТО ЛЕЧИТ ПРЕДМЕТ. `errDetail` собирал причину через `??`. Нулевое слияние реагирует
|
|
6
|
+
// только на null и undefined, а `message` у AggregateError (её кладёт fetch, перебрав
|
|
7
|
+
// адреса) бывает ПУСТОЙ СТРОКОЙ — она «найдена», и до `code` дело не доходит.
|
|
8
|
+
// В живом журнале это дало 125 строк за три часа вида «причина: » с пустотой.
|
|
9
|
+
// 🔴 Хуже отсутствия поля: «причина:» с пустотой читается как «причину узнали, она
|
|
10
|
+
// пустая», а её просто не достали из объекта.
|
|
11
|
+
//
|
|
12
|
+
// 🔴 ВЫРЕЗАЕМ ИЗ ПРЕДМЕТА, а не переписываем: копия стареет молча.
|
|
13
|
+
//
|
|
14
|
+
// ГДЕ НЕ ПРИМЕНЯЕТСЯ: стенд проверяет только РАЗБОР объекта ошибки. Он не проверяет,
|
|
15
|
+
// что отказ вообще случается, и не проверяет доставку строки в журнал.
|
|
16
|
+
|
|
17
|
+
import { readFileSync } from 'node:fs'
|
|
18
|
+
|
|
19
|
+
const SRC = process.env.SVYAZ_SRC || new URL('../src/index.js', import.meta.url).pathname
|
|
20
|
+
let text
|
|
21
|
+
try { text = readFileSync(SRC, 'utf8') } catch (e) {
|
|
22
|
+
console.log(`СЛЕПОТА: предмет ${SRC} не читается: ${e?.message ?? e}`); process.exit(2)
|
|
23
|
+
}
|
|
24
|
+
const m = text.match(/function errDetail\(e\) \{[\s\S]*?\n \}/)
|
|
25
|
+
if (!m) { console.log('СЛЕПОТА: функция errDetail в предмете не найдена — вырезать нечего'); process.exit(2) }
|
|
26
|
+
let errDetail
|
|
27
|
+
try { errDetail = new Function(m[0] + '; return errDetail;')() }
|
|
28
|
+
catch (e) { console.log(`СЛЕПОТА: вырезанное не собирается: ${e?.message ?? e}`); process.exit(2) }
|
|
29
|
+
|
|
30
|
+
let ok = 0, bed = 0
|
|
31
|
+
const t = (imya, delo) => { try { delo(); ok++; console.log(` ok ${imya}`) }
|
|
32
|
+
catch (e) { bed++; console.log(` FAIL ${imya} — ${e?.message ?? e}`) } }
|
|
33
|
+
const nePusto = (v, chto) => {
|
|
34
|
+
if (typeof v !== 'string' || v.trim() === '')
|
|
35
|
+
throw new Error(`${chto}: причина пуста — «${v}»`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
t('cause отсутствует → «не указана», а не пустота', () => {
|
|
39
|
+
const r = errDetail(new Error('fetch failed'))
|
|
40
|
+
nePusto(r, 'нет cause'); if (!/не указана/.test(r)) throw new Error(`ждали «не указана», получили «${r}»`)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// 🔴 ГЛАВНАЯ: ровно тот случай, что дал 125 пустых строк в бою.
|
|
44
|
+
t('cause.message ПУСТАЯ СТРОКА → причина всё равно названа', () => {
|
|
45
|
+
const c = Object.assign(new Error(''), { code: 'ECONNRESET' })
|
|
46
|
+
const r = errDetail(Object.assign(new Error('fetch failed'), { cause: c }))
|
|
47
|
+
nePusto(r, 'пустой message')
|
|
48
|
+
if (!/ECONNRESET/.test(r)) throw new Error(`код не попал в причину: «${r}»`)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
t('AggregateError с пустым message → названа и она сама, и её внутренние', () => {
|
|
52
|
+
const vnutri = [Object.assign(new Error('connect ECONNREFUSED'), { code: 'ECONNREFUSED' })]
|
|
53
|
+
const c = new AggregateError(vnutri, '')
|
|
54
|
+
const r = errDetail(Object.assign(new Error('fetch failed'), { cause: c }))
|
|
55
|
+
nePusto(r, 'AggregateError')
|
|
56
|
+
if (!/AggregateError/.test(r)) throw new Error(`класс ошибки не назван: «${r}»`)
|
|
57
|
+
if (!/ECONNREFUSED/.test(r)) throw new Error(`внутренние причины потеряны: «${r}»`)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
t('обычная причина с message → она и печатается', () => {
|
|
61
|
+
const c = Object.assign(new Error('getaddrinfo ENOTFOUND api.telegram.org'), { code: 'ENOTFOUND' })
|
|
62
|
+
const r = errDetail(Object.assign(new Error('fetch failed'), { cause: c }))
|
|
63
|
+
if (!/ENOTFOUND/.test(r)) throw new Error(`потеряно: «${r}»`)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// Отличать «объект есть, описания нет» от «cause отсутствует» — разные состояния.
|
|
67
|
+
t('cause без единого поля → сказано, что описания нет, а не пустота', () => {
|
|
68
|
+
const r = errDetail(Object.assign(new Error('fetch failed'), { cause: Object.create(null) }))
|
|
69
|
+
nePusto(r, 'пустой объект')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// ── ДВЕ ОСИ ПРИЁМКИ ДЛЯ prichina(). ────────────────────────────────────────
|
|
73
|
+
// 🔴 ОСИ, А НЕ «ЕЩЁ ПРОБЫ». Ось — это другой ВОПРОС к тому же предмету, и пробы одной
|
|
74
|
+
// оси слепы к дефекту другой, сколько бы их ни было.
|
|
75
|
+
// ось 1 — пустое исключение -> строка НЕ пуста
|
|
76
|
+
// ось 2 — брошенный ПРИМИТИВ -> текст СОХРАНЁН
|
|
77
|
+
// Первая присланная редакция закрывала ось 1 и ломала ось 2 (`throw 'текст'` давала
|
|
78
|
+
// «string: (без пояснения)»): пустоту закрыли, потерю открыли рядом. Нашлось это не
|
|
79
|
+
// чтением кода, а второй осью.
|
|
80
|
+
const mP = text.match(/function prichina\(e\) \{[\s\S]*?\n \}/)
|
|
81
|
+
if (!mP) { console.log('СЛЕПОТА: функция prichina в предмете не найдена'); process.exit(2) }
|
|
82
|
+
let prichina
|
|
83
|
+
try { prichina = new Function(mP[0] + '; return prichina;')() }
|
|
84
|
+
catch (e) { console.log(`СЛЕПОТА: prichina не собирается: ${e?.message ?? e}`); process.exit(2) }
|
|
85
|
+
|
|
86
|
+
t('ОСЬ 1: пустые исключения дают НЕпустую строку', () => {
|
|
87
|
+
const bed = []
|
|
88
|
+
for (const [imya, v] of [['Error с пустым message', new Error('')],
|
|
89
|
+
['AggregateError пустой', new AggregateError([], '')],
|
|
90
|
+
['throw null', null], ['throw undefined', undefined],
|
|
91
|
+
['объект пустой', {}]]) {
|
|
92
|
+
const r = prichina(v)
|
|
93
|
+
if (typeof r !== 'string' || r.trim() === '' || /^(undefined|null):?\s*$/.test(r.trim()))
|
|
94
|
+
bed.push(`${imya}: «${r}»`)
|
|
95
|
+
}
|
|
96
|
+
if (bed.length) throw new Error(bed.join(' | '))
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
t('ОСЬ 2: брошенный примитив — текст СОХРАНЁН, не заменён пояснением', () => {
|
|
100
|
+
const bed = []
|
|
101
|
+
for (const [v, chto] of [['сессия не открыта', 'сессия не открыта'], [42, '42'],
|
|
102
|
+
[{ message: 'своё' }, 'своё'], [new Error('сеть'), 'сеть']]) {
|
|
103
|
+
const r = prichina(v)
|
|
104
|
+
if (!r.includes(chto)) bed.push(`потерян текст «${chto}»: «${r}»`)
|
|
105
|
+
}
|
|
106
|
+
if (bed.length) throw new Error(bed.join(' | '))
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
t('код ошибки попадает в причину, когда message пуст', () => {
|
|
110
|
+
const r = prichina(Object.assign(new Error(''), { code: 'ECONNRESET' }))
|
|
111
|
+
if (!/ECONNRESET/.test(r)) throw new Error(`код потерян: «${r}»`)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
console.log(`итог: ${ok} из ${ok + bed}`)
|
|
115
|
+
process.exit(bed === 0 ? 0 : 1)
|