You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Watch for connector requests with a one-shot background watcher, not Monitor - #902
basecamp-connect watched the connector with the Monitor tool. Now it starts skills/basecamp-connect/watch-requests.sh as a one-shot background command instead. The script exits with the request lines whose event_id isn't in a handled file. The session records those ids, starts the watcher again, then handles the lines.
It can't be persistent. Claude Code 2.1.271 removed the option the skill still asks for: "Changed Monitor watches to always have a deadline (at most 30 minutes; 10 in single-prompt -p runs) and notify Claude to re-arm, replacing the no-timeout persistent option." A session has to re-arm every 30 minutes. If it re-arms without replay, it misses whatever arrived in the gap.
Re-arming with a skip-list holds requests back. The obvious re-arm replays with tail -n +1 and drops handled ids with grep -v. Since 2.1.117, native macOS and Linux builds run grep in the Bash tool as Claude Code's embedded ugrep ("the Glob and Grep tools are replaced by embedded bfs and ugrep available through the Bash tool"). Monitor runs in the same shell. ugrep -v holds each line on a stream until the next one arrives, with --line-buffered or without it:
After tail -f -n +1 <file>
One request line written: shown?
grep --line-buffered -F '"type":"request"' (the skill as written)
yes, at once
that, then grep --line-buffered -vE '"event_id":(…),'
no, only when the next line arrives
grep --line-buffered -vF … alone
no, only when the next line arrives
the same two stages with GNU grep (command grep)
yes, at once
that, then awk '!/…/ {print; fflush()}'
yes, at once
One session on this machine re-armed that way on 2026-10-07, and its transcript shows the effect:
Before the re-arm: 7 of 7 requests were notified within 1 second of creation.
After it: 8 of 8 were notified only when the next request arrived, 0.4 to 1.3 seconds after that one was created. The longest wait was 3.5 hours.
The next request was still held 80 seconds later, when the session stopped the Monitor and moved to a polling watcher.
Change
The skill's watch step. The session starts the watcher in the background, reads the lines from the watcher's output file, records each event_id, starts the watcher again, then handles the lines. A watcher's exit is its one notification, so no stream filter sits between a line and the session. Each run reads the output from the start, so nothing is lost between runs.
watch-requests.sh:
reads only complete lines: it counts them with wc -l, then reads that many with head -n
prints each unhandled id once, even when a line was printed twice
reads the handled path from ENVIRON, because awk -v decodes backslashes
exits 2 if it can't create or read the handled file
has a --once mode for draining a stopped connector's file
runs on Bash 3.2 and BSD wc/head/awk
The handled file is ~/.config/basecamp-connect/handled-<profile>, next to last.json. It outlives a connector run, so a line the connector prints again after a crash is still skipped.
Other sections of the skill:
When the connector stops: drain the old file with --once, then start the watcher on the new file.
Stopping: stop the watcher too.
The note that the connector "starts from the present" moves into Checking on it. That keeps the per-request steps inside the layout test's 14,000-character budget; they now end at 13,990.
Outside the skill: the post-compaction notice in internal/commands/agent_hook_connector.go said the session's "monitor" kept running. It now says "request watcher". That's a one-word change in a Go string, so a compacted session isn't told about a watch it doesn't have.
Verification
bin/ci is green locally. The e2e step needed MISE_BATS_VERSION=1.13.0, because mise has no bats pin on this machine.
The script passes a 13-case scenario suite (13 of 13) with GNU awk and with gawk --posix. The cases cover:
usage
a missing output file
an empty file
a fresh handled file
all ids handled
--once, with and without unhandled lines
a partial last line, then its completion
a duplicated id
an escaped event_id inside content
a backslash in the handled path
an unreadable handled file
Three mutations each fail their own case: dropping the complete-line guard, dropping the dedupe within a run, and going back to awk -v.
A read-only run against a live connector's output (7 request lines, all handled) kept waiting, as it should.
Not run on macOS. Portability there was checked by reading the code and with gawk --posix.
Trade-offs
Polling cost. The watcher polls every 2 seconds and reads the whole output file each time. A connector here writes about 80 KB an hour, roughly 2 MB a day, which is negligible but grows with uptime. A tail -f watcher that blocks would avoid the re-reads, but it needs a way to end tail after the match. The Monitor tool's own guidance warns that tail -f | grep -m 1 hangs.
An extra step per request, recording the id and restarting the watcher. A session that forgets to restart it watches nothing. The same was true of a Monitor that expired, which happened every 30 minutes.
Adversarial review
Pi (openai-codex/gpt-6.1-sol, xhigh) read 32d2a31 and made six findings.
Add benchmark harness and baseline results #1, "record ids only after dispatch". To start the watcher again before handling, the ids have to be recorded first. Otherwise the new watcher reports the same lines again at once. Handling before restarting would leave nothing watching while the session waits on a person, for example to pick a repo. The case that loses a request is the session ending mid-handoff, and that stops the connector along with it.
The rest of Restructure for public plugin distribution #3, "an independent liveness check". That's the 5-minute poll the desktop report wants to drop. The watcher exits on every request, so restarting it is one of each request's steps, and the compaction notice now names it.
The fixes since 32d2a31 haven't had a second Pi pass. Copilot reviews on open.
Not in this PR
Moving the acknowledgement into the pipeline that starts the connector. A wrapper could boost the moment a request line appears, so the acknowledgement wouldn't wait for the session to be free. The skill asks for an acknowledgement in the session's own words, never a fixed string, so that's a product call, not this fix.
Summary by cubic
Replaces the Monitor tool with a one-shot background watcher for connector requests, so no request is missed when the watch expires or re-arms with a filter that holds lines back.
Change
skills/basecamp-connect/watch-requests.sh polls the connector output, prints request lines whose event_id isn't in a handled file, then exits; the session records the ids, restarts the watcher, and handles the lines.
The watcher reads only complete lines, dedupes ids within a run, requires a writable handled file (exiting 2 otherwise), exits 2 if the connector output becomes unreadable mid-run, and supports a --once mode for draining a stopped connector's file.
SKILL.md starts and stops the watcher as a background task, drains a stopped connector's file with --once, and moves the "connector starts from the present" note into Checking on it.
The post-compaction notice in agent_hook_connector.go now says the session's "request watcher" exits on each request, so a compacted session checks it and starts it again if needed.
Written for commit e1d4116. Summary will update on new commits.
…Monitor
The skill armed the Monitor tool with `persistent: true`. Claude Code 2.1.271
removed that option: every watch now expires within 30 minutes, so a session
has to re-arm it. Re-arming and skipping handled ids with `grep -v` loses
requests in a different way. In Claude Code's Bash, `grep` is the embedded
ugrep, and `ugrep -v` holds each line on a stream until the next one arrives,
`--line-buffered` or not. The newest request then waits for the next one.
watch-requests.sh reads the connector's output from the start, prints the
request lines whose event_id the handled file lacks, and exits. The session
records each id, starts the watcher again, and handles the lines. The notification
is the exit itself, so no stream filter sits between a line and the session,
and reading from the start leaves no gap between watchers. The watcher reads only
complete lines, and the handled file lives outside the run, so a line
reprinted after a crash is still skipped.
…t, and fail closed on the handled file
A request printed just before the connector exits could sit in the old
output file after the watcher moved to the new one. watch-requests.sh --once
checks the old file a last time, without needing timeout(1), which macOS
lacks.
The watcher's completion notice names its output file rather than carrying
the lines, so the skill says to read them from there.
awk -v decodes backslashes, so the handled path reaches awk through ENVIRON.
A handled file the watcher can't create or read now exits 2 instead of
reporting every request as new.
The post-compaction notice said the session's monitor kept running. It now
names the request watcher.
Replaces expiring Monitor-based connector request handling with a one-shot polling watcher.
Changes:
Adds request filtering, deduplication, and drain support.
Updates connector skill lifecycle instructions.
Renames the post-compaction monitor reference.
[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
The reason will be displayed to describe this comment to others. Learn more.
🤖 Agreed that it would be worth having, but it is outside the scope I was given for this PR, which is the skill text and the script it ships. The 13 scenarios (now 15, with the directory and read-only cases) ran as an ad hoc local suite, and three mutations each failed their own case. Whether to commit them as a BATS file under e2e/ is a call for a maintainer. I will add one here if asked. Leaving this open for that decision.
…he watcher
A directory or a read-only file passed touch and -r, so the watcher ran
against a handled file the session could never record into, and every
restart replayed the same requests. It now requires a regular file it can
read and write.
The post-compaction notice said everything kept running. The watcher exits
on each request, so it may have exited during the compaction. The notice now
says to check it, and to read its output and start it again if it has
exited.
Watcher loops forever when task file becomes unreadable
skills/basecamp-connect/watch-requests.sh:28
The connector output is checked only before polling starts. If that background-task file is later removed or becomes unreadable, this redirection fails and the arithmetic expansion yields zero, so the watcher stays alive and sleeps forever instead of reporting failure. That also defeats the compaction notice's liveness check because the task still appears running while it can no longer observe requests. Revalidate the file on each poll and exit 2 when it cannot be read.
The output file was checked once, before polling. If it went away later,
wc failed, the line count read as zero, and the watcher slept forever while
looking alive. Each poll now checks the file and exits 2 if it can't be read.
Shell-quote expanded watcher paths to handle apostrophes
skills/basecamp-connect/SKILL.md:84
The expanded skill directory and connector output path are inserted between literal single quotes. If either valid path contains an apostrophe, this command is syntactically invalid and the watcher never starts (for example, Bash reports an unmatched quote for a skill path such as skill'copy). Instruct the session to shell-quote each expanded path, including escaping embedded apostrophes, rather than wrapping raw placeholders in fixed quotes.
This issue also appears on line 553 of the same file.
An output file that vanishes mid-poll left the watcher sleeping forever. This was in the review body on b42a8df, and e1d4116 fixes it: each poll checks the file and exits 2 if it can't be read. Checked with a new local case, which the previous script fails.
Declined on merit: an apostrophe in the expanded skill or output path breaking the single-quoted commands (review body on e1d4116, SKILL.md:84 and :553).
The skill's quoting rule already says to write a quote inside a value as '\''.
The pattern predates this PR: tail -f -n +1 '<connector-output-file>' and `${CLAUDE_SKILL_DIR}/subagent.md`.
Both paths come from the harness, which sanitizes the session directory name.
A fix here would also belong in every other command in the skill, so it's a separate change if anyone wants it.
bin/ci is green locally on e1d4116. It needs a human merge.
This branch has not been deployed
No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
basecamp-connectwatched the connector with the Monitor tool. Now it startsskills/basecamp-connect/watch-requests.shas a one-shot background command instead. The script exits with the request lines whoseevent_idisn't in a handled file. The session records those ids, starts the watcher again, then handles the lines.Reported from the Claude desktop app on Agent connector: requests sometimes don't reach the Claude session. The Linux CLI behaves the same way.
Why
The Monitor watch loses requests in two ways.
It can't be persistent. Claude Code 2.1.271 removed the option the skill still asks for: "Changed Monitor watches to always have a deadline (at most 30 minutes; 10 in single-prompt
-pruns) and notify Claude to re-arm, replacing the no-timeoutpersistentoption." A session has to re-arm every 30 minutes. If it re-arms without replay, it misses whatever arrived in the gap.Re-arming with a skip-list holds requests back. The obvious re-arm replays with
tail -n +1and drops handled ids withgrep -v. Since 2.1.117, native macOS and Linux builds rungrepin the Bash tool as Claude Code's embedded ugrep ("theGlobandGreptools are replaced by embeddedbfsandugrepavailable through the Bash tool"). Monitor runs in the same shell.ugrep -vholds each line on a stream until the next one arrives, with--line-bufferedor without it:tail -f -n +1 <file>grep --line-buffered -F '"type":"request"'(the skill as written)grep --line-buffered -vE '"event_id":(…),'grep --line-buffered -vF …alonecommand grep)awk '!/…/ {print; fflush()}'One session on this machine re-armed that way on 2026-10-07, and its transcript shows the effect:
Change
The skill's watch step. The session starts the watcher in the background, reads the lines from the watcher's output file, records each
event_id, starts the watcher again, then handles the lines. A watcher's exit is its one notification, so no stream filter sits between a line and the session. Each run reads the output from the start, so nothing is lost between runs.watch-requests.sh:wc -l, then reads that many withhead -nENVIRON, becauseawk -vdecodes backslashes--oncemode for draining a stopped connector's filewc/head/awkThe handled file is
~/.config/basecamp-connect/handled-<profile>, next tolast.json. It outlives a connector run, so a line the connector prints again after a crash is still skipped.Other sections of the skill:
--once, then start the watcher on the new file.Outside the skill: the post-compaction notice in
internal/commands/agent_hook_connector.gosaid the session's "monitor" kept running. It now says "request watcher". That's a one-word change in a Go string, so a compacted session isn't told about a watch it doesn't have.Verification
bin/ciis green locally. The e2e step neededMISE_BATS_VERSION=1.13.0, because mise has no bats pin on this machine.gawk --posix. The cases cover:--once, with and without unhandled linesevent_idinsidecontentawk -v.gawk --posix.Trade-offs
tail -fwatcher that blocks would avoid the re-reads, but it needs a way to endtailafter the match. The Monitor tool's own guidance warns thattail -f | grep -m 1hangs.Adversarial review
Pi (
openai-codex/gpt-6.1-sol, xhigh) read 32d2a31 and made six findings.Accepted:
--oncedrain.awk -vdecodes the path. Fixed withENVIRON.Declined:
The fixes since 32d2a31 haven't had a second Pi pass. Copilot reviews on open.
Not in this PR
Moving the acknowledgement into the pipeline that starts the connector. A wrapper could boost the moment a request line appears, so the acknowledgement wouldn't wait for the session to be free. The skill asks for an acknowledgement in the session's own words, never a fixed string, so that's a product call, not this fix.
Summary by cubic
Replaces the Monitor tool with a one-shot background watcher for connector requests, so no request is missed when the watch expires or re-arms with a filter that holds lines back.
Change
skills/basecamp-connect/watch-requests.shpolls the connector output, prints request lines whoseevent_idisn't in a handled file, then exits; the session records the ids, restarts the watcher, and handles the lines.--oncemode for draining a stopped connector's file.SKILL.mdstarts and stops the watcher as a background task, drains a stopped connector's file with--once, and moves the "connector starts from the present" note into Checking on it.agent_hook_connector.gonow says the session's "request watcher" exits on each request, so a compacted session checks it and starts it again if needed.Written for commit e1d4116. Summary will update on new commits.