Skip to content

Watch for connector requests with a one-shot background watcher, not Monitor - #902

Open
robzolkos wants to merge 4 commits into
mainfrom
connect-one-shot-watcher
Open

robzolkos wants to merge 4 commits into
mainfrom
connect-one-shot-watcher

Conversation

@robzolkos

@robzolkos robzolkos commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

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.

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 -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.

Accepted:

Declined:

  • 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.

View guided diff Turn on auto-fix

…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.
@robzolkos
robzolkos requested a review from a team as a code owner October 13, 2026 22:24
Copilot AI balanced review requested due to automatic review settings October 13, 2026 22:24
@github-actions github-actions Bot added commands CLI command implementations skills Agent skills labels Oct 13, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Handled-file validation and post-compaction watcher guidance can cause replayed requests or leave sessions unwatched.

3 open findings
What changed in this PR

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.

File Description
skills/​basecamp-connect/​watch-requests.sh Implements the one-shot watcher.
skills/​basecamp-connect/​SKILL.md Documents watcher operation and shutdown.
internal/​commands/​agent_hook_connector.go Updates post-compaction guidance.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/commands/agent_hook_connector.go Outdated
Comment thread skills/basecamp-connect/watch-requests.sh Outdated
exit 2
fi

unhandled() {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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.
Copilot AI balanced review requested due to automatic review settings October 13, 2026 22:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The watcher can remain alive indefinitely after losing access to its connector output file.

1 open finding
2 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity 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.

🧠 Review effort: Balanced

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The documented watcher commands break when expanded paths contain apostrophes.

1 open finding
Previously missed (1)

In code that hasn't changed since last review

Medium severity 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.

🧠 Review effort: Balanced

@robzolkos

Copy link
Copy Markdown
Collaborator Author

🤖 Review status on e1d4116

Reviewers and the commit each read:

  • Pi (openai-codex/gpt-6.1-sol, xhigh) read 32d2a31. Its findings and what I did about each are in the PR description.
  • Copilot read 7667cbf, b42a8df and e1d4116. Its latest review is on the current head.
  • cubic skipped this PR.

Copilot findings fixed:

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.

Open for a maintainer: commit the watcher's scenario suite as a test. It's out of the scope I was given (the skill text and its script). I'll add a BATS file under e2e/ if asked.

bin/ci is green locally on e1d4116. It needs a human merge.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commands CLI command implementations skills Agent skills

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants