alvin-bot 5.6.0 → 5.6.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/CHANGELOG.md +14 -0
- package/dist/paths.js +7 -2
- package/dist/services/subagent-delivery.js +60 -98
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Alvin Bot are documented here.
|
|
4
4
|
|
|
5
|
+
## [5.6.1] — 2026-05-18
|
|
6
|
+
|
|
7
|
+
### Background-task results stay in the chat
|
|
8
|
+
|
|
9
|
+
Results from scheduled and background tasks now appear directly in
|
|
10
|
+
the chat as before. Only an output long enough to span more than two
|
|
11
|
+
messages comes as a single attached file instead — keeping your chat
|
|
12
|
+
tidy without ever splitting a result across a wall of messages. No
|
|
13
|
+
"shortened" notices on normal-sized results; you stay in control of
|
|
14
|
+
when something gets saved as a file.
|
|
15
|
+
|
|
16
|
+
As always, verified with a fresh-install + stress test on a clean
|
|
17
|
+
separate machine.
|
|
18
|
+
|
|
5
19
|
## [5.6.0] — 2026-05-18
|
|
6
20
|
|
|
7
21
|
### Background-task reports are now clean and to the point
|
package/dist/paths.js
CHANGED
|
@@ -19,8 +19,13 @@ export const DATA_DIR = resolve(process.env.ALVIN_DATA_DIR || resolve(os.homedir
|
|
|
19
19
|
export const PUBLIC_DIR = resolve(BOT_ROOT, "web", "public");
|
|
20
20
|
/** plugins/ — Plugin directory */
|
|
21
21
|
export const PLUGINS_DIR = resolve(BOT_ROOT, "plugins");
|
|
22
|
-
/** skills/ — Skill definitions
|
|
23
|
-
|
|
22
|
+
/** skills/ — Skill definitions.
|
|
23
|
+
* Defaults to BOT_ROOT/skills (repo). Override with ALVIN_SKILLS_DIR so
|
|
24
|
+
* tests can redirect skill writes into a throwaway sandbox instead of
|
|
25
|
+
* polluting the real repo. Default (no env) is byte-identical to before. */
|
|
26
|
+
export const SKILLS_DIR = process.env.ALVIN_SKILLS_DIR
|
|
27
|
+
? resolve(process.env.ALVIN_SKILLS_DIR)
|
|
28
|
+
: resolve(BOT_ROOT, "skills");
|
|
24
29
|
/** User skills directory (custom, outside repo) */
|
|
25
30
|
export const USER_SKILLS_DIR = resolve(DATA_DIR, "skills");
|
|
26
31
|
/** Example/template files (always in repo) */
|
|
@@ -56,52 +56,39 @@ async function sendWithMarkdownFallback(api, chatId, text) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
const MAX_TG_CHUNK = 3800; // below Telegram's 4096 limit with headroom
|
|
59
|
-
// V56-T2 honesty fix — the .md file attachment is no longer gated on a
|
|
60
|
-
// separate 20k threshold. It now triggers whenever the cap actually
|
|
61
|
-
// truncates (isTruncated → body.length > BODY_CAP), so every truncated
|
|
62
|
-
// delivery carries the full output as a file and the marker is honest.
|
|
63
|
-
// (The prior 20k-only behavior is fully subsumed by isTruncated.)
|
|
64
59
|
/**
|
|
65
|
-
*
|
|
60
|
+
* Post-v5.6.0 delivery routing — by message count, NOT by a truncating
|
|
61
|
+
* cap.
|
|
66
62
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
63
|
+
* v5.6.0 introduced an inline body cap (1800 chars + a
|
|
64
|
+
* "…(truncated for chat — full output attached)" marker) that ALWAYS
|
|
65
|
+
* attached the full body as a `.md` file whenever it truncated. The
|
|
66
|
+
* effect was that even a small ~4 KB result got truncated + filed,
|
|
67
|
+
* which the user disliked. That cap is removed entirely.
|
|
71
68
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* COMPLETE uncapped output as a `.md` file via the same upload
|
|
76
|
-
* mechanism the old >20000-char path already used. The marker
|
|
77
|
-
* therefore truthfully says the full output is *attached*, instead of
|
|
78
|
-
* the previous wording that pointed at a `~/.alvin-bot/logs/` file the
|
|
79
|
-
* cap path never actually wrote. Net effect: any truncated delivery =
|
|
80
|
-
* bounded inline message + full `.md` attachment; no lossy inline-only
|
|
81
|
-
* range remains. The old >20000 path is unchanged (it already attached
|
|
82
|
-
* the full body); this just extends "attach the full file" down to
|
|
83
|
-
* "whenever the cap truncated".
|
|
69
|
+
* V56-T1 ("deliver the final result, not the transcript") is kept — a
|
|
70
|
+
* normal final result is usually short and now simply appears inline
|
|
71
|
+
* like it did before v5.6.0.
|
|
84
72
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
73
|
+
* The body is routed by how many Telegram messages it would need
|
|
74
|
+
* (MAX_TG_CHUNK = 3800):
|
|
75
|
+
* - body ≤ 1×MAX_TG_CHUNK → ONE inline message
|
|
76
|
+
* - 1×MAX_TG_CHUNK < body ≤ 2× → inline across exactly 2
|
|
77
|
+
* messages (no marker, no file)
|
|
78
|
+
* - body > 2×MAX_TG_CHUNK (≥3 chunks)→ do NOT spam 3+ messages: send
|
|
79
|
+
* the compact header + ONE
|
|
80
|
+
* short neutral note + the FULL
|
|
81
|
+
* (uncapped, complete) body as a
|
|
82
|
+
* `.md` file attachment
|
|
83
|
+
*
|
|
84
|
+
* The `(empty output)` truncated-run signal (~14 chars) is tier-1, so
|
|
85
|
+
* it stays a single inline message with no note and no file.
|
|
86
|
+
*
|
|
87
|
+
* The file in the ≥3-chunk case is the COMPLETE body — nothing is cut,
|
|
88
|
+
* so the note must NOT say "truncated". It is a minimal neutral line.
|
|
96
89
|
*/
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
function capBody(body) {
|
|
101
|
-
if (body.length <= BODY_CAP)
|
|
102
|
-
return body;
|
|
103
|
-
return `${body.slice(0, BODY_CAP)}\n\n${TRUNCATION_MARKER}`;
|
|
104
|
-
}
|
|
90
|
+
const FILE_THRESHOLD = MAX_TG_CHUNK * 2; // > this ⇒ would need ≥3 messages
|
|
91
|
+
const FULL_RESULT_NOTE = "📎 Full result attached (too long for chat).";
|
|
105
92
|
let injectedApi = null;
|
|
106
93
|
let runtimeApi = null;
|
|
107
94
|
/** Test-only hook for injecting a fake bot API. Production code must NEVER call this. */
|
|
@@ -346,56 +333,40 @@ export async function deliverSubAgentResult(info, result, opts = {}) {
|
|
|
346
333
|
}
|
|
347
334
|
const banner = buildBanner(info, result);
|
|
348
335
|
const body = result.output?.trim() || `(empty output)`;
|
|
349
|
-
// V56-T2 — bounded variant for the INLINE message path. Whenever this
|
|
350
|
-
// actually truncates (isTruncated), the FULL uncapped `body` is also
|
|
351
|
-
// attached as a .md file below, so the cap never costs the user
|
|
352
|
-
// access to the complete result and the marker stays truthful.
|
|
353
|
-
const inlineBody = capBody(body);
|
|
354
336
|
try {
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
// uncapped body as a
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
// is unchanged — it already attached the full body; the change is
|
|
361
|
-
// that mid-size now also attaches it and the marker no longer
|
|
362
|
-
// points at a logs file that was never written.)
|
|
363
|
-
if (isTruncated(body)) {
|
|
337
|
+
// Tier 3: body would need ≥3 Telegram messages → don't spam the
|
|
338
|
+
// chat. Send the compact header + ONE short neutral note + the FULL
|
|
339
|
+
// (uncapped, COMPLETE) body as a single `.md` file. Nothing is cut,
|
|
340
|
+
// so the note says nothing about truncation.
|
|
341
|
+
if (body.length > FILE_THRESHOLD) {
|
|
364
342
|
await sendWithMarkdownFallback(api, tgChatId, banner);
|
|
365
|
-
|
|
366
|
-
// the short marker is well under MAX_TG_CHUNK); send it as plain
|
|
367
|
-
// text so an unbalanced markdown slice can't crash the send.
|
|
368
|
-
await api.sendMessage(tgChatId, inlineBody.slice(0, MAX_TG_CHUNK));
|
|
343
|
+
await api.sendMessage(tgChatId, FULL_RESULT_NOTE);
|
|
369
344
|
try {
|
|
370
345
|
const { InputFile } = await import("grammy");
|
|
371
346
|
const buf = Buffer.from(body, "utf-8");
|
|
372
347
|
await api.sendDocument(tgChatId, new InputFile(buf, `${info.name}.md`));
|
|
373
348
|
}
|
|
374
349
|
catch (err) {
|
|
375
|
-
// Upload failed → the
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
// didn't attach) but this is the rare failure path, not the
|
|
379
|
-
// normal one, and there is no silent data loss.
|
|
350
|
+
// Upload failed → the user still has the banner + the note, so
|
|
351
|
+
// they know a result exists and is large. Rare failure path,
|
|
352
|
+
// no silent data loss (nothing was promised inline).
|
|
380
353
|
console.error(`[subagent-delivery] file upload failed:`, err);
|
|
381
354
|
}
|
|
382
355
|
return OK;
|
|
383
356
|
}
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (inlineBody.length + banner.length + 2 <= MAX_TG_CHUNK) {
|
|
388
|
-
await sendWithMarkdownFallback(api, tgChatId, `${banner}\n\n${inlineBody}`);
|
|
357
|
+
// Tier 1: body fits with the banner in a single message → join.
|
|
358
|
+
if (body.length + banner.length + 2 <= MAX_TG_CHUNK) {
|
|
359
|
+
await sendWithMarkdownFallback(api, tgChatId, `${banner}\n\n${body}`);
|
|
389
360
|
return OK;
|
|
390
361
|
}
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
362
|
+
// Tier 1/2: body alone needs 1 or 2 messages (≤ 2×MAX_TG_CHUNK).
|
|
363
|
+
// Send the banner, then the body chunked across at most 2 messages.
|
|
364
|
+
// No marker, no file — this is the pre-v5.6.0 inline behavior.
|
|
394
365
|
await sendWithMarkdownFallback(api, tgChatId, banner);
|
|
395
|
-
for (let i = 0; i <
|
|
366
|
+
for (let i = 0; i < body.length; i += MAX_TG_CHUNK) {
|
|
396
367
|
// Body chunks are always sent as plain text — markdown across
|
|
397
368
|
// arbitrary chunk boundaries would be inconsistent anyway.
|
|
398
|
-
await api.sendMessage(tgChatId,
|
|
369
|
+
await api.sendMessage(tgChatId, body.slice(i, i + MAX_TG_CHUNK));
|
|
399
370
|
}
|
|
400
371
|
return OK;
|
|
401
372
|
}
|
|
@@ -428,25 +399,16 @@ async function deliverViaRegistry(platform, info, result) {
|
|
|
428
399
|
const chatId = info.parentChatId;
|
|
429
400
|
const banner = buildBannerPlain(info, result);
|
|
430
401
|
const body = result.output?.trim() || `(empty output)`;
|
|
431
|
-
|
|
432
|
-
// cap truncates, the FULL uncapped `body` is attached as a .md file
|
|
433
|
-
// (if the adapter supports uploads) so the marker stays truthful and
|
|
434
|
-
// the complete output remains accessible.
|
|
435
|
-
const inlineBody = capBody(body);
|
|
436
|
-
const NON_TG_CHUNK = 3800;
|
|
402
|
+
const NON_TG_CHUNK = MAX_TG_CHUNK; // same conservative 3800 cap
|
|
437
403
|
try {
|
|
438
|
-
//
|
|
439
|
-
// the
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
|
|
444
|
-
// file) — no silent data loss.
|
|
445
|
-
if (isTruncated(body)) {
|
|
404
|
+
// Tier 3: body would need ≥3 messages → don't spam the channel.
|
|
405
|
+
// Send the banner + ONE short neutral note + the FULL (uncapped,
|
|
406
|
+
// COMPLETE) body as a `.md` file (if the adapter supports uploads).
|
|
407
|
+
// Mirrors the Telegram path exactly. No truncation — the file is
|
|
408
|
+
// the complete result.
|
|
409
|
+
if (body.length > FILE_THRESHOLD) {
|
|
446
410
|
await adapter.sendText(chatId, banner);
|
|
447
|
-
|
|
448
|
-
await adapter.sendText(chatId, inlineBody.slice(i, i + NON_TG_CHUNK));
|
|
449
|
-
}
|
|
411
|
+
await adapter.sendText(chatId, FULL_RESULT_NOTE);
|
|
450
412
|
if (adapter.sendDocument) {
|
|
451
413
|
try {
|
|
452
414
|
await adapter.sendDocument(chatId, Buffer.from(body, "utf-8"), `${info.name}.md`);
|
|
@@ -457,16 +419,16 @@ async function deliverViaRegistry(platform, info, result) {
|
|
|
457
419
|
}
|
|
458
420
|
return;
|
|
459
421
|
}
|
|
460
|
-
//
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
await adapter.sendText(chatId, `${banner}\n\n${inlineBody}`);
|
|
422
|
+
// Tier 1: body + banner fit in one message → join.
|
|
423
|
+
if (body.length + banner.length + 2 <= NON_TG_CHUNK) {
|
|
424
|
+
await adapter.sendText(chatId, `${banner}\n\n${body}`);
|
|
464
425
|
return;
|
|
465
426
|
}
|
|
466
|
-
//
|
|
427
|
+
// Tier 1/2: banner, then body chunked across at most 2 messages.
|
|
428
|
+
// No marker, no file.
|
|
467
429
|
await adapter.sendText(chatId, banner);
|
|
468
|
-
for (let i = 0; i <
|
|
469
|
-
await adapter.sendText(chatId,
|
|
430
|
+
for (let i = 0; i < body.length; i += NON_TG_CHUNK) {
|
|
431
|
+
await adapter.sendText(chatId, body.slice(i, i + NON_TG_CHUNK));
|
|
470
432
|
}
|
|
471
433
|
}
|
|
472
434
|
catch (err) {
|