alvin-bot 5.1.7 → 5.1.8

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,28 @@
2
2
 
3
3
  All notable changes to Alvin Bot are documented here.
4
4
 
5
+ ## [5.1.8] — 2026-05-17
6
+
7
+ ### Interrupted jobs auto-resume after a controlled restart
8
+
9
+ If an auto-update, `/update` or `/restart` interrupted a job while it
10
+ was running, the bot used to give up on that run and ask you to
11
+ re-trigger it manually. Now, when the interruption was a controlled
12
+ restart (not a crash) and it happened within the last 15 minutes, the
13
+ job is re-run automatically on the next tick after the bot is back.
14
+ Crash-loops are deliberately excluded — a job that keeps crashing the
15
+ bot will never resume itself — and each interrupted run is resumed at
16
+ most once. You can opt a single job out with `autoResume: false`.
17
+
18
+ ### Sub-agents report a result, not a play-by-play
19
+
20
+ Finished sub-agents (background tasks, cron jobs) no longer dump a long
21
+ step-by-step recap of how they thought and what tools they called. You
22
+ get the compact status line (success/failed · duration · tokens) plus
23
+ the actual result — nothing more. Cron reports still arrive in full;
24
+ only the meta-narration is gone, since the orchestrator processes the
25
+ real output anyway.
26
+
5
27
  ## [5.1.7] — 2026-05-17
6
28
 
7
29
  ### Scheduled jobs no longer run twice after a restart
