alvin-bot 5.1.5 → 5.1.6

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,29 @@
2
2
 
3
3
  All notable changes to Alvin Bot are documented here.
4
4
 
5
+ ## [5.1.6] — 2026-05-15
6
+
7
+ ### Planned restarts really stop counting as crashes now
8
+
9
+ v5.1.5 added a flag that marks self-updates and `/update` / `/restart`
10
+ as intentional so they don't inflate the crash counter. Half of it
11
+ didn't actually work: the code that reads the saved state back on the
12
+ next boot rebuilt it field by field and silently dropped that very
13
+ flag, so the crash detector never saw it and planned restarts were
14
+ still scored as crashes. (The other half of v5.1.5 — not counting
15
+ benign log lines as errors — was unaffected and has been working.)
16
+
17
+ This release makes the read path preserve the flag, so a planned
18
+ restart is now genuinely treated as a clean exit. The state-parsing
19
+ logic was pulled into a tested pure function so the read-back round
20
+ trip can't silently regress like this again.
21
+
22
+ ### What this means for you
23
+
24
+ If you updated to 5.1.5 and still saw the crash count tick up by one
25
+ each time the bot updated itself, that stops now. The error-trend
26
+ half of the 5.1.5 fix already worked; this completes the crash half.
27
+
5
28
  ## [5.1.5] — 2026-05-15
6
29
 
7
30
  ### Health monitor no longer cries wolf about its own log lines
@@ -27,6 +27,45 @@ export const DEFAULTS = {
27
27
  * crashes with ≥5 min gaps sailed right past the brake. 1 h is safer. */
28
28
  RESET_AFTER_MS: 60 * 60_000,
29
29
  };
30
+ /**
31
+ * Validate + normalize a parsed beacon JSON into a BeaconData (or null
32
+ * if the core fields are missing/wrong-typed). Pure so the read-path
33
+ * field mapping is unit-testable — extracted after v5.1.5 shipped a
34
+ * broken expectedRestart: the old readBeacon() rebuilt the object
35
+ * field-by-field and silently dropped expectedRestart, so the flag
36
+ * never reached decideBrakeAction and intentional restarts were still
37
+ * counted as crashes. Whatever round-trips here is what the brake sees.
38
+ */
39
+ export function normalizeBeacon(parsed) {
40
+ if (!parsed)
41
+ return null;
42
+ if (typeof parsed.lastBeat === "number" &&
43
+ typeof parsed.pid === "number" &&
44
+ typeof parsed.bootTime === "number" &&
45
+ typeof parsed.crashCount === "number" &&
46
+ typeof parsed.crashWindowStart === "number" &&
47
+ typeof parsed.version === "string") {
48
+ return {
49
+ lastBeat: parsed.lastBeat,
50
+ pid: parsed.pid,
51
+ bootTime: parsed.bootTime,
52
+ crashCount: parsed.crashCount,
53
+ crashWindowStart: parsed.crashWindowStart,
54
+ version: parsed.version,
55
+ // Older beacons don't have daily-counter fields — default them to
56
+ // 0/now so the brake logic treats this run as the start of the
57
+ // first daily window.
58
+ dailyCrashCount: typeof parsed.dailyCrashCount === "number" ? parsed.dailyCrashCount : 0,
59
+ dailyCrashWindowStart: typeof parsed.dailyCrashWindowStart === "number"
60
+ ? parsed.dailyCrashWindowStart
61
+ : Date.now(),
62
+ // The whole point of the v5.1.6 fix: propagate expectedRestart so
63
+ // a planned restart is not scored as a crash on the next boot.
64
+ expectedRestart: parsed.expectedRestart === true,
65
+ };
66
+ }
67
+ return null;
68
+ }
30
69
  /**
31
70
  * Given the previous beacon (or null on first boot) and the current time,
32
71
  * decide whether the bot should proceed with boot or engage the crash-loop
@@ -29,7 +29,7 @@ import { execSync } from "child_process";
29
29
  import { BOT_VERSION } from "../version.js";
30
30
  import { emitCritical } from "./critical-notify.js";
31
31
  import { writeDiagnosticBundle } from "./auto-diagnostic.js";
32
- import { decideBrakeAction, shouldResetCrashCounter, DEFAULTS, } from "./watchdog-brake.js";
32
+ import { decideBrakeAction, shouldResetCrashCounter, normalizeBeacon, DEFAULTS, } from "./watchdog-brake.js";
33
33
  const DATA_DIR = process.env.ALVIN_DATA_DIR || resolve(os.homedir(), ".alvin-bot");
34
34
  const STATE_DIR = resolve(DATA_DIR, "state");
35
35
  const BEACON_FILE = resolve(STATE_DIR, "watchdog.json");
@@ -50,30 +50,7 @@ function ensureStateDir() {
50
50
  function readBeacon() {
51
51
  try {
52
52
  const raw = fs.readFileSync(BEACON_FILE, "utf-8");
53
- const parsed = JSON.parse(raw);
54
- if (typeof parsed.lastBeat === "number" &&
55
- typeof parsed.pid === "number" &&
56
- typeof parsed.bootTime === "number" &&
57
- typeof parsed.crashCount === "number" &&
58
- typeof parsed.crashWindowStart === "number" &&
59
- typeof parsed.version === "string") {
60
- // Older beacons don't have daily-counter fields — default them to
61
- // 0/now so the brake logic treats this run as the start of the
62
- // first daily window.
63
- return {
64
- lastBeat: parsed.lastBeat,
65
- pid: parsed.pid,
66
- bootTime: parsed.bootTime,
67
- crashCount: parsed.crashCount,
68
- crashWindowStart: parsed.crashWindowStart,
69
- version: parsed.version,
70
- dailyCrashCount: typeof parsed.dailyCrashCount === "number" ? parsed.dailyCrashCount : 0,
71
- dailyCrashWindowStart: typeof parsed.dailyCrashWindowStart === "number"
72
- ? parsed.dailyCrashWindowStart
73
- : Date.now(),
74
- };
75
- }
76
- return null;
53
+ return normalizeBeacon(JSON.parse(raw));
77
54
  }
78
55
  catch {
79
56
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alvin-bot",
3
- "version": "5.1.5",
3
+ "version": "5.1.6",
4
4
  "description": "Alvin Bot — Your personal AI agent on Telegram, WhatsApp, Discord, Signal, and Web.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",