From 1b8fdd2fc2fba614d0aa04642e492537cc010cc5 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Tue, 13 Oct 2026 18:05:58 -0400 Subject: [PATCH 1/4] Watch for connector requests with a one-shot background watcher, not 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. --- skills/basecamp-connect/SKILL.md | 38 ++++++++++++----------- skills/basecamp-connect/watch-requests.sh | 33 ++++++++++++++++++++ 2 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 skills/basecamp-connect/watch-requests.sh diff --git a/skills/basecamp-connect/SKILL.md b/skills/basecamp-connect/SKILL.md index 6206e9f01..521638493 100644 --- a/skills/basecamp-connect/SKILL.md +++ b/skills/basecamp-connect/SKILL.md @@ -75,25 +75,21 @@ see `connector: running` in its log. If it exited instead, explain the error (see *When the connector stops*), and stop. If it says `could not get the agent's token yet`, Basecamp's token endpoint is rate-limiting it or having trouble: it waits as long as it says and tries again on its own. Tell the -person, leave it running, and arm the monitor as usual. Restarting it doesn't +person, leave it running, and start the watcher as usual. Restarting it doesn't help. -Then arm a persistent monitor on that file with the Monitor tool -(`persistent: true`), replaying from the start so nothing that arrived between -the start and the monitor is missed: +Then start the watcher, also with `run_in_background: true`: ```bash -tail -f -n +1 '' | grep --line-buffered -F '"type":"request"' +bash '${CLAUDE_SKILL_DIR}/watch-requests.sh' '' ~/.config/basecamp-connect/'handled-' ``` -Single-quote the path, as with every other value. - -Each notification is one request: handle it with *For each request*. A request -that arrives while you're waiting on the person is not their reply. - -The connector starts from the present. Anything asked more than a minute -before it started is not picked up. If someone says the agent ignored them, that's the first -thing to check. +It exits, notifying you, with the request lines whose `event_id` the handled +file lacks. Record each (`echo >> ~/.config/basecamp-connect/'handled-'`), +start the watcher again, then handle each line with *For each request*. Keep +one watcher running. Not the Monitor tool: its watches expire, and a `grep -v` +in one holds a line back until the next. A request that arrives while you're +waiting on the person is not their reply. ### 2. The request line @@ -549,12 +545,17 @@ output and tell the person what it means: Requests that arrive while it's stopped are not picked up later. +Each start writes a new output file. Stop the watcher (TaskStop), and start it +again on the new file once the connector is running. + ## Checking on it - `basecamp connect doctor -P ''`: the credential, the identity, the event feed and the ledger. Start here when something seems wrong. It posts nothing. -- **"Did that mention reach it?"**: search the connector's output file for the +- **"Did that mention reach it?"**: the connector starts from the present, so + anything asked more than a minute before it started is not picked up. Check + that first. Then search the connector's output file for the recording id. A `"type":"request"` line means it was handed to you. A `"type":"event"` line with a `reason` says why it was turned away (for example `untrusted_performer`). No line means the feed never delivered it. @@ -564,7 +565,8 @@ Requests that arrive while it's stopped are not picked up later. ## Stopping -When the person asks you to stop, or the session is ending, stop the connector -task (TaskStop, or send it SIGINT). There's nothing else to clean up: no -webhooks, nothing public. Subagents already running finish their requests and -reply. Say that new mentions won't be picked up until it's started again. +When the person asks you to stop, or the session is ending, stop the watcher +and the connector task (TaskStop, or send it SIGINT). There's nothing else to +clean up: no webhooks, nothing public. Subagents already running finish their +requests and reply. Say that new mentions won't be picked up until it's +started again. diff --git a/skills/basecamp-connect/watch-requests.sh b/skills/basecamp-connect/watch-requests.sh new file mode 100644 index 000000000..90c62e869 --- /dev/null +++ b/skills/basecamp-connect/watch-requests.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -u + +if [ $# -ne 2 ]; then + echo "usage: watch-requests.sh " >&2 + exit 2 +fi + +out=$1 +handled=$2 + +if [ ! -f "$out" ]; then + echo "watch-requests: no connector output at $out" >&2 + exit 2 +fi +mkdir -p "$(dirname "$handled")" + +unhandled() { + local lines + lines=$(($(wc -l < "$out"))) + [ "$lines" -gt 0 ] || return 1 + head -n "$lines" "$out" | awk -v handled="$handled" ' + BEGIN { while ((getline id < handled) > 0) seen[id] = 1 } + /"type":"request"/ && match($0, /"event_id":[0-9]+/) { + id = substr($0, RSTART + 11, RLENGTH - 11) + if (!(id in seen)) { print; seen[id] = 1; found = 1 } + } + END { exit !found }' +} + +until unhandled; do + sleep 2 +done From 60162162d6f9f557758911e2843665d5c6c1b0db Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Tue, 13 Oct 2026 18:24:38 -0400 Subject: [PATCH 2/4] Drain a stopped connector's file, read lines from the watcher's output, 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. --- internal/commands/agent_hook_connector.go | 2 +- skills/basecamp-connect/SKILL.md | 24 +++++++++++++++-------- skills/basecamp-connect/watch-requests.sh | 21 ++++++++++++++++---- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/internal/commands/agent_hook_connector.go b/internal/commands/agent_hook_connector.go index 841a0213d..03ccb59b9 100644 --- a/internal/commands/agent_hook_connector.go +++ b/internal/commands/agent_hook_connector.go @@ -58,7 +58,7 @@ func connectorReloadNotice(input agentHookInput) string { "finished cut that skill to its first 5,000 tokens, which drops the handoff steps and the subagent's " + "instructions. If this session is still the connector's orchestrator, read " + skill + " in full with " + "the Read tool now, before you go on with anything, a request in hand included, then carry on from the " + - "step you were on. The connector, its monitor and any subagents in flight kept running through the " + + "step you were on. The connector, its request watcher and any subagents in flight kept running through the " + "compaction: don't start them again." } diff --git a/skills/basecamp-connect/SKILL.md b/skills/basecamp-connect/SKILL.md index 521638493..779603870 100644 --- a/skills/basecamp-connect/SKILL.md +++ b/skills/basecamp-connect/SKILL.md @@ -84,12 +84,13 @@ Then start the watcher, also with `run_in_background: true`: bash '${CLAUDE_SKILL_DIR}/watch-requests.sh' '' ~/.config/basecamp-connect/'handled-' ``` -It exits, notifying you, with the request lines whose `event_id` the handled -file lacks. Record each (`echo >> ~/.config/basecamp-connect/'handled-'`), -start the watcher again, then handle each line with *For each request*. Keep -one watcher running. Not the Monitor tool: its watches expire, and a `grep -v` -in one holds a line back until the next. A request that arrives while you're -waiting on the person is not their reply. +It exits once request lines arrive whose `event_id` the handled file lacks. +Read them in full from its output file, record each +(`echo >> ~/.config/basecamp-connect/'handled-'`), start +the watcher again, then handle each with *For each request*. Keep one running. +Not the Monitor tool: its watches expire, and a `grep -v` in one holds a line +back until the next. A request that arrives while you're waiting on the person +is not their reply. ### 2. The request line @@ -545,8 +546,15 @@ output and tell the person what it means: Requests that arrive while it's stopped are not picked up later. -Each start writes a new output file. Stop the watcher (TaskStop), and start it -again on the new file once the connector is running. +Each start writes a new output file. Stop the watcher (TaskStop), then check +the old file once, so a request printed just before the exit isn't left there: + +```bash +bash '${CLAUDE_SKILL_DIR}/watch-requests.sh' --once '' ~/.config/basecamp-connect/'handled-' +``` + +Handle any line it prints as usual. Start the watcher on the new file once the +connector is running. ## Checking on it diff --git a/skills/basecamp-connect/watch-requests.sh b/skills/basecamp-connect/watch-requests.sh index 90c62e869..ba6a3c6b1 100644 --- a/skills/basecamp-connect/watch-requests.sh +++ b/skills/basecamp-connect/watch-requests.sh @@ -1,8 +1,13 @@ #!/usr/bin/env bash set -u +once=false +if [ "${1:-}" = --once ]; then + once=true + shift +fi if [ $# -ne 2 ]; then - echo "usage: watch-requests.sh " >&2 + echo "usage: watch-requests.sh [--once] " >&2 exit 2 fi @@ -13,14 +18,17 @@ if [ ! -f "$out" ]; then echo "watch-requests: no connector output at $out" >&2 exit 2 fi -mkdir -p "$(dirname "$handled")" +if ! { mkdir -p "$(dirname "$handled")" && touch "$handled" && [ -r "$handled" ]; }; then + echo "watch-requests: can't use the handled file $handled" >&2 + exit 2 +fi unhandled() { local lines lines=$(($(wc -l < "$out"))) [ "$lines" -gt 0 ] || return 1 - head -n "$lines" "$out" | awk -v handled="$handled" ' - BEGIN { while ((getline id < handled) > 0) seen[id] = 1 } + head -n "$lines" "$out" | HANDLED=$handled awk ' + BEGIN { while ((getline id < ENVIRON["HANDLED"]) > 0) seen[id] = 1 } /"type":"request"/ && match($0, /"event_id":[0-9]+/) { id = substr($0, RSTART + 11, RLENGTH - 11) if (!(id in seen)) { print; seen[id] = 1; found = 1 } @@ -28,6 +36,11 @@ unhandled() { END { exit !found }' } +if [ "$once" = true ]; then + unhandled + exit 0 +fi + until unhandled; do sleep 2 done From cd6ca164fe0e071308e17855d53fc6189570c9b6 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Tue, 13 Oct 2026 18:33:27 -0400 Subject: [PATCH 3/4] Require a writable handled file, and have a compacted session check the 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. --- internal/commands/agent_hook_connector.go | 5 +++-- skills/basecamp-connect/watch-requests.sh | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/commands/agent_hook_connector.go b/internal/commands/agent_hook_connector.go index 03ccb59b9..4e5d2b0da 100644 --- a/internal/commands/agent_hook_connector.go +++ b/internal/commands/agent_hook_connector.go @@ -58,8 +58,9 @@ func connectorReloadNotice(input agentHookInput) string { "finished cut that skill to its first 5,000 tokens, which drops the handoff steps and the subagent's " + "instructions. If this session is still the connector's orchestrator, read " + skill + " in full with " + "the Read tool now, before you go on with anything, a request in hand included, then carry on from the " + - "step you were on. The connector, its request watcher and any subagents in flight kept running through the " + - "compaction: don't start them again." + "step you were on. The connector and any subagents in flight kept running through the compaction: don't " + + "start them again. The request watcher exits on each request, so check whether it is still running; if it " + + "has exited, read its output and start it again as the skill says." } // lastConnectorSkillDir returns the directory of the last basecamp-connect diff --git a/skills/basecamp-connect/watch-requests.sh b/skills/basecamp-connect/watch-requests.sh index ba6a3c6b1..1480158c3 100644 --- a/skills/basecamp-connect/watch-requests.sh +++ b/skills/basecamp-connect/watch-requests.sh @@ -18,7 +18,7 @@ if [ ! -f "$out" ]; then echo "watch-requests: no connector output at $out" >&2 exit 2 fi -if ! { mkdir -p "$(dirname "$handled")" && touch "$handled" && [ -r "$handled" ]; }; then +if ! { mkdir -p "$(dirname "$handled")" && touch "$handled" && [ -f "$handled" ] && [ -r "$handled" ] && [ -w "$handled" ]; }; then echo "watch-requests: can't use the handled file $handled" >&2 exit 2 fi From c5bf320b73de103d069dabb08dae3b5dc7c66835 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Tue, 13 Oct 2026 18:46:15 -0400 Subject: [PATCH 4/4] Stop the watcher when the connector output it watches can't 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. --- skills/basecamp-connect/watch-requests.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/skills/basecamp-connect/watch-requests.sh b/skills/basecamp-connect/watch-requests.sh index 1480158c3..8656e77e0 100644 --- a/skills/basecamp-connect/watch-requests.sh +++ b/skills/basecamp-connect/watch-requests.sh @@ -25,6 +25,10 @@ fi unhandled() { local lines + if [ ! -r "$out" ]; then + echo "watch-requests: can't read the connector output at $out" >&2 + exit 2 + fi lines=$(($(wc -l < "$out"))) [ "$lines" -gt 0 ] || return 1 head -n "$lines" "$out" | HANDLED=$handled awk '