@@ -149,6 +149,15 @@ export function prepareForExecution(job, now) {
149
149
  // ── Startup catch-up ───────────────────────────────────────
150
150
  /** Default grace window for catching up an interrupted attempt on boot. */
151
151
  export const DEFAULT_CATCHUP_GRACE_MS = 6 * 60 * 60 * 1000; // 6 h
152
+ /**
153
+ * Short window for *fast-resume*: when a controlled restart (auto-update,
154
+ * /update, /restart) interrupts a running job, re-run it immediately on
155
+ * the next boot instead of telling the user to re-trigger — but only if
156
+ * the interruption is this fresh. Deliberately minutes, not hours: a
157
+ * boot many hours later is the "surprise rerun" case slotAlreadyAttempted
158
+ * is designed to suppress; fast-resume is the immediate self-heal.
159
+ */
160
+ export const DEFAULT_FAST_RESUME_MS = 15 * 60 * 1000; // 15 min
152
161
  /**
153
162
  * Returns true when the job's *current* schedule slot has already been
154
163
  * attempted — i.e. the bot fired the job for this slot before the
@@ -195,7 +204,8 @@ function slotAlreadyAttempted(job, now) {
195
204
  *
196
205
  * PURE: returns a fresh array, never mutates the input.
197
206
  */
198
- export function handleStartupCatchup(jobs, now, graceMs = DEFAULT_CATCHUP_GRACE_MS) {
207
+ export function handleStartupCatchup(jobs, now, graceMs = DEFAULT_CATCHUP_GRACE_MS, opts = {}) {
208
+ const fastResumeMs = opts.fastResumeMs ?? DEFAULT_FAST_RESUME_MS;
199
209
  return jobs.map((job) => {
200
210
  if (!job.enabled)
201
211
  return job;
@@ -210,11 +220,29 @@ export function handleStartupCatchup(jobs, now, graceMs = DEFAULT_CATCHUP_GRACE_
210
220
  return job; // clock weirdness — skip
211
221
  if (ageMs > graceMs)
212
222
  return job; // outside grace — give up
213
- // The current schedule slot has already been attempted (even if it
214
- // crashed). Skip catch-up so the user doesn't get a surprise rerun
215
- // hours after the originally scheduled time.
216
- if (slotAlreadyAttempted(job, now))
217
- return job;
223
+ if (slotAlreadyAttempted(job, now)) {
224
+ // The slot was already attempted. Normally we leave it alone so the
225
+ // user doesn't get a surprise rerun hours later. EXCEPTION —
226
+ // fast-resume: a *controlled* restart interrupted it just minutes
227
+ // ago. Re-run immediately instead of "please re-trigger".
228
+ // • expectedRestart → never true after a crash, so a job that
229
+ // crashes the bot can't loop-resume itself.
230
+ // • autoResume === false → per-job opt-out.
231
+ // • fresh within fastResumeMs → not the "hours later" case.
232
+ // • lastFastResumeAttemptAt !== lastAttemptAt → resume a given
233
+ // interrupted attempt at most once.
234
+ const canFastResume = opts.expectedRestart === true &&
235
+ job.autoResume !== false &&
236
+ ageMs < fastResumeMs &&
237
+ job.lastFastResumeAttemptAt !== job.lastAttemptAt;
238
+ if (!canFastResume)
239
+ return job;
240
+ return {
241
+ ...job,
242
+ nextRunAt: now,
243
+ lastFastResumeAttemptAt: job.lastAttemptAt,
244
+ };
245
+ }
218
246
  // Within grace, never completed, and current slot hasn't been
219
247
  // attempted yet → catch up on next tick.
220
248
  return { ...job, nextRunAt: now };
@@ -15,6 +15,7 @@ import { resolve, dirname } from "path";
15
15
  import { CRON_FILE, BOT_ROOT } from "../paths.js";
16
16
  import { prepareForExecution, handleStartupCatchup, calculateNextRunFrom, } from "./cron-scheduling.js";
17
17
  import { resolveJobByNameOrId } from "./cron-resolver.js";
18
+ import { bootWasExpectedRestart } from "./watchdog.js";
18
19
  // ── Storage ─────────────────────────────────────────────
19
20
  function loadJobs() {
20
21
  try {
@@ -343,7 +344,9 @@ export function startScheduler() {
343
344
  // catch-up nextRunAt rewind is visible on the very next pass.
344
345
  try {
345
346
  const bootJobs = loadJobs();
346
- const caught = handleStartupCatchup(bootJobs, Date.now());
347
+ const caught = handleStartupCatchup(bootJobs, Date.now(), undefined, {
348
+ expectedRestart: bootWasExpectedRestart(),
349
+ });
347
350
  // Only persist if something actually changed to avoid needless writes
348
351
  const mutated = caught.some((j, i) => j.nextRunAt !== bootJobs[i].nextRunAt);
349
352
  if (mutated) {
@@ -286,7 +286,9 @@ async function runSubAgent(id, agentConfig, abort, resolvedName) {
286
286
  const effectiveCwd = inheritCwd
287
287
  ? agentConfig.workingDir || os.homedir()
288
288
  : os.homedir();
289
- const systemPrompt = `You are a sub-agent named "${resolvedName}". Complete the following task autonomously and report your results clearly when done. Working directory: ${effectiveCwd}`;
289
+ const systemPrompt = `You are a sub-agent named "${resolvedName}". Complete the following task autonomously. Working directory: ${effectiveCwd}
290
+
291
+ When done, return ONLY the final result/outcome, concisely. Do NOT narrate your intermediate steps, your reasoning, your tool calls, or a play-by-play of what you did — the orchestrator only needs the outcome (the answer, the report, the list, the artifact path), and on failure the error plus what was and wasn't done. No preamble, no "Here's what I did", no step-by-step recap. Run status, duration and token usage are reported separately, so don't restate them.`;
290
292
  // v4.12.2 — Map the toolset preset to an explicit allowedTools list.
291
293
  // The provider honors this override (see src/providers/claude-sdk-provider.ts
292
294
  // line ~140). Passing undefined = full access (provider default).
@@ -39,6 +39,18 @@ const BEACON_INTERVAL_MS = 30_000; // write a beacon every 30 s
39
39
  let beaconTimer = null;
40
40
  let resetTimer = null;
41
41
  let bootTime = 0;
42
+ /** Captured in startWatchdog(): did the previous process exit via a
43
+ * controlled restart? Read by the cron scheduler for fast-resume. */
44
+ let bootExpectedRestart = false;
45
+ /**
46
+ * True when this boot was preceded by a *controlled* restart
47
+ * (`markExpectedRestart` had set the beacon flag) rather than a crash.
48
+ * Returns false until startWatchdog() has run, and false after a crash —
49
+ * the safe default (no fast-resume) in both cases.
50
+ */
51
+ export function bootWasExpectedRestart() {
52
+ return bootExpectedRestart;
53
+ }
42
54
  function ensureStateDir() {
43
55
  try {
44
56
  fs.mkdirSync(STATE_DIR, { recursive: true });
@@ -155,6 +167,11 @@ export function startWatchdog() {
155
167
  ensureStateDir();
156
168
  bootTime = Date.now();
157
169
  const previous = readBeacon();
170
+ // Capture whether the *previous* process exited via a controlled
171
+ // restart (markExpectedRestart set the flag) BEFORE writeBeacon below
172
+ // resets it. The cron scheduler uses this to fast-resume a job that a
173
+ // controlled restart interrupted, while never resuming after a crash.
174
+ bootExpectedRestart = previous?.expectedRestart === true;
158
175
  const decision = decideBrakeAction(previous, bootTime);
159
176
  if (decision.action === "brake") {
160
177
  console.error(`[watchdog] crash-loop brake triggered: ${decision.reason}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alvin-bot",
3
- "version": "5.1.7",
3
+ "version": "5.1.8",
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",