Add the Pings service and find a Ping's chat-line Campfire
Pings were not usable through the SDK. The SDK could not list a Ping, open one, or create one, and an agent connector could not read a single Ping line it was sent. Recordings().Summarize reported every Ping chat line as recording_unresolved ("tried 0 campfires"). That is why basecamp-cli's connector blocks every ping line as read_unresolved (basecamp/basecamp-cli#903).
This is two feature commits and a repin commit.
1. The Pings service
ListPings, GetPing and CreatePing cover GET /circles.json, GET /circles/{id}.json and POST /circles.json in all seven SDKs. The service is named for the product, the way Campfires sits over /chats.json. "Circle" is only the bucket type and the wire path. The names match bc3 #13943's merged pings.md ("Get Pings", "Get a Ping", "Create a Ping").
- Go.
AccountClient.Pings() *PingsServiceprovidesList(ctx) ([]Ping, error),Get(ctx, pingID int64) (*Ping, error)andCreate(ctx, *CreatePingRequest{UserIDs []int64; NewGroup bool}) (*Ping, error).(*Ping).CampfireID() (int64, bool)reads the Campfire id fromroom_urlthrough the SDK's route table. - Shape. A Ping's id is its bucket's id.
room_urlnames its one Campfire.PingLinksmodels onlylinesandfiles. The wire'sself,app,bookmarkandedit_titlerepeat the Ping's own URLs, andselfis not a legal Rust field name. - Errors.
CreatePing's bodiless 422 (nobody in the Ping can use Pings) gets a newBareValidationError. All three operations answer403 {"error":"Pings have been disabled"}when the caller can't ping.
2. Chat-line discovery for Pings
Discovery read the project dock, then /chats.json filtered to the bucket. A Ping's bucket is not a project, so the dock read 404s. bc3's /chats.json lists only dock-positioned Campfires, so a Ping's Campfire is never in it. Old comments assumed Pings were in the listing; they now say what is true.
How the per-bucket source now works:
- It reads the bucket's own Campfire. A project still answers from its dock, with no extra request.
- When the project read 404s,
GetPingnames the Ping's Campfire. - That answer goes in the same per-bucket cache, under the same TTL, refresh floor and budget. A second line in the same Ping costs no new read.
- A bucket that is neither a project nor a Ping costs one extra 404 per TTL, then falls through to the listing as before.
- A failed Ping read, such as the 403, is returned as that read's error, never as unresolved.
No source was added beyond these two.
All seven SDKs carry the fix. RecordingsSummarize is conformance-tested from one fixture with sequential mocks, so a Go-only change would have broken the other six runners. In conformance/tests/recording_summary.json, the five non-project cases now expect the Ping read's 404. A new case pins the Ping path at three requests with no listing.
Provenance: repinned past bc3 #13943
bc3 #13943 (merged 2026-10-19, b1f18fd182) documents Pings and adds the API index template. The third commit repins spec/api-provenance.json to it and regenerates spec/bc3-routes.json (403 to 416 routes), so the three Ping routes are now documented upstream and the temporary sdk_routes_absent_from_bc3_docs entries for them are deleted. spec/api-gaps/pings.md records it.
The repin also made four other waivers stale (PollEvents, PollInbox, CreateStreamTicket, MoveRecordingToVault; their docs were past the old pin), and the gate asked for them to be removed. The wider range documents five routes the SDK models no operation for: hill chart progress updates (#13620), joining an all-access project (#13623) and a vault's view preference (#13372). They are registered in spec/api-gaps/ with registry: entries, not absorbed here, and the range triage is in spec/api-gaps/README.md.
Known transitional gap
Until bc3 deploys the GET /circles.json index template to production, the route answers with a bodiless 204 (observed 2026-10-19, before the merge). Go and TypeScript read that as an empty list. Ruby, Python, Kotlin, Swift and Rust fail to decode it, like every unpaginated array read in those SDKs. The template ends that window, so I did not change shared base code for it.
Verification
Full gate. make check with Smithy 1.68.0 and uv pinned through mise ends "All checks passed". The Swift targets SKIP on Linux, so CI's macOS job is the first full Swift run.
Conformance results from the final run (after review fixes, with the Pings fixture added):
| SDK | Passed | Failed | Skipped |
|---|---|---|---|
| Go | 317 | 0 | 2 |
| Kotlin | 316 | 0 | 3 |
| Ruby | 300 | 0 | 19 |
| Python | 319 | 0 | 0 |
| Rust | 319 | 0 | 0 |
TypeScript's runner reported 376 passed and 2 skipped. The six cases in conformance/tests/pings.json (ListPings, CreatePing) and the Ping discovery cases pass on every runner except Swift, which needs macOS (CI).
Red then green. On the pre-fix recording_summary.go, the Go conformance runner failed all 6 discovery cases. With the fix, all pass. The new Go tests, TestSummarize_ChatLineFoundViaPing and TestSummarize_ChatLinePingReadFailureIsNotUnresolved, fail pre-fix with exactly the production error ("tried 0 campfires"). Each port recorded the same red then green in its native tests.
Swift. The SDK sources were generated and tested on Linux through a local toolchain shim. Discovery and Pings tests pass there, and its conformance runner passed 312 of 313 with one existing skip. FlexibleIntTests failed under the shim. That is unconfirmed as pre-existing, and macOS CI settles it.
Production, read-only. GET /circles/{id}.json returned 200 with room_url ending /chats/{id}.json. GET /circles.json returned 204. No writes were made.
Summary by cubic
Adds a Pings service and makes chat-line discovery find a Ping's Campfire, fixing RecordingsSummarize returning recording_unresolved for every Ping line.
Previously the SDK could not list, open, or create Pings, and an agent connector could not read a Ping line it was sent. The service adds ListPings, GetPing, and CreatePing over GET /circles.json, GET /circles/{id}.json, and POST /circles.json in all seven SDKs. Discovery now reads a non-project bucket's Ping (via room_url) instead of falling through to the Campfire listing, which never lists Ping Campfires; a bucket that is neither still falls through as before, and the Ping read's failures (like 403) surface as errors. A room_url that is not a text chat URL (non-string, unparseable, or wrong shape) refuses the read as malformed rather than caching "no Campfire" and reporting recording_unresolved.
The conformance suite adds six ListPings/CreatePing cases (array in order and empty, disabled-Pings 403, request and 201, new_group omitted when unset, bodiless 422), run by every runner. Go and Ruby now assert the disabled-Pings 403 surfaces instead of an empty list.
Migration
- The provenance pin advances to bc3
b1f18fd182(2026-10-19), dropping the three Pings routes out of the bc3 route allowlist and deleting four waivers the repin made stale (PollEvents,PollInbox,CreateStreamTicket,MoveRecordingToVault). - The wider docs range documents five routes the SDK models no operation for; three new
spec/api-gaps/files register them (hill-chart-progress-updates,project-join-admission,vault-view-preference) instead of absorbing them. - Until the index template deploys to production,
GET /circles.jsonanswers a bodiless 204; Go and TypeScript read that as an empty list, while the other five SDKs fail to decode it. No shared base code was changed for this.