From 40bcbcabfec391237fb4442f6cebe1d05cf967b9 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Tue, 13 Oct 2026 18:05:58 -0400 Subject: [PATCH 1/8] 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 | 30 ++++++++++++--------- skills/basecamp-connect/watch-requests.sh | 33 +++++++++++++++++++++++ 2 files changed, 50 insertions(+), 13 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 544337918..4b91062f8 100644 --- a/skills/basecamp-connect/SKILL.md +++ b/skills/basecamp-connect/SKILL.md @@ -67,21 +67,21 @@ It runs until stopped, from any folder. After a few seconds its log should say `connector: running`. 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 limiting it or failing: it -retries on its own. Tell the person, leave it running, and arm the monitor; +retries on its own. Tell the person, leave it running, and start the watcher; restarting 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. +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 @@ -652,6 +652,9 @@ 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 @@ -675,7 +678,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 2ace6c16e2d7b788519f7c5ac6d8267af308ae57 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Tue, 13 Oct 2026 18:24:38 -0400 Subject: [PATCH 2/8] 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 4b91062f8..104276301 100644 --- a/skills/basecamp-connect/SKILL.md +++ b/skills/basecamp-connect/SKILL.md @@ -76,12 +76,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 @@ -652,8 +653,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 e70f31b5bea3aaee84ea3b5286da3a0e53f5d8a5 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Tue, 13 Oct 2026 18:33:27 -0400 Subject: [PATCH 3/8] 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 e0dc39df9ff1c635da88e58e8ea2306ed50f71bd Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Tue, 13 Oct 2026 18:46:15 -0400 Subject: [PATCH 4/8] 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 ' From ff0e32d71a2dc953b16c7dc85e45823f669359bc Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Fri, 23 Oct 2026 15:45:56 -0400 Subject: [PATCH 5/8] Keep request-watcher regressions in the existing BATS suite --- e2e/connect_watcher.bats | 130 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 e2e/connect_watcher.bats diff --git a/e2e/connect_watcher.bats b/e2e/connect_watcher.bats new file mode 100644 index 000000000..ec18c58a1 --- /dev/null +++ b/e2e/connect_watcher.bats @@ -0,0 +1,130 @@ +#!/usr/bin/env bats + +# Focused regressions for the shipped request-watching script, using the +# existing BATS suite. No Basecamp server or credentials are used. +setup() { + WATCHER="${WATCHER_UNDER_TEST:-$BATS_TEST_DIRNAME/../skills/basecamp-connect/watch-requests.sh}" + WORK="$(mktemp -d)" + OUT="$WORK/connector.log" + HANDLED="$WORK/handled" + : > "$OUT" +} + +teardown() { + if [ -n "${WATCH_PID:-}" ]; then + kill "$WATCH_PID" 2>/dev/null || true + wait "$WATCH_PID" 2>/dev/null || true + fi + chmod -R u+rwX "$WORK" + rm -rf "$WORK" +} + +@test "watcher reports usage without its required paths" { + run bash "$WATCHER" + [ "$status" -eq 2 ] + [[ "$output" == *usage:* ]] +} + +@test "watcher refuses a missing connector output" { + run bash "$WATCHER" --once "$WORK/missing" "$HANDLED" + [ "$status" -eq 2 ] + [[ "$output" == *"no connector output"* ]] +} + +@test "watcher can drain an empty output" { + run bash "$WATCHER" --once "$OUT" "$HANDLED" + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "watcher creates the handled directory and returns a fresh request" { + printf '%s\n' '{"type":"request","event_id":101}' > "$OUT" + run bash "$WATCHER" --once "$OUT" "$WORK/nested/handled" + [ "$status" -eq 0 ] + [ "$output" = '{"type":"request","event_id":101}' ] + [ -f "$WORK/nested/handled" ] +} + +@test "watcher skips ids already handled" { + printf '%s\n' '{"type":"request","event_id":101}' > "$OUT" + printf '%s\n' 101 > "$HANDLED" + run bash "$WATCHER" --once "$OUT" "$HANDLED" + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "watcher matches handled ids exactly rather than by prefix" { + printf '%s\n' '{"type":"request","event_id":101}' '{"type":"request","event_id":1011}' > "$OUT" + printf '%s\n' 101 > "$HANDLED" + run bash "$WATCHER" --once "$OUT" "$HANDLED" + [ "$status" -eq 0 ] + [ "$output" = '{"type":"request","event_id":1011}' ] +} + +@test "watcher emits a duplicated request once" { + printf '%s\n' '{"type":"request","event_id":101}' '{"type":"request","event_id":101}' > "$OUT" + run bash "$WATCHER" --once "$OUT" "$HANDLED" + [ "$status" -eq 0 ] + [ "$output" = '{"type":"request","event_id":101}' ] +} + +@test "watcher ignores event and pointer lines" { + printf '%s\n' '{"type":"event","event_id":101}' '{"event_id":102}' '{"type":"request","event_id":103}' > "$OUT" + run bash "$WATCHER" --once "$OUT" "$HANDLED" + [ "$status" -eq 0 ] + [ "$output" = '{"type":"request","event_id":103}' ] +} + +@test "watcher does not deliver a partial line until it is complete" { + printf '%s\n' '{"type":"event","event_id":100}' > "$OUT" + printf '%s' '{"type":"request","event_id":101}' >> "$OUT" + run bash "$WATCHER" --once "$OUT" "$HANDLED" + [ "$status" -eq 0 ] + [ -z "$output" ] + printf '\n' >> "$OUT" + run bash "$WATCHER" --once "$OUT" "$HANDLED" + [ "$status" -eq 0 ] + [ "$output" = '{"type":"request","event_id":101}' ] +} + +@test "watcher does not mistake an escaped id in content for the request id" { + printf '%s\n' '{"type":"request","content":"quoted \"event_id\":999","event_id":101}' > "$OUT" + printf '%s\n' 999 > "$HANDLED" + run bash "$WATCHER" --once "$OUT" "$HANDLED" + [ "$status" -eq 0 ] + [[ "$output" == *'"event_id":101}' ]] +} + +@test "watcher preserves backslashes in the handled path" { + printf '%s\n' '{"type":"request","event_id":101}' > "$OUT" + printf '%s\n' 101 > "$WORK/handled\name" + run bash "$WATCHER" --once "$OUT" "$WORK/handled\name" + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "watcher refuses an unusable handled path" { + mkdir "$HANDLED" + run bash "$WATCHER" --once "$OUT" "$HANDLED" + [ "$status" -eq 2 ] + [[ "$output" == *"can't use the handled file"* ]] +} + +@test "watcher wakes when a new complete request arrives" { + bash "$WATCHER" "$OUT" "$HANDLED" > "$WORK/result" 2> "$WORK/errors" & + WATCH_PID=$! + printf '%s\n' '{"type":"request","event_id":101}' >> "$OUT" + for _ in {1..100}; do + [ ! -s "$WORK/result" ] || break + sleep 0.05 + done + [ -s "$WORK/result" ] + for _ in {1..100}; do + kill -0 "$WATCH_PID" 2>/dev/null || break + sleep 0.05 + done + ! kill -0 "$WATCH_PID" 2>/dev/null + wait "$WATCH_PID" + WATCH_PID= + [ "$(< "$WORK/result")" = '{"type":"request","event_id":101}' ] +} From 2830aac6442c5ec46a6a81ea1bc5a7038e357528 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Fri, 23 Oct 2026 16:02:19 -0400 Subject: [PATCH 6/8] Keep watcher instructions inside the existing per-request skill budget --- skills/basecamp-connect/SKILL.md | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/skills/basecamp-connect/SKILL.md b/skills/basecamp-connect/SKILL.md index 104276301..136cfb3d0 100644 --- a/skills/basecamp-connect/SKILL.md +++ b/skills/basecamp-connect/SKILL.md @@ -63,12 +63,10 @@ note the output file the harness reports: basecamp connect -P '' ``` -It runs until stopped, from any folder. After a few seconds its log should say -`connector: running`. 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 limiting it or failing: it -retries on its own. Tell the person, leave it running, and start the watcher; -restarting doesn't help. +It runs from any folder until stopped. Wait a few seconds for +`connector: running`. If it exits, explain why (*When the connector stops*) +and stop. If it logs `could not get the agent's token yet`, tell the person +it will retry; leave it running and start the watcher. Restarting won't help. Then start the watcher, also with `run_in_background: true`: @@ -76,13 +74,11 @@ Then start the watcher, also with `run_in_background: true`: bash '${CLAUDE_SKILL_DIR}/watch-requests.sh' '' ~/.config/basecamp-connect/'handled-' ``` -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. +It exits with unhandled requests. Read every line from its output file, +record each id (`echo >> ~/.config/basecamp-connect/'handled-'`), +restart the watcher, then handle each with *For each request*. Keep one running. +Use this instead of Monitor: watches expire and `grep -v` can buffer live +lines. A request arriving while you wait on the person is not their reply. ### 2. The request line From d5b976f9d7e74b9eed4631864526ea3199f112d6 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Fri, 23 Oct 2026 16:06:56 -0400 Subject: [PATCH 7/8] Remove repeated skill introduction without dropping request instructions --- skills/basecamp-connect/SKILL.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/skills/basecamp-connect/SKILL.md b/skills/basecamp-connect/SKILL.md index 136cfb3d0..3aeecf124 100644 --- a/skills/basecamp-connect/SKILL.md +++ b/skills/basecamp-connect/SKILL.md @@ -38,14 +38,11 @@ Three pieces: - **The agent**: a Basecamp person of its own, owned by you, connected to this computer under a CLI profile (often named after the agent). -- **The connector**: `basecamp connect -P ''`. It listens to the - account's event feed as the agent, checks each event against who may give the - agent work and which projects it serves, and prints one line per trusted - request. It boosts each one that asks for something first, and does - nothing else. -- **This session**: the orchestrator. It watches those lines, words each - acknowledgement, chooses the repo, and dispatches one background subagent - per request. It never does the work itself. +- **The connector**: `basecamp connect -P ''`. It checks the agent's + event feed against its trust and served projects, boosts each request that + asks for something, and prints one line per trusted request. Nothing else. +- **This session**: watches those lines, words acknowledgements, chooses the + repo, and dispatches one background subagent per request. Never does the work. Claude Code only. The connector runs on Linux and macOS. From 20449f010b6bfb42949884b9580b877866c7a58d Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Fri, 23 Oct 2026 16:40:32 -0400 Subject: [PATCH 8/8] Fail closed if a watcher scan loses its handled file --- e2e/connect_watcher.bats | 19 ++++++++++++++++ skills/basecamp-connect/watch-requests.sh | 27 ++++++++++++++++------- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/e2e/connect_watcher.bats b/e2e/connect_watcher.bats index ec18c58a1..b49a1ba70 100644 --- a/e2e/connect_watcher.bats +++ b/e2e/connect_watcher.bats @@ -110,6 +110,25 @@ teardown() { [[ "$output" == *"can't use the handled file"* ]] } +@test "watcher fails closed if the handled file vanishes after startup" { + mkdir "$WORK/bin" + printf '%s\n' '#!/usr/bin/env bash' 'rm -f "$WATCHER_HANDLED"' 'exec "$REAL_WC" "$@"' > "$WORK/bin/wc" + chmod +x "$WORK/bin/wc" + local real_wc + real_wc="$(command -v wc)" + printf '%s\n' '{"type":"request","event_id":101}' > "$OUT" + for mode in once polling; do + printf '%s\n' 101 > "$HANDLED" + local args=() + [ "$mode" != once ] || args+=(--once) + run env PATH="$WORK/bin:$PATH" WATCHER_HANDLED="$HANDLED" REAL_WC="$real_wc" \ + bash "$WATCHER" "${args[@]}" "$OUT" "$HANDLED" + [ "$status" -eq 2 ] + [[ "$output" == *"cannot read the handled file"* ]] + [[ "$output" != *'"event_id":101'* ]] + done +} + @test "watcher wakes when a new complete request arrives" { bash "$WATCHER" "$OUT" "$HANDLED" > "$WORK/result" 2> "$WORK/errors" & WATCH_PID=$! diff --git a/skills/basecamp-connect/watch-requests.sh b/skills/basecamp-connect/watch-requests.sh index 8656e77e0..c8eaed514 100644 --- a/skills/basecamp-connect/watch-requests.sh +++ b/skills/basecamp-connect/watch-requests.sh @@ -32,19 +32,30 @@ unhandled() { lines=$(($(wc -l < "$out"))) [ "$lines" -gt 0 ] || return 1 head -n "$lines" "$out" | HANDLED=$handled awk ' - BEGIN { while ((getline id < ENVIRON["HANDLED"]) > 0) seen[id] = 1 } + BEGIN { + while ((read = (getline id < ENVIRON["HANDLED"])) > 0) seen[id] = 1 + if (read < 0) { + print "watch-requests: cannot read the handled file " ENVIRON["HANDLED"] > "/dev/stderr" + failed = 1 + exit 2 + } + } /"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 }' + END { if (failed) exit 2; exit !found }' } -if [ "$once" = true ]; then +while :; do unhandled - exit 0 -fi - -until unhandled; do - sleep 2 + result=$? + case "$result" in + 0) exit 0 ;; + 1) + [ "$once" = false ] || exit 0 + sleep 2 + ;; + *) exit "$result" ;; + esac done