Keep the connector's watch alive across sleep and an expired Claude login
What people saw
On a laptop with default sleep settings, an agent run through basecamp connect acknowledges a request with 👀 and then never answers. The same happens after Claude Code's login expires, even once the person logs back in.
Why
The connector is fine. It keeps receiving events and boosting them. The Claude Code session watching its output is what stops.
- The skill told the session to arm a monitor with
persistent: true. The Monitor tool has no such option. Every monitor is killed after 30 minutes at most, and only a turn of the session arms it again. - When that turn can't reach Claude (a laptop just woken, or a login that expired), Claude Code retries for about four minutes and gives up. Nothing re-arms the watch, so every later request is boosted by the connector and never reaches the session.
- A request delivered during that window is dropped the same way.
The fix
All of it is in skills/basecamp-connect/SKILL.md. The connector's code doesn't change.
- Monitor. The session arms the monitor for the full 30 minutes and re-arms it each time it expires.
- Handled file. Each request the session takes is recorded in a file beside the connector's output. The monitor replays every request line not yet recorded. A request that arrived while no monitor ran is still delivered, and none is handed off twice.
- Heartbeat. A recurring
CronCreatejob, every 10 minutes, picks up missed requests and re-arms a dead monitor. Cron jobs keep firing through failed turns, so the first heartbeat after Claude is reachable again restores the watch. It renews itself before Claude Code's 7-day limit on recurring jobs. - awk, not
grep -v. The filter is awk because Claude Code's owngrepholds back-voutput on a live pipe. A monitor built ongrep -vnever fires.
New tests in internal/connector/skill_watch_test.go run the skill's filter line itself. One checks it skips handled requests by exact id (101 handled, 1011 still delivered). The other checks it streams each line while its input stays open. Removing fflush() fails the second test, and loosening the id match fails the first.
How it was verified
I drove real Claude Code sessions (2.1.309) in tmux. Each sat behind a local proxy to the Anthropic API that I could switch to closed connections or to a 401 "OAuth token has expired" reply. A stub basecamp on PATH played the connector, printing request lines in the real format and recording every call. One session followed main's skill and one followed this branch's, under the same script:
- Request 101 with the API up.
- A 401 outage lasting past the monitor's expiry and past Claude Code's retries ("Please run /login"), with request 102 sent during it.
- The API restored, then request 103.
| 101 | 102 (during outage) | 103 (after) | Watch afterwards | |
|---|---|---|---|---|
main | answered | never answered | never answered | no monitor |
| this branch | answered | answered | answered | re-armed |
On this branch each request got exactly one acknowledgement update and one reply. To fit the run into minutes, the sessions used a 2-minute monitor and a 1-minute heartbeat in place of 30 and 10.
The filter was tested with GNU awk, GNU awk --posix, and the BWK awk that macOS ships (built from source).
Not verified:
- A real
basecamp connectagainst Basecamp. The connector's code and output are unchanged. - A real Mac or real sleep.
- The 7-day heartbeat renewal.
Cost
The heartbeat is about 144 small turns a day while the connector runs. Each turn is one awk command, and most end with nothing to do.
Summary by cubic
Stops the connector's watch from dying across laptop sleep and an expired Claude login, so requests boosted with 👀 are answered instead of going silent.
- The session records every finished request in a
.handledfile, replays unhandled ones through an awk filter that streams live output, and re-arms the monitor on expiry. - A recurring CronCreate heartbeat starts with
basecamp connect status, deletes itself when the connector stopped, and otherwise re-arms a dead monitor and delivers requests missed while Claude was unreachable; it renews itself before the 7-day recurring-job limit and costs about 144 small turns a day. - A restart copies the old
.handledfile so a dropped request isn't handled twice, and the monitor and heartbeat are found and cancelled via task, CronList, and cron prefix. - New tests run the skill's filter itself, covering exact-id dedupe and live streaming of each line.