Say why one connector serves every repo, and drop references to redispatch
Part of the end-to-end connector testing stack, but independent of the other PRs: it touches no test harness and depends on none of them.
Why
- A second connector is refused without saying why it is never needed. People reach for a second
basecamp connectto serve another repo. But the repo is not the connector's choice: the session reading its requests picks the repo for each project. The refusal named the holder and stopped there. - The skill did not say how to run many agents, or one agent over many repos. It said "only one connector per agent" and nothing about the shape that works.
- Text still pointed at
basecamp connect redispatch, which #815 removed with the worker side. The status line counted "redispatches waiting for their task", and admission's comments said a blockedno_routerecord "receives a holding reply". Nothing posts that reply now, and nothing re-runs that record.
What
The lock refusal (still lock_unavailable, exit 5):
Another connector for this agent is already running: its lock is held by pid 4242 since 2026-10-02T10:00:00Z
One connector serves every repo: the session reading its requests picks the repo for each project, so an agent never needs a second connector. Use the session already running it, or stop that connector before starting another.
- The reason is in the hint, and the message names the holder as before.
- A
--shadowrun has its own state directory and lock, so it never meets the connector's lock. A shadow refused here met another shadow, and it is told only that. Nothing implies that a shadow is a second connector. connector.AlreadyRunningErrorcarries the holder, so the command can word the refusal itself.Error()is unchanged for the other callers (discard,release,shadow promote).
skills/basecamp-connect/SKILL.md:
- A new section, "One connector per agent, any number of repos". One agent with many repos uses one connector, and the session picks the repo for each project from
reposinlast.json. Many agents use one connector each, each in its own session, started from any folder. last.jsonis shared by those sessions, so each write reads the file again first and keeps thereposalready there.- "When the connector stops" now quotes the new refusal and says what to do.
- The
basecampskill's paragraph aboutbasecamp connectsays the same in two sentences.
Redispatch and holding replies:
- The intake sweep,
LoadUndecided, the blocked-retry schedule (NextBlockedRetry), the reason docs inmatrix.go,WithServed, the setup lock's doc insetup/private.go, and the ledger's decision comments now say what happens today. A blocked record that the timed retry leaves alone (untimed reasons such asno_route, or a window that has passed) stays blocked until a person discards it. connect statuskeeps theredispatch_pendingcount, because the JSON field is part of the output. Only a build that ran workers wrote a pending redispatch or ended a task, so the text line now reads "redispatches left by an older build, which this one never runs".- No behavior changes.
Skill evals (skill-evals/cases/basecamp-connect/). The request line is given in the context, and the commands are mocked:
boost-retry-pre-send: every boost fails withNot authenticated for profile:. The boost is retried, three tries at most, with no login, and the handoff says the acknowledgement is still owed.boost-no-retry-after-send: the boost fails after the request was sent (a deadline). One try only.skip-handled-event: a restart prints anevent_idthat was already handled, next to a new one. Only the new one is boosted. Nothing touches the old one's recording or thread.holding-reply-unmapped-repo: the project maps to no repo. The session boosts, then posts a holding reply as the agent atreply_tothat mentions the requester, then asks which repo. It never guesses one.
Dead code found, not removed here
#815 left the ledger's worker-era write paths in place. No command reaches them:
Ledger.Redispatch(withRedispatchResult,LiveWorker,authorizeBlocked), andAuthorizedBlocked, which already said "Nothing calls it".Ledger.LaunchTaskandLedger.EndAttempt, and with them the only code that ends a task.setup.TryLock, which only tests call now.- Most of
internal/connector/driver.
make deadcode reports the driver package and TryLock. It misses the ledger methods, because *Ledger escapes to an interface. Removing them touches the schema's triggers and many tests, so it should be its own PR. This one only makes the comments true.
Tests
TestASecondConnectorIsToldOneServesEveryRepo: both wordings, the holder,lock_unavailableand exit 5.TestASecondConnectorIsRefusedBeforeItWaitsForATokennow also checks the holder and the hint end to end through the command.TestSecondConnectorOnTheSameAgentIsRefusedchecksAlreadyRunningError.Holder.- The new eval cases compile under Onigmo (
make check-eval-patterns) and pass the Go guard inconnect_skilleval_test.go. The runner's owngradewas given good and bad traces for each case: the good ones pass, and each bad one fails on the rule it breaks. The evals themselves were not run (they need an API key). bin/ciis green.
Summary by cubic
Explains in the connector's lock refusal why an agent never needs a second connector, and rewrites the docs, comments, and skill text so they stop pointing at basecamp connect redispatch and holding replies, which went away with the worker side in #815.
- A refused connector now gets a hint: one connector serves every repo, since the session picks the repo for each project. A shadow run gets its own phrasing, and the refusal still names the holder, carried by a new
AlreadyRunningError. - Comments now say that a blocked record the timed retry leaves alone (untimed reasons such as
no_route, or a window that passed) stays blocked until a person discards it;connect status's text line notes its redispatch count was left by older builds. - Adds four skill eval cases: boost retried before send, no retry after send, skipping an already-handled event, and a holding reply when a project maps to no repo.
- Worker-era dead code (
Ledger.Redispatch,LaunchTask,EndAttempt,setup.TryLock) is identified but not removed: dropping it touches schema triggers and many tests, so it stays for its own PR.