alvin-bot 5.6.0 → 5.6.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 CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  All notable changes to Alvin Bot are documented here.
4
4
 
5
+ ## [5.6.2] — 2026-05-19
6
+
7
+ ### Long background-task results now reliably arrive in chat
8
+
9
+ A background task that produced a long final answer could finish
10
+ successfully and yet never be delivered — you would see nothing and
11
+ have to ask for the status by hand. Alvin now recognises a finished
12
+ background task no matter how long its result is, so the answer always
13
+ lands in your chat the moment the task completes.
14
+
15
+ ## [5.6.1] — 2026-05-18
16
+
17
+ ### Background-task results stay in the chat
18
+
19
+ Results from scheduled and background tasks now appear directly in
20
+ the chat as before. Only an output long enough to span more than two
21
+ messages comes as a single attached file instead — keeping your chat
22
+ tidy without ever splitting a result across a wall of messages. No
23
+ "shortened" notices on normal-sized results; you stay in control of
24
+ when something gets saved as a file.
25
+
26
+ As always, verified with a fresh-install + stress test on a clean
27
+ separate machine.
28
+
5
29
  ## [5.6.0] — 2026-05-18
6
30
 
7
31
  ### Background-task reports are now clean and to the point
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  > Your personal AI agent — on Telegram, WhatsApp, Discord, Slack, Signal, Terminal, and Web.
4
4
 
5
- Open-source, self-hosted, multi-model. Lives where you chat, has full shell + filesystem access, remembers across sessions, and dispatches detached sub-agents for long-running work. Built on the Claude Agent SDK with a provider-agnostic engine that also drives OpenAI, Groq, Gemini, NVIDIA NIM, OpenRouter, and Ollama.
5
+ Alvin Bot is an open-source, MIT-licensed, self-hosted autonomous AI agent that runs on your own machine and answers you on Telegram, Slack, Discord, WhatsApp, Signal, a terminal TUI, and a web dashboard. It is built on the official Claude Agent SDK and runs a provider-agnostic engine that also drives OpenAI, Groq, Google Gemini, NVIDIA NIM, OpenRouter, and Ollama, with automatic failover after two consecutive provider failures and a heartbeat health check every five minutes. Unlike most personal AI agents, it ships a zero-config indexed memory store: with no embedding API key it falls back to a built-in SQLite FTS5 keyword index, so recall works out of the box. It dispatches detached sub-agents as independent `claude -p` subprocesses that keep running and deliver their result even if the parent conversation is aborted. It is local-first and telemetry-free — prompts and responses are never logged off-machine, secrets live in a chmod-0600 `.env`, and shell execution is allowlisted by default.
6
6
 
7
- > **What's new — v4.22 (May 2026):** Pluggable memory backends Gemini · OpenAI · Ollama · **FTS5 keyword fallback (zero-config)**. Users without an embedding API key now get a working indexed memory store out of the box. Smart inject mode trims ~25 k tokens per turn off long system prompts. [Full changelog →](CHANGELOG.md)
7
+ > **What's new — v5.6.2 (May 2026):** Background-task and scheduled-job results now land cleanly in chat a tight header (what ran, duration, tokens, success) plus the answer, with very long output attached as a single file instead of a wall of messages. Built on v5.5's instant, honest ⛔ Stop and calmer, evidence-based health monitoring. Earlier in the 5.x line: a zero-config FTS5 keyword memory index (indexed recall with **no embedding API key**), automatic multi-provider failover with a 5-minute heartbeat monitor, and detached sub-agents that survive a parent abort. [Full changelog →](CHANGELOG.md)
8
8
 
9
9
  ---
10
10
 
@@ -47,6 +47,34 @@ Open-source, self-hosted, multi-model. Lives where you chat, has full shell + fi
47
47
 
48
48
  ---
49
49
 
50
+ ## ⚖️ How Alvin Bot compares
51
+
52
+ Alvin Bot sits in the same category as **Hermes Agent** (Nous Research) and **OpenClaw** — self-hosted, open-source personal AI agents that live on your machine and reach you on the chat apps you already use. They optimize for different things. This table is intended to be fair: where Hermes or OpenClaw is the better tool, it says so.
53
+
54
+ | Dimension | **Alvin Bot** | **Hermes Agent** | **OpenClaw** |
55
+ |---|---|---|---|
56
+ | License / hosting | MIT · self-hosted · local-first · zero telemetry | MIT · self-hosted · 7 execution backends | Open-source · self-hosted · bring-your-own-key |
57
+ | Model providers | Claude Agent SDK + OpenAI · Groq · Gemini · NVIDIA NIM · OpenRouter · Ollama, with **automatic failover after 2 provider failures + a 5-min heartbeat monitor** | 200+ models | Bring-your-own model / key |
58
+ | Sub-agents | **Detached `claude -p` subprocesses that survive a parent abort**; `readonly`/`research` toolset presets | Isolated subagents for parallel workstreams | Not a primary focus |
59
+ | Browser automation | **4-tier escalation**: WebFetch → stealth Playwright → persistent-profile CDP → agent-browser CLI | Built-in browse / vision tools | Via tools |
60
+ | Platforms | Telegram · Slack · Discord · WhatsApp · Signal · terminal TUI · Web (7) | 20+ platforms from one gateway | 25–50+ platforms · native mobile apps · voice activation |
61
+ | Memory | Layered L0–L3; SQLite embeddings with a **zero-config FTS5 keyword fallback (works with no API key)**; smart prompt-injection trims ~25 k tokens/turn | SQLite + full-text search · agent-curated · Honcho user profiling | Transparent plain Markdown/YAML files you can grep and git-track |
62
+ | Extensibility | Hot-reload skills + 6 plugins · self-modifying skills · hooks · MCP client | 40+ built-in tools · **autonomous self-improving skill loop** | Skills as files · very large ecosystem |
63
+ | MCP | MCP **client** (connect any MCP server) | MCP client **and `hermes mcp serve`** (acts as an MCP server for Claude Desktop / Cursor / VS Code) | Tool integrations |
64
+ | Self-healing | **Startup preflight · dead-man's-switch heartbeat · crash forensic bundles · AI self-diagnosis · crash-loop brake · trend anomaly detection** | Stable in practice; self-improving | Frequent updates can break running instances |
65
+ | Security defaults | Exec **allowlist + shell-metachar filter on by default** · DM pairing · timing-safe webhook auth · 0600 file perms enforced · `alvin-bot audit` CLI · honestly documented threat model | Standard | Standard |
66
+ | Maturity / community | Small, focused, single-maintainer; modest public adoption | Large community, Nous Research team | Large community + team, Nvidia NemoClaw fork |
67
+
68
+ ### Use the right tool for the job
69
+
70
+ - **Use Alvin Bot when** you want one resilient, self-healing agent on your own box that keeps working when a provider rate-limits or fails, gives you indexed memory **without buying an embedding API key**, ships safe-by-default execution sandboxing, and is built directly on the official Claude Agent SDK — and you mainly live in Telegram / Slack / Discord / WhatsApp / Signal.
71
+ - **Use Hermes Agent when** you want a research-grade self-improving agent, need it to act as an **MCP server** for Claude Desktop / Cursor / VS Code, want 200+ model choice or many execution backends, and value a large community.
72
+ - **Use OpenClaw when** you want the **widest messaging reach** (25–50+ channels) plus native mobile apps and voice activation, fully transparent plain-file memory you can git-track, and the largest ecosystem.
73
+
74
+ <!-- comparison-page link intentionally deferred until the landing page is live (HTTP 200); re-enabled in a separate commit per docs/positioning/05-SHIP-CHECKLIST.md Step 5 -->
75
+
76
+ ---
77
+
50
78
  ## 🚀 Quick Start
51
79
 
