Repository navigation
Conversation
…ogin A Claude Code monitor is killed after 30 minutes at most, and only a turn of the session arms it again. The skill asked for `persistent: true`, which the Monitor tool doesn't have. When that turn can't reach Claude, on a laptop just woken or with a login that expired, nothing re-arms the watch. The connector keeps boosting each request, and the agent goes silent after the 👀 for as long as the session runs. The skill now arms the monitor for its full 30 minutes and re-arms it on expiry. It records each request it takes in a handled file beside the connector's output, and the monitor replays everything not yet handled, so a request that arrived while no monitor ran is still delivered, and none is handed off twice. A recurring CronCreate heartbeat re-arms a dead watch and picks up missed requests. Cron jobs keep firing through failed turns, so the first heartbeat after Claude is reachable again restores the watch. The filter is awk, not `grep -v`. Claude Code's own grep holds back `-v` output on a live pipe, so a monitor built on it never fires. The new tests run the skill's filter line itself: it skips handled requests by exact id, and it streams each line while its input stays open.
There was a problem hiding this comment.
🟡 Changes recommended
Handled-request tracking omits local refusals and resets on restart, allowing repeated replies and duplicate work.
2 open issues
What changed in this PR
This PR updates the connector skill to recover its Claude Code watch after sleep or an expired login, without changing connector runtime code.
Changes:
- Adds monitor renewal, a recurring recovery heartbeat, and handled-request tracking.
- Updates restart and shutdown instructions to manage the watch lifecycle.
- Adds tests for exact-ID filtering and live output streaming.
[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or rungh pr ready --undo.
Click "Ready for review" or rungh pr readyto reengage.
| File | Description |
|---|---|
skills/basecamp-connect/SKILL.md |
Defines watch recovery, request tracking, and lifecycle cleanup. |
internal/connector/skill_watch_test.go |
Tests the skill's filter for exact-ID matching and streaming. |
Review effort: Balanced
|
🤖 Reviewed at 6cf77db. CI is green. The diagnosis holds up: the Monitor tool has no 1. The heartbeat can outlive the connector, and once renewed it never stops. The skill says to "keep its job id" (L330), and Stopping and When the connector stops delete the heartbeat by that id (L710, L740). But from day six the heartbeat schedules a new copy of itself and deletes the old one (L335-L342), and nothing says to keep the new id. After a renewal, or after a compaction that loses the id, CronDelete targets a job that's already gone. The live heartbeat keeps going, and its prompt doesn't check whether the connector is still running. Every 30 minutes it arms a new Suggested changes:
2. Only (b) and (c) write the handled file (L309-L310), so any other way a request ends gets replayed every 10 minutes and on every re-arm. Copilot's thread covers the owner-only refusal. The same gap applies to an owner's control request this session carries out itself instead of dispatching it (a setup change that waits on the person, say), and to any line the session decides to leave alone. Each replay runs step (a) again and repeats whatever reply went out. Changing the rule to "add the Not raised: two monitors after a compaction, or a heartbeat landing next to a monitor notification, can each deliver the same line. The session's turns run one at a time and it checks the handled file first, so that's a duplicate notification, not a duplicate dispatch. |
Review found three gaps in the watch. Only a dispatch or a holding reply wrote the handled file, so a request the session finished any other way, such as an owner-only refusal, was replayed by every heartbeat and re-armed monitor, and its reply posted again. The rule is now: once done with a request, however it ended, record it. The heartbeat was deleted by a remembered job id, which is gone after it renews itself or after a compaction, and it never checked whether the connector still ran. It now starts with `basecamp connect status`: when `pings.running` is absent it arms nothing and deletes itself. Stopping and restarting find it with CronList by its prompt's prefix. A restart began with an empty handled file, although a connector restarted right after a crash can print a request again. The new handled file now starts as a copy of the old one.
|
Thanks, Jeremy. Both points are fixed in 0a2ef66. 1. The heartbeat outliving the connector. The heartbeat's prompt now starts with
2. Requests that end without (b) or (c). I took your wording: "once you're done with a request, however it ended, add its Copilot's restart point is fixed too: the new handled file starts as a copy of the old one. CI is green on 0a2ef66, and both Copilot threads are resolved. Copilot last reviewed 6cf77db. I couldn't re-request it on the new head with my token, so it hasn't reviewed these fixes. |
| recurring, cron `'7-59/10 * * * *'`. Its prompt names every path, the profile, | ||
| today's date and the filter in full, since it must work after a compaction: | ||
|
|
||
| > basecamp-connect heartbeat for '<profile>'. Run `basecamp connect status | ||
| > -P '<profile>' --json`. If its `pings.running` is absent, the connector has | ||
| > stopped: arm nothing, CronDelete this heartbeat, and follow *When the | ||
| > connector stops* in the basecamp-connect skill. Otherwise run `<the filter> | ||
| > '<connector-output-file>'` and handle each line it prints as a request. |
| > <today's date>: from six days on, schedule this prompt again with the new | ||
| > date, and CronDelete the older heartbeat. If there is nothing to do, say |
|
🤖 Local review at 0a2ef66. CI is green on it (27 passed, 4 skipped, 1 neutral). Your two fixes.
Tests. 0a2ef66 changes only SKILL.md, so reverting either fix fails no test. The two tests in The other open Copilot thread (L342, heartbeat expiry): not addressed, and it holds. Renewal starts "from six days on", so the heartbeat has to get one turn through in its last day. An outage that covers day six to day seven, like a login that expires on day five and comes back on day eight, leaves no heartbeat and no monitor, and nothing reports it. Renewing from one day on instead of six means an outage would have to last six days to do that, at the cost of a renewal a day. Nothing else turned up. The local review ran |


What people saw
On a laptop with default sleep settings, an agent run through
basecamp connectacknowledges 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.
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.The fix
All of it is in
skills/basecamp-connect/SKILL.md. The connector's code doesn't change.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.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.gorun 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. Removingfflush()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
basecampon PATH played the connector, printing request lines in the real format and recording every call. One session followedmain's skill and one followed this branch's, under the same script:mainOn 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:
basecamp connectagainst Basecamp. The connector's code and output are unchanged.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.
.handledfile, replays unhandled ones through an awk filter that streams live output, and re-arms the monitor on expiry.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..handledfile so a dropped request isn't handled twice, and the monitor and heartbeat are found and cancelled via task, CronList, and cron prefix.Written for commit 0a2ef66. Summary will update on new commits.