Answer an agent's Pings from one connector only

Merged
JE
2d ago
+2141-41

Only one computer should answer an agent's Pings. A Ping is in no project, so serving projects can't divide Pings between connections the way it divides everything else: every connector for the agent sees all of them, and each one that admitted a line answered it. Once Basecamp adds connections (bc3 #13840), that is every Ping answered once per computer. This is the act-once rule for Pings, with no server change.

What a connector hears

StartedHears Pings
--pingsyes, and anything else outside a project
--no-pingsno; only its projects
neither, agent's first connection (credential is the agent client's own secret)yes
neither, a connection added after the first (secret starts bcac_)no
neither, a person's login (a bot user such as Clawdito)yes, as before
--projectno, as before; --pings --project is refused

Every connector still serves the projects it's started with, as before.

  • Kept per profile. The first start without --project saves the answer as pings.json beside the profile's ledger (either flag saves its own), and restarts keep it until a flag changes it. The start decides it while holding its instance lock, so another start of the same profile can't override it in between. A flag given false means the other flag (--pings=false is --no-pings). An unreadable pings.json refuses the start, and every surface that meets one names it to remove before choosing again.
  • Visible. The start line says pings=true|false and why. connect status (styled and --json) and connect doctor's Pings check read one view: the saved choice (always read, so a running connector can't hide an unreadable one), the connector running now from its lock (one an older build runs counts as "may hear"), and the other connectors on this computer for the same agent that hear Pings. They warn only when two would answer. The one answerer beside a connector that leaves Pings to it is named, not flagged. Guided setup uses the same view only to decide whether to suggest pinging the agent, and to say how to recover a saved choice that can't be read. How much more it should say is open for a decision; see the summary comment.
  • Warned about. The running connector writes whether it hears Pings beside its instance lock. A connector that hears Pings warns at start when another one for the same agent on this computer does too. An older build's connector counts as "may hear them", since it doesn't say. Connectors on other computers can't be seen, so starting just one there is the person's job, as ruled.
  • Admission and handoff. Admission discards a confirmed Ping's line as pings_not_heard when the run doesn't hear Pings, after the one Ping read and before any other. The handoff hands off a Ping's request only when the run hears Pings, and never on a run narrowed by --project. A test holds that outside a served project, nothing but a Ping's line is ever admitted, so this flag decides everything there.
  • Docs. The connect and connect setup help, README, docs/agent-connections-rollout.md and the basecamp-connect skill say "only one computer should answer Pings". Guided setup on an added connection no longer suggests pinging the agent to try it, and says how to make this computer the one that answers. The skill's new section sits after the per-request steps, which were 6 characters inside the compaction budget.

Calls I made that you may want to overrule

  1. --pings with --project is refused instead of supported. A narrowed run's feed is filtered to its projects server-side, so it never receives a Ping. Supporting the pair would mean dropping that filter and separating the blocked-retry sweep's scope from the feed filter: a wider change to a guarded invariant, for a case connect setup --serve already covers. If you want it, it's a follow-up.
  2. The first start saves its default, not only an explicit flag. Without this, reconnecting a profile where Basecamp adds connections would turn its credential into a bcac_ one and quietly stop it answering Pings. The cost: a profile that started out as an added connection keeps not hearing Pings even if it later becomes the only one. That's fixed with --pings.
  3. A shadow reads the profile's choice and saves none, so it admits what the connector it shadows would. A flag on a shadow run applies to that run only.
  4. The choice lives in the state directory, not in connect.json. connect.json is setup's to write, under the credential lock, and an older build's strict parse refuses an unknown key. A ledger table would need a schema migration that older builds refuse. Older builds never read pings.json, so a downgrade drops the choice and every connector hears Pings again, as before. The runbook says so.

Tests

Tests came first and failed before the code.

  • admission: a Ping's line from the operator (direct and group) or a person let into that Ping is discarded pings_not_heard, spending one Ping read; projects are untouched; and the invariant that nothing outside a served project is admitted without Pings, checked for every matrix type.
  • handoff: a Ping request is handed off only with Pings, and never on a narrowed run.
  • lock holder and pings.json: round-trip, an older holder reads as unknown, and malformed, foreign-readable or symlinked files are refused.
  • commands: the resolution table, then full runs against the fake Basecamp. They cover a first connection, an added connection across five restarts, --project, the refusals, and flags given false. They cover the second-connector warning, and its absence for --no-pings, for a shadow and for a stopped connector. They also cover the shadow mirroring the profile, a legacy holder not being counted against its own profile, and status, doctor and guided setup reporting a narrowed run.
  • Removing the line that hands the decision to admission fails three run tests.

bin/ci passes on Linux.

The one thing these tests don't do is run a Ping through a live feed into runConnect: the fake Basecamp here serves no account feed. The start line reads the decision from the admission policy itself, so the run tests catch a dropped wiring line, and the admission and handoff tests cover the behaviour.