52
80
  ```bash
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
- export const SKILLS_DIR = resolve(BOT_ROOT, "skills");
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) */
@@ -68,6 +68,92 @@ export function parseAsyncLaunchedToolResult(raw) {
68
68
  return { agentId, outputFile };
69
69
  }
70
70
  const DEFAULT_TAIL_BYTES = 64 * 1024;
71
+ /**
72
+ * Upper bound for the window-independent final-line read (see
73
+ * readLastCompleteLine). Generous — ~128× the tail window — so any
74
+ * realistic final report is captured, but bounded so a pathological
75
+ * single line can't blow up memory. Beyond this we fall back to the
76
+ * windowed / staleness logic unchanged.
77
+ */
78
+ const MAX_LAST_LINE_BYTES = 8 * 1024 * 1024;
79
+ /**
80
+ * Read the LAST complete newline-delimited record of the file, regardless
81
+ * of how large it is, by scanning backward from EOF in chunks.
82
+ *
83
+ * Why this exists: for `claude -p --output-format stream-json` the
84
+ * terminating `{"type":"result",...}` event is ALWAYS the final line, but
85
+ * that line embeds the entire final report and is frequently larger than
86
+ * DEFAULT_TAIL_BYTES (e.g. an agent that writes a long status report
87
+ * after an auto-declined AskUserQuestion). The windowed tail read drops
88
+ * such a line as a truncated head fragment, so completion was missed and
89
+ * the agent sat "running" until the 12 h timeout — never auto-delivered.
90
+ * Reading the true final line makes completion detection independent of
91
+ * the tail-window size.
92
+ *
93
+ * Returns the final record WITHOUT its trailing newline, or null if the
94
+ * file is empty, the final line is not newline-terminated (still being
95
+ * written), or the line exceeds MAX_LAST_LINE_BYTES. In every null case
96
+ * the caller falls through to the existing windowed/staleness logic with
97
+ * no behavior change.
98
+ */
99
+ async function readLastCompleteLine(path, size) {
100
+ if (size <= 0)
101
+ return null;
102
+ let fh;
103
+ try {
104
+ fh = await fs.open(path, "r");
105
+ const chunkSize = 64 * 1024;
106
+ let pos = size;
107
+ let collected = Buffer.alloc(0);
108
+ while (pos > 0) {
109
+ if (size - pos > MAX_LAST_LINE_BYTES)
110
+ return null;
111
+ const readLen = Math.min(chunkSize, pos);
112
+ pos -= readLen;
113
+ const buf = Buffer.alloc(readLen);
114
+ await fh.read(buf, 0, readLen, pos);
115
+ collected = Buffer.concat([buf, collected]);
116
+ // Strip exactly one trailing newline (the file terminator) so we
117
+ // search for the delimiter BEFORE the final record, not after it.
118
+ let end = collected.length;
119
+ if (end > 0 && collected[end - 1] === 0x0a) {
120
+ end--;
121
+ if (end > 0 && collected[end - 1] === 0x0d)
122
+ end--;
123
+ }
124
+ else {
125
+ // No terminating newline → final line still being written.
126
+ return null;
127
+ }
128
+ if (end <= 0) {
129
+ // File is just a newline (or empty after trim) — nothing usable.
130
+ if (pos === 0)
131
+ return null;
132
+ continue;
133
+ }
134
+ const nl = collected.lastIndexOf(0x0a, end - 1);
135
+ if (nl >= 0) {
136
+ return collected.toString("utf-8", nl + 1, end);
137
+ }
138
+ if (pos === 0) {
139
+ // Whole file is a single (newline-terminated) record.
140
+ return collected.toString("utf-8", 0, end);
141
+ }
142
+ }
143
+ return null;
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ finally {
149
+ try {
150
+ await fh?.close();
151
+ }
152
+ catch {
153
+ /* ignore */
154
+ }
155
+ }
156
+ }
71
157
  /**
72
158
  * v4.12.4 — Default staleness window for partial-output delivery.
73
159
  *
@@ -148,12 +234,67 @@ export async function parseOutputFileStatus(path, opts = {}) {
148
234
  const usable = lines
149
235
  .slice(headIncomplete, lines.length - (trailIncomplete > 0 ? trailIncomplete : 0))
150
236
  .filter((l) => l.length > 0);
237
+ // Window-independent completion check (regression fix). The terminating
238
+ // `{"type":"result",...}` event for `claude -p --output-format
239
+ // stream-json` is ALWAYS the final line, but it embeds the whole final
240
+ // report and is routinely larger than maxTailBytes — the windowed tail
241
+ // below would drop it as a truncated head fragment, leaving the agent
242
+ // mis-classified "running" until the 12 h timeout (so a completed
243
+ // sub-agent is never auto-delivered and the user must ask "status?").
244
+ // Inspect the TRUE final complete line directly so detection no longer
245
+ // depends on the tail-window size. Falls through unchanged when the
246
+ // last line is not a result event (running / killed mid-write / etc.).
247
+ const finalLine = await readLastCompleteLine(path, stat.size);
248
+ if (finalLine) {
249
+ let parsedFinal = null;
250
+ try {
251
+ parsedFinal = JSON.parse(finalLine);
252
+ }
253
+ catch {
254
+ parsedFinal = null;
255
+ }
256
+ if (parsedFinal && parsedFinal.type === "result") {
257
+ let output = typeof parsedFinal.result === "string" ? parsedFinal.result : "";
258
+ if (!output) {
259
+ // Same aggregation fallback as the windowed FIRST PASS: when the
260
+ // result event carries no `result` text, stitch together the
261
+ // assistant text blocks visible in the tail.
262
+ const fragments = [];
263
+ for (const line of usable) {
264
+ let p;
265
+ try {
266
+ p = JSON.parse(line);
267
+ }
268
+ catch {
269
+ continue;
270
+ }
271
+ if (p.type === "assistant" && Array.isArray(p.message?.content)) {
272
+ for (const c of p.message.content) {
273
+ if (c?.type === "text" && typeof c.text === "string") {
274
+ fragments.push(c.text);
275
+ }
276
+ }
277
+ }
278
+ }
279
+ output = fragments.join("\n\n").trim();
280
+ }
281
+ const usage = parsedFinal.usage;
282
+ const tokensUsed = usage
283
+ ? {
284
+ input: usage.input_tokens ?? 0,
285
+ output: usage.output_tokens ?? 0,
286
+ }
287
+ : undefined;
288
+ return { state: "completed", output, tokensUsed };
289
+ }
290
+ }
151
291
  // v4.13 — FIRST PASS: look for a `{"type":"result"}` event anywhere in
