Repository navigation
Conversation
A Ping is a Circle bucket, never in connect.json's served projects, so every line in one was blocked as an unserved project's, or held read_failed in project trust when the membership read 404ed on the Circle. Admission now reads a chat line's unserved bucket as a Ping before anyone's trust, and decides it by the Ping's rules: - Only an operator (the operator, or someone the allowlist names) gives the agent work by Ping. Project trust's members are a project's, so anyone else is discarded untrusted_performer without a membership or line read. - In a direct Ping, the agent and the writer alone, every such line is a request, trigger pinged. In a group Ping only a mention of the agent is. - The agent's own line, another agent's, a delegated one and an allowed agent's mention are refused as before; the last is discarded at the gate in any bucket connect.json does not serve. A Ping's conversation is the Ping (ping:<id>), answered in its Campfire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Ping is in no project connect.json serves, so the handoff, which hands off only records in served projects, would have kept an admitted ping waiting forever, and handed off nothing at all for an agent serving no project. It now also hands off a record whose conversation is a Ping (ping:<id>), which admission writes only after the Ping read said the bucket is one; a run narrowed with --project hears no Ping. No ledger schema change. The request line gains ping: true for a Ping's line, so the session knows there is no project, AGENTS.md or project repo, and replies in the Ping. The skill and subagent say so, the owner's direct ping counts as their word like their mention, and setup, the guided summary and the no-project warning say operators' pings are heard with no setup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The live subscription holds every bucket the agent can see, Pings included, but the membership listing named only projects. Every line in a Ping therefore read as from a bucket the subscription did not hold and reconnected the feed, again once per membership interval per active Ping. The listing now names the agent's Pings too; Pings turned off (403) list none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
robzolkos
force-pushed
the
connect-pings
branch
from
October 19, 2026 21:49
24b970c to
293b74b
Compare
Trust was agent-wide: an operator was heard everywhere, and a Ping heard
nobody else. connect.json may now name, per Ping, people who reach the
agent there and nowhere else:
"pings": {"<ping-id>": {"participants": [<person-id>, ...]}}
Such a person's line in that Ping is a participant's request, by mention
only, in every trust mode: a direct Ping's every-line rule stays an
operator's. Anywhere else, other Pings and their direct Ping with the agent
included, they are whoever the rest of the policy makes them.
The gate opens the entry only in a bucket connect.json does not serve, and
before project trust, so a Ping is never asked for members. The entry
counts only once the Ping read says the bucket is a Ping. Validate refuses
the agent itself and any allowed agent as a participant.
basecamp connect setup -P <agent> --allow-in-ping <ping-id>:<person-id> writes the person into that Ping's entry in connect.json; it is repeatable, --disallow-in-ping takes one out, and the entries are kept across runs like the allowed agents. A Ping nobody is let into is no entry, and with none the "pings" key is left out, so a file that uses none still opens in a CLI that predates it. Setup reads each Ping as the agent, the way admission does, and refuses a Ping the agent cannot read, a person not among its people, the agent, an Agent and a client, as project trust refuses clients. A person already recorded who has since left, or whose Ping can no longer be read, is a warning, as for the allowlist. connect show lists each Ping's participants, and the guided setup treats both flags as policy flags.
The connect skill gains the "only in our group Ping" row, a paragraph on letting someone into one Ping (how to find the Ping's id, including the sidebar URL form, and the person's id, what setup refuses, and the restart), and says a participant's role covers them. Its per-request section stays inside the compaction budget, at 13,951 of 14,000 characters. The subagent learns that in a Ping the operator's go is a mention only, since a boost there never reaches the agent, and that an ask the operator cannot see is said and left.
This was referenced Oct 19, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An agent like R2-D2 now answers when its operator pings it. Today
basecamp connectblocks every line in a Ping. The line was never handed off, so a direct message to your own agent went nowhere (#903).This depends on #913 (the pings commands) and on the unreleased SDK in basecamp/basecamp-sdk#1015, through #913's DO-NOT-MERGE replace commit. basecamp/basecamp-sdk#1015 is also where
Summarizelearned to find a Ping line's Campfire. It needs bc3 #13943 server-side. The base ispings-commands.Who can now make the agent run
trigger: pinged,owner: true)mentioned)--allow)role: operator,owner: false)--allow-in-ping)mentioned,role: participant,owner: false)untrusted_performer(gate, no read)untrusted_performer--trust projectuntrusted_performeruntrusted_performeruntrusted_performeruntrusted_performeragent_authored(gate, no read)agent_authored--allow-agent) or notdelegateddelegatedPings are on by default and need no setup. Existing setups keep working without a re-run. There is no switch to turn pings off. A run narrowed with
--projecthears no Ping, because the feed filter already drops its buckets.What this does not open. Project trust's participants are a project's members, and a Ping belongs to no project, so they get nothing here. An allowed agent's mention stays a participant's request. A Ping hears none, and the gate discards it as it already does in any unserved bucket. That also keeps the agent caps out of Pings. A line's author must be an operator as well as its performer. The owner's direct-ping line counts as their word, the same as their mention, so it can carry a connector change (
owner: true). The reason is that nobody but its author writes a chat line. An assignment is the opposite case, because anyone can edit the card.A person in one Ping only
Trust used to be agent-wide: an operator was heard everywhere, and nobody else in any Ping. The operator can now let one person reach the agent in one Ping and nowhere else:
"pings": {"<ping-id>": {"participants": [<person-id>, ...]}}, read intoadmission.Policy.Pings(map[int64]PingTrust). Absent means nobody. Setup writes no key when there is no entry, so a file that uses none is byte-for-byte what it was.participantthere, sosubagent.md's A participant asks, an operator authorizes applies, andowneris false.matchPing.untrusted_performerunless trusted some other way. Works in every trust mode, additively.Decidediscards the event unless the Ping read confirms the bucket is a Ping.matchPingtakes the author's role from the operator set or the Ping's entry, and givespingedonly when both performer and author are operators.Policy.Validaterefuses the agent itself and any--allow-agentagent as a participant. Setup reads the Ping as the agent, the way admission does, and refuses a Ping the agent cannot read, a person not among its people, an Agent, and a client (as project trust refuses clients). A person already recorded who has since left, or whose Ping can no longer be read, is a warning, as for the allowlist.setup.Parserefuses unknown keys, so an older CLI refuses a connect.json that has apingsentry.connect,showandsetupall stop with "unknown field" rather than run without it. That fails closed, and it happens only once someone uses the flag. Admission's own reader ignores unknown keys, but nothing reaches it withoutsetup.Load.connect showlists each Ping's participants. The skill has a "talk to it only in our group Ping" row and explains how to find the Ping id (pings list,.../circles/<id>or.../my/sidebar/circles/<id>) and the person id (pings show <ping> --json). The subagent learns that the operator's go in a Ping is a mention only, because a boost in a Ping never reaches the agent, and that it should stop and say so when the operator is not in the Ping.Data shape
Reads.Pings(PingReader.Ping(ctx, bucketID) (*basecamp.Ping, error)). It returns nil when the bucket is not a Ping the agent is in: a 404, which is every project, or a 403, which means Pings are turned off. The SDK adapter caches the "not a Ping" answer for 10 minutes. A Ping itself is read fresh each time, because its people decide whether it is direct.chat.line.createdin a bucket connect.json does not serve, the bucket is read as a Ping before any trust read. The place decides whose word counts there. Under--trust projectit also prevents a membership read on the Circle, which bc3 answers 404 and which used to hold the lineread_failed.matchPingholds the Ping rules in one place. The author must be an operator and no Agent. A mention givesmentioned. A direct Ping givespinged. Anything else isnot_addressed.TriggerPingedis a new trigger. It is not a matrix row, because only a Ping can admit under it.ping:<id>(admission.PingConversationKey). The reply goes to the Ping's Campfire.Servedstays false andClassstays empty, since connect.json serves no Ping. The handoff hands off(served AND bucket in served projects) OR conversation_key = 'ping:' || bucket_id. There is no ledger schema change, so an older CLI can still open the ledger."ping": true|false, added to the request contract and the skill's example.Direct versus group. The wire has no group flag, so "direct" is computed from
Ping.Peopleas the agent sees it, which lists the others in the Ping. The rule is exactly one other person, and that person wrote the line. One edge case is a group that has shrunk to the agent and one person: it reads as direct. I think that is fine, because nobody else reads the line any more. A direct Ping never gains a third person, since bc3 allowscan_change_access?only on groups. A stale read can therefore only make a Ping look like a group, which is the safe direction.Why not bc3's
pingedaddressing reason. It lives on the inbox lane, and the connector reads the account feed, where intake refusesreasons. It also addresses every participant for every line, group Pings included, so it says nothing about direct versus group or about mentions. The Ping read and the mention check would still be needed.The live lane. The live subscription holds every bucket the agent can see, Pings included. The membership listing named only projects. Every line in a Ping therefore looked like it came from an unheld bucket and reconnected the feed, repeating every 10 minutes for each active Ping. The listing now includes the agent's Pings. If Pings are turned off (403), it lists none.
Skill and docs
SKILL.mdcovers thepingedtrigger and thepingkey. For repo choice, a Ping has no AGENTS.md orlast.jsonmapping, so the agent uses the repo the request names or asks. The owner rule now acceptsmentionedorpinged. The setup section says operators can ping the agent with no setup. The per-request section sat at 13,983 of its 14,000-character compaction budget, so some prose there was tightened to make room. It now ends at 13,974.subagent.mdsays to read a Ping withbasecamp pings messages, to skip AGENTS.md, and to reply withbasecamp pings post <bucket_id> -. It also describes thepingedtrigger.connect setup --helphas a Pings paragraph, andconnect --helpand--projectmention pings. The no-project warning and the guided summary now say operators' pings still reach the agent.Verification
internal/connector/admission/pings_test.go. They were written before the code and failed against it: direct pings were discarded, and a member's ping in project trust was heldread_failed. Each rule was then removed in turn from the code, and its test failed every time (7 of 7): the performer check, the author check, the "exactly one other person" rule, the "a direct line needs no mention" rule, the served-project exemption, Agent-authored words, and the owner's direct ping.internal/connector/admission/ping_participants_test.go,internal/connector/setup/pings_test.goandTestConnectSetupLetsAPersonIntoOnePing. The admission tests failed before the gate and verdict change. 18 mutations were applied one at a time, and each made a test fail: 9 in admission (entry for any Ping, served buckets, entry checked after project trust, no Ping-read confirmation, in-ping refused in a Ping, participant needing no mention in a direct Ping, any author, and the two Validate refusals) and 9 in setup and the command (not-in-Ping, client, Agent, unreadable Ping, recorded warning, disallow, Apply aliasing, verification unwired, show omitting pings). Each setup refusal was also checked to fail for its own reason, not an incidental one.TestAPingsRequestIsHandedOffWithNoServedProject) fails when Pings are left out of the handoff query. The membership test fails without the Pings listing.TestThePingReadTellsAPingFromAProjectcovers the SDK adapter's 404, 403 and 500 answers and its cache.auth agent connect, with the device approval done server-side. The connector ran in--trust projectfrom a throwaway state directory.pingedand its request line hadping:true,owner:true, andreply_toset to the Ping's Campfire.untrusted_performer. Two earlier lines from the same member, heldread_failed, were retried and also endeduntrusted_performer.not_addressed. With a mention it was admitted asmentioned, withping:true.chat line <url>,pings messagesandpings post <ping> -all worked.bin/cipasses locally, including skill drift, the request contract and the compaction budget. The one exception is the SDK provenance check, which it skips while Add pings commands: list, show, start, messages, post, upload #913's replace points at the local SDK. Local golangci-lint 2.15 flaggedcapshadowing a predeclared name inpolicy.go. CI pins 2.11.1, which does not flag it, and I renamed it tolimit.Not run: the paid connect skill evals (
make -C skill-evals eval-connect). The existing cases cover setup and repo mapping, which this keeps.Summary by cubic
Makes the connector hand off an operator's Ping, so the agent answers when its operator pings it. Previously every line in a Ping was blocked because Pings are not served projects.
pingedtrigger, the"ping": truerequest flag, and theping:<id>conversation key; no setup or re-run needed.--projecthears no Ping, and no ledger schema changes.Who can ping
pinged,owner: true)mentioned)--allow)role: operator,owner: false)--allow-in-ping)mentioned,role: participant)untrusted_performer), no readuntrusted_performer), no readagent_authored,delegated)Project trust's participants are a project's members, and a Ping belongs to no project, so they get nothing here. That also keeps the agent caps out of Pings. Each line's author must be an operator as well as its performer; the owner's direct-ping line counts as their own word, like their mention.
Connecting a person to one Ping
connect setup --allow-in-ping <ping-id>:<person-id>and--disallow-in-pingwrite connect.json'spingskey; a named person is a participant in that Ping alone, by mention only, and everywhere else is whatever the rest of the policy makes them.pingskey, so an older CLI still opens it; an older CLI refuses one that has it (unknown field).connect showlists each Ping's participants.Data shape
chat.line.createdin a bucket connect.json does not serve, the bucket is read as a Ping before any trust read. The SDK adapter caches the "not a Ping" answer (404, or 403 when Pings are off) for 10 minutes; a Ping's people are read fresh, since they decide whether it is direct.matchPingholds the rules in one place.TriggerPingedis a new trigger, not a matrix row. "Direct" is computed fromPing.Peopleas exactly one other person who wrote the line; a stale read can only make a Ping look like a group, the safe direction.ping:<id>(admission writes it only after the Ping read says the bucket is one). Older CLIs can still open the ledger.ping: true|false, added to the request contract.Skill and docs
SKILL.mdandsubagent.mdcover thepingedtrigger, thepingkey, and the per-Ping participant row. A Ping has no project: no AGENTS.md or repo mapping, so the agent uses the repo the request names or asks, and replies withbasecamp pings post <bucket_id> -.Verification
pings_test.gowere written before the code and failed against it; each of the 7 rules removed in turn failed its test. The handoff, membership, and SDK adapter tests fail without their change.ping_participants_test.goand setup'spings_test.go; 18 mutations were applied one at a time, and each made a test fail.pinged, a project member's discarded, a group Ping without a mention discarded and with one admitted, and boosts,chat line,pings messagesandpings postall worked. Existing Pings caused no reconnect; a group Ping created after start caused one.bin/cipasses locally, including skill drift and the compaction budget, except the SDK provenance check skipped while Add pings commands: list, show, start, messages, post, upload #913's replace points at the local SDK.Depends on #913 (the pings commands) and the unreleased SDK in basecamp/basecamp-sdk#1015, through #913's replace commit. Not run: the paid connect skill evals (
make -C skill-evals eval-connect).Written for commit 14c799f. Summary will update on new commits.