Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions internal/commands/agent_hook_connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 28 additions & 18 deletions skills/basecamp-connect/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<connector-output-file>' | grep --line-buffered -F '"type":"request"'
bash '${CLAUDE_SKILL_DIR}/watch-requests.sh' '<connector-output-file>' ~/.config/basecamp-connect/'handled-<profile>'
```

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 <event_id> >> ~/.config/basecamp-connect/'handled-<profile>'`), 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

Expand Down Expand Up @@ -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 '<old-output-file>' ~/.config/basecamp-connect/'handled-<profile>'
```

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 '<profile>'`: 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.
Expand All @@ -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.
50 changes: 50 additions & 0 deletions skills/basecamp-connect/watch-requests.sh
Original file line number Diff line number Diff line change
@@ -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] <connector-output-file> <handled-file>" >&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() {

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.

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
Loading