Repository navigation
Add events, inbox, and stream-ticket commands on the account event feed - #726
Conversation
The account event feed's three operations — poll events, poll the inbox, mint a stream ticket — are not in an SDK release yet, so the pin is a basecamp-sdk main pseudo-version until the next tag. Re-syncing the vendored MCP model brings the EventFeed tag with it, and an SDK tag no DomainSpec claims fails the catalog build. It gets its own domain rather than joining the account grab-bag: the feed is a resumable lane with its own contract — a position and a continuation URL instead of page numbers — and the blurb is where an MCP client learns that.
…nt feed The account-wide event feed is how an agent hears about activity it did not cause. It is a different resource from `basecamp events <id>`, which is one recording's audit trail, so it lands as subcommands under the same noun: `events poll` for the catch-up poll lane and `events ticket` to mint a live-stream credential, plus `inbox` at the top level for the addressed-items lane, which is its own endpoint rather than a filter over the feed. Pagination here is the response body, not the Link-header page walk every other listing in this CLI uses: a page carries a durable position to resume from and a continuation URL while the current walk has more to serve. One call is one page; --all walks the continuation to the end of that walk, bounded by --max-pages, and says so when it stops at the bound rather than at the end. That is not a live tail — the live lane is a WebSocket the ticket opens, and connecting it would mean taking a WebSocket dependency the CLI does not have today. The three documented failures get the taxonomy they deserve. A 409 is the caller's to fix, so it exits as usage and names both filter digests. A 410 is not, so it exits as not-found carrying the lane's own re-entry point: the feed re-enters at the epoch the body names, the inbox at the earliest retained item. A 400 keeps the server's message, because a malformed position and a malformed filter have different fixes and only the body knows which. The inbox's 403 says what it means — the lane is served to agent principals only for now. The ticket and its URL are redacted unless --show-secret: the URL embeds the ticket, both are bearers for the ~2 minute window, and a mint that printed them by default would put a credential in every terminal scrollback and log. Bare `basecamp events` now shows the group's help instead of refusing for a missing id, so the subcommands are discoverable. Passing an id still does what it did.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
🟡 Changes recommended
The SDK bump leaves the Nix vendor hash stale, which will fail the repository’s Nix build check.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds account-wide event polling, agent inbox access, and stream-ticket minting.
Changes:
- Implements resumable event and inbox polling with bounded continuation handling.
- Adds secure stream-ticket redaction and feed-specific errors.
- Updates SDK/MCP models, CLI surface, documentation, and tests.
[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or rungh pr ready --undo.
Click "Ready for review" or rungh pr readyto reengage.
File summaries
| File | Description |
|---|---|
skills/basecamp/SKILL.md |
Documents event-feed workflows. |
scripts/check-bare-groups.sh |
Allows the actionable events group. |
internal/version/sdk-provenance.json |
Records the updated SDK revision. |
internal/mcpserver/testdata/catalog_snapshot.txt |
Adds the EventFeed MCP domain snapshot. |
internal/mcpserver/server_test.go |
Updates MCP tool count. |
internal/mcpserver/model/PROVENANCE.json |
Updates model provenance. |
internal/mcpserver/model/openapi.json |
Syncs event-feed API schemas. |
internal/mcpserver/model/behavior-model.json |
Adds feed operations and secret metadata. |
internal/mcpserver/domains.go |
Registers the EventFeed domain. |
internal/mcpserver/catalog_test.go |
Updates catalog expectations. |
internal/commands/recording_test.go |
Supports sequential stub responses. |
internal/commands/mcp_test.go |
Updates MCP command expectations. |
internal/commands/inbox.go |
Implements the inbox command. |
internal/commands/feed.go |
Implements feed polling, errors, continuations, and tickets. |
internal/commands/feed_test.go |
Tests event-feed behavior. |
internal/commands/events.go |
Adds poll and ticket subcommands. |
internal/commands/commands.go |
Adds commands to the catalog. |
internal/commands/commands_test.go |
Registers inbox in catalog tests. |
internal/cli/root.go |
Registers the inbox command. |
go.sum |
Updates SDK checksums. |
go.mod |
Updates the SDK dependency. |
e2e/smoke/smoke_event_feed.bats |
Adds live smoke coverage. |
e2e/events.bats |
Updates bare-events behavior tests. |
e2e/event_feed.bats |
Adds validation E2E coverage. |
API-COVERAGE.md |
Records the three covered endpoints. |
.surface-breaking |
Acknowledges the widened events argument. |
.surface |
Updates the CLI surface snapshot. |
Review details
- Files reviewed: 26/27 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
… flake The two poll lanes number different things: --since takes an event id on the feed and an addressed-item id on the inbox. One shared remedy string sent an inbox caller looking for the wrong identifier, so the hint now comes from the lane, which already carried the rest of its per-lane text. The SDK pin also moved vendored dependencies, which invalidates the Nix flake's vendorHash. Recomputed and verified against a Docker nix build.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4fb6a920d7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
🔵 Needs a closer look
The optional events argument regresses agent-help flag filtering, and two updated documents retain contradictory version or coverage metadata.
Review details
Suppressed comments (3)
Previously missed (3) — in code that hasn't changed since the last review.
internal/commands/events.go:22
- Making the identifier optional bypasses
curatedInheritedFlags:internal/cli/help.go:418-433suppresses--projectonly whenUsecontains<id|url>. As a result,basecamp events --agent --helpnow advertises project scope even though the recording lookup is account-scoped. Extend that detection to[id|url]and cover this optional-ID case in the help tests.
API-COVERAGE.md:54 - This new note describes the current pin but leaves the heading immediately above at “SDK version: v0.16.0,” contradicting both
go.modandinternal/version/sdk-provenance.json(v0.18.1-0.20260916113525-e58b5823a8ca). Update the heading so the coverage document does not report two different current SDK versions.
skills/basecamp/SKILL.md:146 - Adding the event-feed commands here leaves the skill headline at line 84 saying “189 tracked in-scope endpoints” and omitting the event feed, while this PR updates
API-COVERAGE.mdto 195. Update that headline as part of the skill sync so agents receive consistent coverage metadata.
- Files reviewed: 27/28 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Three review findings, all real. A 410's resume URL preserves the request's canonical filters, and the hint threw them away: re-entering with a bare --since drops --types, --buckets and the --exclude-performers self loop guard, so an agent recovering from an expired position would start consuming its own activity. The hint now renders the filters back out of the resume URL through the lane's own parser. A resume URL that will not parse, or one carrying a value outside srv2's alphabet, falls back to the bare re-entry — a hint is meant to be pasted, and half-rendered server text is not. The continuation origin check compared raw scheme and host strings, so a base URL written with an explicit default port, or a host in different case, rejected its own continuations and broke --all after the first page. It now compares the scheme case-insensitively and reuses this package's sameHostPort, which normalizes the omitted port to the scheme's default. Both poll commands handed the whole envelope to the output writer, which is one object: --count answered 1 for every page and --ids-only found no id to print. The rows go through WithDisplayData, the seam the account-wide listings already use for exactly this. An inbox row's identity is its addressing id, because applying the feed's dedupe-by-event-id rule to the inbox would discard every reason but one.
There was a problem hiding this comment.
🟡 Changes recommended
The new live smoke suite is not wired into the smoke runner, and the skill’s account-wide command list omits events ticket.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
skills/basecamp/SKILL.md:146
- The project-scope exceptions omit the new
basecamp events ticketcommand, even though its implementation is account-scoped just likeevents polland requires no--in. This rule can therefore lead agents to add an unnecessary project scope when minting a ticket.
- Files reviewed: 27/28 changed files
- Comments generated: 1
- Review effort level: Balanced
The file was written but named in no level array, so `make smoke` never ran it while the static coverage check — which scans files, not the arrays — reported it covered. It joins Level 0: the ticket mint is a POST, but it writes no record and leaves nothing behind for a parallel suite. The inbox case is allowlisted as an accepted gap. The lane is served to agent principals only, so a person's smoke token gets the 403 the test asserts before marking the gap, and an unallowlisted unverifiable blocks the run.
There was a problem hiding this comment.
🔵 Needs a closer look
Resume URL validation is incomplete, and human-readable output omits the position required to resume either feed lane.
Review details
Suppressed comments (3)
Previously missed (2) — in code that hasn't changed since the last review.
internal/commands/feed.go:225
- The pinned SDK requires callers to validate a resume URL's origin before parsing its query, but
gone.ResumereachesPoll*OptionsFromURLhere without thecheckContinuationapplied tonext. A wrong-origin 410 can therefore steer the filters rendered into the pasteable recovery command, including the self-loop guard. Thread the configured base URL through this error path, preserve filters only after the same-origin check, and otherwise use the bare fallback.
This issue also appears on line 530 of the same file.
internal/commands/inbox.go:130
- Styled/Markdown output replaces the envelope with
inboxRows(items), which hidesposition, and this breadcrumb leaves only a literal placeholder. As a result, a normal terminal user cannot resume the inbox without rerunning with JSON. Render the shell-quoted returned position in this command.
internal/commands/feed.go:530
WithDisplayData(events)makes styled/Markdown output render only the rows, so the durablepositionis hidden, while this breadcrumb prints only a literal placeholder. A default terminal invocation therefore gives users no value with which to resume (and a capped walk even says to use the “position above”). Put the shell-quoted returned position into the breadcrumb so non-JSON output remains resumable.
Cmd: "basecamp events poll --position <position>",
- Files reviewed: 29/30 changed files
- Comments generated: 0 new
- Review effort level: Balanced
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d61a138a67
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A position is bound to the filter set it was minted for, so every command this code hands back has to carry the filters or it is a command that fails. Three places did not. The resume breadcrumb offered `--position <position>` alone. On a filtered poll that is a different filter set, and the server answers 409 — the breadcrumb advertised its own failure. It now renders the filters the poll used, on both lanes. The 409 hint did the same thing in the other direction: the caller has just changed filters, and the remedy told them to re-enter without any. It now repeats the filters they presented. And a 410's resume URL is server text that this code displays as authoritative and reads filters out of, which is acting on it — so it now faces the same origin check the walk applies to a continuation. A resume URL pointing elsewhere is neither shown nor parsed, and the re-entry falls back to the filters the request was invoked with. The filter rendering is one helper now, fed either by the lane's continuation parser or by the request's own options. The skill's two project-scope exception lists had drifted apart, and the feed section described the inbox as the event filter set plus --reasons. It is not a superset: --creators, --performers, --exclude-performers and --actor-types are not flags there, and generated calls using them would fail. Both lanes now list their own filters.
|
@codex review |
There was a problem hiding this comment.
🟢 Approved
The implementation is cohesive and thoroughly tested; only a minor non-blocking documentation inconsistency remains.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
internal/output/envelope.go:618
- This documentation says every format renders an informational notice, but
writedeliberately suppressesWithNoticeinFormatQuiet/--agentunless it is diagnostic (lines 216–227). Document that exception so callers do not rely on informational notices being delivered in agent mode.
- Files reviewed: 34/35 changed files
- Comments generated: 0 new
- Review effort level: Balanced
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 03edb00b33
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The scheme was not on the list of components checked, so a response that echoed the ticket there printed it. A list of components to check is a list to be caught out by; the finished string is checked instead, alongside the decoded path, which is the one place an escaped copy could hide from it.
The documented way to resume is --position "$POSITION". An unset variable makes that an empty string, which read the same as omitting the flag and entered at the present — silently skipping the backlog the caller was resuming. Both lanes now say so instead, following the blank-flag pattern checkins already uses.
The allowlist gained events without the documented exception list gaining it, so the enforced convention and the written one disagreed. events is also the one plural noun in that set, so appending it to a list of commands "without a sibling plural group" would have moved the contradiction rather than fixed it.
Asserting on the finished string is what makes the class dead — it covers components nobody enumerated, which is how the scheme was missed. But what is known to be covered still has to be readable, so there is now a case per component: scheme, userinfo, host, port, path, escaped path, a second parameter, a repeated one, and the fragment. Each also checks the URL was withheld whole rather than partially scrubbed.
|
@codex review |
There was a problem hiding this comment.
🟡 Changes recommended
Ticket redaction can expose the URL’s bearer when it differs from the separate ticket field, and two notice comments misstate quiet-mode behavior.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
internal/commands/feed.go:505
--quiet/--agentdeliberately suppresses this informational notice, so the claim that every format receives the resume notice is false. Quiet output still includespositionin the data payload; describe that distinction instead.
internal/output/envelope.go:618- This contract is inaccurate for
FormatQuiet:writeemits onlyResponse.Datathere and deliberately suppresses non-diagnosticWithNoticevalues. Document that exception so callers do not assume an informational notice survives--quiet/--agent.
- Files reviewed: 35/36 changed files
- Comments generated: 1
- Review effort level: Balanced
The assertion checked the body's ticket field only. A response whose URL carries a different ticket than its body makes the URL's the operative bearer — the one that would actually open the stream — and it survived in the path because nothing was looking for it. The check now runs over the body's ticket and the URL's own ticket values alike.
|
@codex review |
There was a problem hiding this comment.
🔵 Needs a closer look
Explicit empty filter values currently become unfiltered account-wide polls instead of usage errors.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
internal/commands/feed.go:231
- Returning
nilwhen every entry is empty makes an explicitly supplied narrowing filter indistinguishable from omitting it.pflagparses--types "$EMPTY"as an empty slice, so either feed lane sends no filter and polls everything; the same silently removes an empty--exclude-performersloop guard. Reject a changed filter flag with no non-empty values (while still allowing a trailing comma after a real value) before account or network work.
- Files reviewed: 35/36 changed files
- Comments generated: 0 new
- Review effort level: Balanced
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3b6f8f358c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
url.URL.String escapes per component and picks the encoding itself, so a search for the raw secret misses whichever component it re-spelled: the scheme got through that way, then a non-ASCII host. Decoding the finished string normalizes every component at once, including the ones nobody has named, and replaces the hand-placed path check rather than joining it.
An unset variable marks the flag provided but leaves it carrying nothing, and dropping it silently widened the request: --buckets to account-wide, and --exclude-performers to no loop guard at all, so an agent starts reacting to its own activity. The guard reads the flags the caller set rather than a list of names to keep in step, so a filter added later is covered without being enrolled. A trailing comma after a real value is still fine.
|
@codex review |
|
Codex Review: Didn't find any major issues. Breezy! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
#726 put the account event feed on this server: poll_events and poll_inbox in the eventfeed domain, through the mechanical dispatch every other operation uses. That dispatch turns a failed call into its message and nothing more, because the canonical SDK error keeps only the message, the hint and a validation body's field errors. For the feed that throws the contract away. Its refusals are the recovery instructions — a 410 names where to re-enter, a 409 names the two filter digests that disagree, a 400 says whether to re-enter or to fix the filters — and a consumer that receives them as prose has lost the lane it was following. So the two poll lanes now run through EventFeedService, whose typed errors carry those members back out, and they come back as typed in-band errors in the vocabulary basecamp-mcp-server serves. The request is still built from the vendored model, so parameters, validation and the describe schema stay the model's. Rebuilt on #726 and on basecamp-sdk#912, which it pins. 912 split the refusals by lane, and each gets its own arm here: - The feed's 410, *FeedPositionGoneError, carries resume and epoch_after_id. - The inbox's 410, *InboxPositionGoneError, carries resume and never an epoch: that lane fences on 30-day retention and re-enters at since=0. - Either lane's 400, *FeedRequestError, is typed from its reason. The remedy sentence is read only when the deploy predates the reason, and an unknown reason is not guessed at through the sentence. - A 409 is filter_mismatch only when both digests are present. These types unwrap to the canonical error, so a missing arm is not a compile error: the refusal degrades to request_failed and silently drops the member a consumer recovers from. That is the trap 912 sets for any consumer, and the tests close it: inbox 410 arm removed: --- FAIL: TestFeedStalePositionsKeepTheirOwnRecoveryData/the_inbox_lane_resumes_at_since=0_with_no_epoch the 410 must carry its recovery data --- FAIL: TestFeedInbox410NeverCarriesAnEpoch 400 reason ignored: --- FAIL: TestFeedTellsTheTwo400sApart/the_reason_names_a_filter_error_even_when_the_sentence_says_otherwise --- FAIL: TestFeedTellsTheTwo400sApart/the_reason_names_a_position_error_on_the_inbox unknown reason guessed through the sentence: --- FAIL: TestFeedTellsTheTwo400sApart/a_reason_this_code_does_not_know_is_not_guessed_at_through_the_sentence Carried from review of the earlier revision of this PR, each proven red on the old code there: - A filter that normalizes to nothing (`buckets=,`, `types=,`) is refused, not dropped. Dropped, it left the dimension unset, which on the wire is no filter, and the caller got the whole account feed. - agents_only is claimed only on the bodyless 403 the inbox's principal guard sends, never on a scope or access refusal a caller could repair. - Argument refusals (a filter that narrows nothing, both entry points at once, a parameter buildRequest rejects) are typed invalid_arguments rather than prose. - EventFeed() is part of the required dispatcher API, so a client that cannot reach the feed cannot build a server that advertises it. The stream-ticket mint stays excluded, as #726 left it. Its result is a replayable bearer and a URL embedding it, this dispatcher returns results verbatim into a model transcript with no redaction, and nothing here could open the WebSocket it is for. The earlier revision of this PR served it read-only; that is not carried forward. Reversing a credential-exposure decision is not something to do inside a rebase.
The account-wide event feed is how an agent hears about activity it did not cause. This adds the three operations the SDK now generates:
basecamp events pollGET /events.jsonbasecamp inboxGET /inbox.json(agent principals only)basecamp events ticketPOST /events/stream_ticket.jsonContract:
doc/api/sections/event_feed.mdon bc3 master.Why
events polland not bareeventsbasecamp events <id|url>already means one recording's audit trail, and that is a shipped surface. Overloading the same command by arity — no args means the account feed, one arg means a recording — would give two unrelated payloads and two unrelated flag sets one help page, which is exactly what STYLE.md's bare-group rule resists. So the feed lands as subcommands under the same noun, andinboxsits at the top level because it is its own endpoint rather than a filter over the feed.Bare
basecamp eventsnow shows the group's help instead of refusing for a missing id, so the subcommands are discoverable. Passing an id still does what it did. That turns the<id|url>positional optional, which is a strict widening but still a.surfaceline change, so it is acknowledged in.surface-breaking.NewEventsCmdjoins thecheck-bare-groups.shallowlist for the same reasonrecordings,assignmentsandnotificationsare on it: it performs an action and has subcommands.Pagination is the body, not the Link walk
Every other listing in this CLI walks
Link: rel=nextpage numbers. The feed's pagination is the response envelope: a durablepositionto resume from later, and anextcontinuation URL while the current walk has more to serve. One call is one page.--allwalksnextto the end of the current walk, bounded by--max-pages(default 50 — the server serves up to 100 events a page, so that admits 5,000 before the walk reports itself capped). When it stops at the bound it says so in a notice instead of reading as complete. This is not a live tail: the live lane is a WebSocket the ticket opens, and the SDK's cable client carries agithub.com/coder/websocketdependency the CLI does not take today, so--stream/events tailis deliberately out of scope here.Filters are validated for shape only — integer-ness of ids,
selfor an id for performers,agent/personfor actor types. The 100-id caps are the server's and are not mirrored here: a cap written into the client goes stale as a refusal of a request the server would have served.--typesand--reasonspass through unvalidated because those catalogs grow.Errors
validation)reasonnames — re-enter for a malformed position, fix the filters for a malformed filter. No reason, no hint: the two remedies are opposite.--since". The caller's to fix.--since 0. Not the caller's mistake.All of it lives in one classification helper (
feedError), keyed by lane. basecamp/basecamp-sdk#912 has since merged and is adopted here: each lane owns the arm that recognizes its own 410, so one lane can no longer read the other's recovery by accident, and the 400'sreasonselects between the two opposite remedies. A 400 carrying no reason — an older server — still gets the server's message and no hint at all, because guessing between "re-enter" and "fix your filters" sends the caller to the wrong one.Every command this code hands back carries the filters the poll used, because a position is bound to its filter set: a resume without them answers 409, and a recovery without
--exclude-performers selfwalks an agent into its own activity. That covers the resume breadcrumb, the 409 hint, and the 410 hint — which prefers the filters the server's resume URL preserved, falling back to the request's own. A resume URL that fails the same origin check the walk applies to a continuation is neither displayed nor parsed.The ticket is a credential
events ticketredacts both the ticket and the URL unless--show-secret. The URL embeds the ticket, both are replayable bearers for the ~2 minute window, and a mint that printed them by default would put a credential in every terminal scrollback and CI log. An unparseable URL, or one with noticketparameter, is withheld whole rather than echoed — the ticket is opaque, so any component of a URL that did not parse could be carrying it.The mint is excluded from the MCP surface. The dispatcher returns a result verbatim into a model transcript with no equivalent of
--show-secret, and nothing on that surface could use the ticket anyway — opening the stream needs a WebSocket, which a JSON tool call is not. The sync script's existing exclusion list is held to a "still not JSON" assertion that exists for bodies the toolkit refuses to load, so the policy exclusion is its own list, named for the reason it exists.Continuation URLs
The SDK's
PollEventsOptionsFromURLdocuments an origin check as the caller's obligation, so--allchecks a continuation's scheme and host against the configured API base before following it. Only the query is ever re-issued — the SDK builds the next request against the configured base URL, so a foreign origin never receives the bearer — but a server has no business steering a walk's filters somewhere else either.SDK pin
The feed operations are unreleased, so
go.modpins a basecamp-sdkmainpseudo-version (a8046263835c, the head carrying basecamp/basecamp-sdk#912) viamake bump-sdk, andinternal/mcpserver/model/is re-synced from that same checkout. Re-pinning to the next SDK release tag is the operator's release step. The vendored OpenAPI diff is large because the sync also carries everything that landed on SDK main since the previous pin, not just the feed.The re-sync brings the
EventFeedtag with it, and an SDK tag noDomainSpecclaims fails the catalog build, so it gets its own MCP domain rather than joining theaccountgrab-bag.Tests
Unit tests run every envelope and every error shape against the recording stub transport: single page, empty page,
--allacross a continuation (asserting the second request re-issues the continuation's own filters, not the first call's flags), the--max-pagescap, a refused cross-origin continuation, 400/403/409/410, and every flag the command refuses before a request leaves the process.stubRoutegained abodiessequence because routes match on path alone and a position-based walk re-requests one path with a different query.e2e/event_feed.batscovers the refusals and the--agent --helphints;e2e/smoke/smoke_event_feed.batscovers the three leaves in the pre-release suite. The smoke suite enters at--since nowand prints no position, ticket or cable URL — a smoke trace is a file somebody keeps.bin/ciis green apart from the TTY-detection tests (TestIsInteractive*,TestIsTerminal,TestDeleteConfirmable…, the wizard's first-run preconditions), which fail identically on an unmodifiedorigin/mainin this sandbox and are about the environment's pty, not this change.Live canary
Against production, account read-only:
events poll --since now— exit 0, empty page with a position, nonext.events poll --since 0 --all --max-pages 2— exit 0, 200 events over 2 pages, continuation followed and origin-checked against production, capped notice emitted.events poll --position <malformed>— exit 9, "Unrecognized position. Resume with since=<id> or since=now."inbox --since now— exit 4, the agents-only message (the profile is a person principal, which is the documented path).events ticket— exit 0,expires_in: 120, ticket and URL redacted, notice shown.One thing deliberately not fixed
Ten other files under
e2e/smoke/are named in no level array ofrun_smoke.shand so never run: aliases, assignments, attachments, bookmarks, calendars, drafts, gauges, hillcharts, notes, notifications.smoke_event_feed.batswas the eleventh and is now wired in. The other ten are pre-existing, and wiring unrelated suites into the live release smoke is not this PR's call — but a check that everye2e/smoke/*.batsappears in an array would have caught all eleven, and is what would stop the twelfth.