Skip to content

fix(pulse/imessage): block startIMessage until stop, ending supervisor restart loop#1277

Open
janrenz wants to merge 1 commit into
danielmiessler:mainfrom
janrenz:fix/imessage-supervisor-restart-loop
Open

fix(pulse/imessage): block startIMessage until stop, ending supervisor restart loop#1277
janrenz wants to merge 1 commit into
danielmiessler:mainfrom
janrenz:fix/imessage-supervisor-restart-loop

Conversation

@janrenz
Copy link
Copy Markdown

@janrenz janrenz commented May 17, 2026

Problem

Pulse/modules/imessage.ts:startIMessage() sets up the SQLite poll setInterval and then returns immediately. The supervisor in pulse.ts:supervise() treats the resolved promise as a clean exit:

// pulse.ts:200-205
await fn()
if (!shuttingDown()) {
  log("info", \`${name} exited cleanly, restarting in 10s\`)
  await Bun.sleep(10_000)
}

Every 10 seconds it re-invokes startIMessage(), where the running guard catches the duplicate call and emits a warn. pulse-stdout.log ends up with this pattern forever after the module successfully starts:

{"msg":"iMessage module already running, ignoring start request"}
{"msg":"imessage exited cleanly, restarting in 10s"}

The poll itself works correctly throughout — message ingestion isn't affected — but every 10s there's needless work plus log noise that drowns out real events.

Why this pattern is unique to imessage

modules/telegram.ts:345 already documents the same shape and how it avoided the bug:

// Start polling — await keeps startTelegram() alive until bot.stop() is called.
// Without await, the supervisor thinks the function exited and restarts it,
// causing a grammY 409 conflict (two polling loops on the same bot token).
await bot.start({ ... })

iMessage uses a bare setInterval instead of an awaited polling primitive, so the same protection was missing.

Fix (+15 lines, one file)

  • Module-scope: let stopResolve: (() => void) | null = null
  • End of startIMessage: after setInterval(...) and the polling every Xms log, await new Promise<void>(resolve => { stopResolve = resolve })
  • End of stopIMessage: after running = false, release the captured resolver

With this, the supervisor sees startIMessage as long-running. The 10s restart cycle goes away.

Repro

  1. Healthy macOS install with Full Disk Access granted to the launchd-supervised bun.
  2. PULSE.toml [imessage] enabled with at least one allowed_handles entry.
  3. tail -f ~/.claude/PAI/Pulse/logs/pulse-stdout.log | grep imessage shows the alternating start/exited lines every 10s.

After fix: a single iMessage module startedpolling every 3000ms then silence (until a real inbound message or shutdown).

Notes

  • No behavior change for inbound messages, replies, cursor persistence, or shutdown.
  • stopIMessage() already idempotent on running=false; the new resolver release follows the same guard.
  • A similar but separate pattern exists for the telegram module path where the module returns early on missing bot token — that produces the same 10s log spam. Out of scope for this PR; happy to follow up.

Found while wiring up iMessage on a fresh v5.0.0 install. Local fix verified: 0 already running warnings over 25s observation window after Pulse restart, vs. 3 in the same window before the fix.

…r restart loop

startIMessage sets up the setInterval poll timer and then returns immediately.
The supervisor in pulse.ts treats that return as a clean exit and re-invokes
the module every 10 seconds, where the running-flag guard catches the second
call and logs 'iMessage module already running, ignoring start request'.
The poll loop itself works correctly throughout — this is pure log spam, plus
an unnecessary every-10s call overhead — but it's the same shape of bug the
telegram module solved by awaiting bot.start() (see modules/telegram.ts:344-348
comment).

Fix mirrors that pattern: at end of startIMessage, await a Promise whose
resolve() is captured in a module-scoped stopResolve. stopIMessage releases
it after clearing the timer and persisting the cursor. With this, supervisor
sees startIMessage as long-running and stops the 10s restart cycle.

Reproduces on any healthy install with Full Disk Access granted and an
allowed_handles list. Observable in pulse-stdout.log as alternating
'imessage exited cleanly, restarting in 10s' / 'iMessage module already
running, ignoring start request' lines every 10s.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant