diff --git a/internal/commands/agent_hook_connector.go b/internal/commands/agent_hook_connector.go index 841a0213d..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 monitor 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/SKILL.md b/skills/basecamp-connect/SKILL.md index 6206e9f01..779603870 100644 --- a/skills/basecamp-connect/SKILL.md +++ b/skills/basecamp-connect/SKILL.md @@ -75,25 +75,22 @@ 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 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 @@ -549,12 +546,24 @@ 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), 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 - `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 +573,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..8656e77e0 --- /dev/null +++ b/skills/basecamp-connect/watch-requests.sh @@ -0,0 +1,50 @@ +#!/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 [--once] " >&2 + exit 2 +fi + +out=$1 +handled=$2 + +if [ ! -f "$out" ]; then + echo "watch-requests: no connector output at $out" >&2 + exit 2 +fi +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 + +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 ' + 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 } + } + END { exit !found }' +} + +if [ "$once" = true ]; then + unhandled + exit 0 +fi + +until unhandled; do + sleep 2 +done