152
292
  // the tail. This is the completion signal for `claude -p
153
293
  // --output-format stream-json` output (used by the v4.13 dispatch
154
294
  // mechanism). When present, the `result` field holds the authoritative
155
295
  // final text. If `result.result` is missing, aggregate from all
156
- // assistant text blocks in the tail.
296
+ // assistant text blocks in the tail. (Retained as a defensive fallback
297
+ // for the rare case the result event is NOT the final line.)
157
298
  for (let i = usable.length - 1; i >= 0; i--) {
158
299
  let parsed;
159
300
  try {
@@ -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
- * V56-T2 (Layer-2)honest hard cap on the INLINE delivered body.
60
+ * Post-v5.6.0 delivery routing by message count, NOT by a truncating
61
+ * cap.
66
62
  *
67
- * V56-T1 made delivery carry the SDK final result instead of the whole
68
- * transcript, but a final result can itself occasionally be very long.
69
- * This bounds the inline-message body so a single agent answer can't
70
- * flood the chat, while staying HONEST.
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
- * Honesty contract (fixed after a review found a self-defeating
73
- * regression): whenever `capBody` actually truncates i.e. the body is
74
- * non-empty AND longer than BODY_CAP — the delivery ALSO attaches the
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
- * This is a pure bounded slice + a fixed marker — NOT a structure-
86
- * guessing heuristic. It no-ops on empty/whitespace so the
87
- * `(empty output)` truncated-run signal keeps working (and no spurious
88
- * file is attached for it).
89
- */
90
- const BODY_CAP = 1800;
91
- const TRUNCATION_MARKER = "…(truncated for chat — full output attached)";
92
- /**
93
- * True when `capBody` would actually truncate this body the single
94
- * source of truth for "did we drop content, so the full output must be
95
- * attached as a file". Mirrors the `length > BODY_CAP` test in capBody.
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
- function isTruncated(body) {
98
- return body.length > BODY_CAP;
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
- // Truncated honest delivery: short banner + bounded inline body
356
- // (with the truthful "full output attached" marker) + the COMPLETE
357
- // uncapped body as a .md file. This single branch covers the whole
358
- // truncated range (mid-size AND the old > 20000-char range): there
359
- // is no lossy inline-only range anymore. (The old >20000 behavior
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
- // The bounded inline body fits in one message (BODY_CAP=1800 plus
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 bounded inline body was already delivered
376
- // above, so the user still has something honest (banner + capped
377
- // text + marker). The marker slightly over-promises here (file
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
- // Not truncated (body BODY_CAP)unchanged passthrough.
385
- // inlineBody === body here (capBody is a no-op), no marker, no file.
386
- // Case A: fits in a single message → banner + body joined
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
- // Case B: defensive a ≤1800-char body still under-runs MAX_TG_CHUNK
392
- // with the banner, but keep the banner-then-chunk fallback for
393
- // safety against an unusually long banner.
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 < inlineBody.length; i += MAX_TG_CHUNK) {
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, inlineBody.slice(i, i + MAX_TG_CHUNK));
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
- // V56-T2 same honest contract as the Telegram path. Whenever the
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
- // Truncated honest delivery: banner + bounded inline body (with
439
- // the truthful "full output attached" marker) + the COMPLETE
440
- // uncapped body as a .md file. Covers the whole truncated range
441
- // (mid-size AND > the old 20k threshold)no lossy inline-only
442
- // range remains. If the adapter has no sendDocument or the upload
443
- // fails, the bounded inline body still went out (honest, just no
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 truncationthe file is
408
+ // the complete result.
409
+ if (body.length > FILE_THRESHOLD) {
446
410
  await adapter.sendText(chatId, banner);
447
- for (let i = 0; i < inlineBody.length; i += NON_TG_CHUNK) {
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
- // Not truncated (body BODY_CAP)unchanged passthrough.
461
- // inlineBody === body here, no marker, no file.
462
- if (inlineBody.length + banner.length + 2 <= NON_TG_CHUNK) {
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
- // Defensive banner-then-chunk fallback (e.g. unusually long banner).
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 < inlineBody.length; i += NON_TG_CHUNK) {
469
- await adapter.sendText(chatId, inlineBody.slice(i, i + NON_TG_CHUNK));
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) {
package/llms.txt ADDED
@@ -0,0 +1,38 @@
1
+ # Alvin Bot
2
+
3
+ > Alvin Bot is an open-source, MIT-licensed, self-hosted autonomous AI agent that runs on your own machine and answers you on Telegram, Slack, Discord, WhatsApp, Signal, a terminal TUI, and a web dashboard. It is built on the official Claude Agent SDK and runs a provider-agnostic engine that also drives OpenAI, Groq, Google Gemini, NVIDIA NIM, OpenRouter, and Ollama, with automatic failover after two consecutive provider failures and a heartbeat health check every five minutes. It is local-first and telemetry-free: prompts and responses are never logged off-machine, secrets live in a chmod-0600 .env, and shell execution is allowlisted by default.
4
+
5
+ Alvin Bot is in the same category as Hermes Agent (Nous Research) and OpenClaw: a self-hosted personal AI agent with persistent memory, scheduled tasks, real shell/filesystem access, and multi-platform chat delivery. It differentiates on resilience (automatic provider failover + a self-preservation subsystem), zero-config memory (indexed recall with no embedding API key), detached sub-agents that survive a parent abort, and safe-by-default security.
6
+
7
+ ## Key capabilities
8
+
9
+ - Multi-provider engine: Claude Agent SDK + OpenAI, Groq, Google Gemini, NVIDIA NIM, OpenRouter, Ollama, any OpenAI-compatible API; automatic failover after 2 provider failures, 5-minute heartbeat health check, reorderable fallback chain.
10
+ - Detached sub-agents: `alvin_dispatch_agent` spawns independent `claude -p` subprocesses that keep running and deliver their result even if the parent conversation is aborted; `readonly`/`research` toolset presets restrict their privileges.
11
+ - Zero-config layered memory: SQLite embeddings store with a built-in FTS5 keyword fallback so indexed recall works with no embedding API key; smart prompt-injection mode trims ~25k tokens/turn once the store is populated.
12
+ - 4-tier browser automation: WebFetch → stealth Playwright → persistent-profile Chromium over CDP → opt-in agent-browser CLI; handles login-walled and bot-protected pages.
13
+ - Self-preservation subsystem: startup preflight, dead-man's-switch heartbeat, crash forensic bundles, AI self-diagnosis, crash-loop brake, daily trend anomaly detection — detects and recovers from its own failures unattended.
14
+ - Multi-platform: Telegram, Slack (one channel = one isolated workspace), Discord, WhatsApp (owner-approval gate), Signal, terminal TUI, full web dashboard.
15
+ - Cron / scheduled AI tasks in natural language (e.g. "check my email every morning"); MCP client (connect any Model Context Protocol server); hot-reload skills + 6 built-in plugins; hooks; per-channel context-isolated workspaces with shared memory.
16
+ - Security defaults: exec allowlist + shell-metachar filter on by default, DM pairing, timing-safe webhook auth, enforced 0600 file permissions, `alvin-bot audit` CLI, an honestly documented threat model (prompt injection is acknowledged as unsolved, not pretended away).
17
+ - Distribution: npm global install, from-source, one-line installer, Docker, macOS Electron .dmg with system tray; macOS launchd integration keeps the Claude Max OAuth token working across restarts.
18
+
19
+ ## When to use Alvin Bot
20
+
21
+ - Use Alvin Bot when you want one resilient, self-healing personal agent on your own machine that keeps working when a provider fails, gives you indexed memory without buying an embedding API key, ships safe-by-default execution sandboxing, and is built directly on the official Claude Agent SDK.
22
+ - Use Hermes Agent when you want a research-grade self-improving learning loop, need the agent to act as an MCP server for Claude Desktop / Cursor / VS Code, want 200+ model choice or many execution backends, or value a large community.
23
+ - Use OpenClaw when you want the widest messaging reach (25–50+ channels) plus native mobile apps and voice activation, fully transparent plain-file memory you can git-track, and the largest ecosystem.
24
+
25
+ ## Documentation
26
+
27
+ - [README](https://github.com/alvbln/Alvin-Bot/blob/main/README.md): full feature list, quick start, architecture, provider matrix, configuration.
28
+ - [Handbook](https://github.com/alvbln/Alvin-Bot/blob/main/docs/HANDBOOK.md): complete standalone reference — providers, sub-agents, cron, plugins, MCP, platforms.
29
+ - [Changelog](https://github.com/alvbln/Alvin-Bot/blob/main/CHANGELOG.md): per-release notes; current version 5.6.1.
30
+ - [Security threat model](https://github.com/alvbln/Alvin-Bot/blob/main/docs/security.md): honest threat model and hardening guide.
31
+ - [Alvin Bot vs Hermes vs OpenClaw](https://alvin.alev-b.com/vs/hermes-openclaw): fair, named head-to-head comparison and decision guide.
32
+ - [npm package](https://www.npmjs.com/package/alvin-bot): `npm install -g alvin-bot`.
33
+ - [GitHub repository](https://github.com/alvbln/Alvin-Bot): source, issues, releases.
34
+
35
+ ## Optional
36
+
37
+ - [Multi-session workspaces](https://github.com/alvbln/Alvin-Bot/blob/main/README.md#-multi-session-workspaces-v4120): parallel per-channel context-isolated sessions with globally shared memory.
38
+ - [Slack setup](https://github.com/alvbln/Alvin-Bot/releases/latest): copy-paste Slack App manifest + step-by-step guide.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "alvin-bot",
3
- "version": "5.6.0",
4
- "description": "Alvin Bot — Your personal AI agent on Telegram, WhatsApp, Discord, Signal, and Web.",
3
+ "version": "5.6.2",
4
+ "description": "Alvin Bot — open-source, self-hosted autonomous AI agent on Telegram, Slack, Discord, WhatsApp, Signal, terminal & web. Built on the Claude Agent SDK with a multi-provider engine (OpenAI, Groq, Gemini, NVIDIA NIM, OpenRouter, Ollama) and automatic failover, detached sub-agents that survive a parent abort, zero-config indexed memory (no embedding key needed), 4-tier browser automation, cron tasks, MCP client and a self-preservation subsystem. Local-first, telemetry-free. An OpenClaw / Hermes Agent alternative.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -143,6 +143,8 @@
143
143
  "ai",
144
144
  "claude",
145
145
  "agent",
146
+ "ai-agent",
147
+ "autonomous-agent",
146
148
  "llm",
147
149
  "multi-model",
148
150
  "gpt",
@@ -150,14 +152,26 @@
150
152
  "nvidia",
151
153
  "self-hosted",
152
154
  "autonomous",
155
+ "personal-assistant",
153
156
  "whatsapp",
154
157
  "discord",
155
158
  "signal",
159
+ "slack",
156
160
  "openai",
157
161
  "groq",
162
+ "ollama",
163
+ "openrouter",
158
164
  "chatbot",
159
165
  "assistant",
160
- "electron"
166
+ "electron",
167
+ "sub-agents",
168
+ "mcp",
169
+ "mcp-client",
170
+ "claude-agent-sdk",
171
+ "cron",
172
+ "skills",
173
+ "openclaw-alternative",
174
+ "hermes-alternative"
161
175
  ],
162
176
  "author": "alvbln",
163
177
  "license": "MIT",