Skip to content

fix(client): a 304 confirms a payload rather than establishing one - #129

Open
XieX wants to merge 2 commits into
xie/agent-skillsfrom
xie/skills-304-first-payload
Open

XieX wants to merge 2 commits into
xie/agent-skillsfrom
xie/skills-304-first-payload

Conversation

@XieX

@XieX XieX commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes a path in the FDv2 delivery transport that could report a store holding nothing as initialized, which authorizes write_skills("*") to prune every managed SKILL.md on disk. Raised by Bugbot on #87 (comment r4146430929); verified reachable, and traced to two claims that were wider than what happened.

A poll adopted the response ETag after any body that did not raise, and a 304 published a first payload unconditionally. Between them, a body this SDK could not apply — a future intentCode, or a payload-transferred with no recognized intent — lent its etag to the next request, and the 304 that answered it reported the empty committed set as current: initialized, healthy, and with nothing in a 304 to notice it on.

Three claims narrowed:

  • payload-transferred reports a commit only when it applied a pending set. A none intent builds none, nor does an unrecognized intentCode, and a foreign payload's contents are declined — those transfers now report nothing, where they reported a commit. They no longer adopt the selector of a payload that was never applied either, which on its own left a store resuming from content it does not hold while every diagnostic read healthy.
  • A poll adopts the response ETag only from a body that completed an exchange: a committed payload, or a none intent, which is the server saying the content held is what the etag describes. Gating on the commit alone would have broken the steady state — the none body's etag is how a poll goes from a tiny 200 every interval to a bodiless 304.
  • A 304 no longer publishes a first payload. It confirms the payload the store holds; the exchange it stands in for, the none intent, does not publish one either.

Not a new rule: TESTING.md §3.25 already defines is_initialized() as true "once a payload has committed", and §3.21/§3.22 turn prune suppression on it. The spec gains the bullets that were missing for the poll path in launchdarkly/ai-sdks-monorepo#34, and js-ai-sdk carries the identical fix in launchdarkly/js-ai-sdk#104.

test_a_304_before_any_payload_still_releases_wait_for_skills justified the old behavior as "a reconnect with a cached basis", which this transport has no mechanism for — _basis and _etag both start as None with no injection point — so a 304 reaching a store that holds nothing takes a server answering a request that carried no etag at all. It now fails closed, and the test asserts that.

Test plan

  • pytest — 2164 passed, 11 skipped
  • ruff check / ruff format --check / mypy packages/*/src clean
  • New: a body under an unrecognized intent leaves the next request carrying no If-None-Match, the store uninitialized, nothing held, and no selector adopted
  • New: all four shapes that reach payload-transferred with no pending set report neither a commit nor an up-to-date answer, and still count the transfer
  • Rewritten: a 304 answering a request that carried no etag leaves wait_for_skills false and is_initialized() false without recording a failure
  • The over-cap poll test was leaning on the same path for its "delivery carries on" assertion; its retry now gets a real payload

🤖 Generated with Claude Code


Note

Overview
Fixes FDv2 poll/stream handling so an empty or unapplied store cannot become is_initialized() and authorize write_skills("*") to prune every managed skill on disk.

payload-transferred now commits (and adopts resume basis) only when a pending set was actually applied—none, unknown intent codes, foreign payloads, and lone transfers with nothing pending report no commit, no up-to-date signal, and no basis.

Polling stops treating HTTP 304 as publishing the first payload; it only confirms content already held. Response ETags are adopted only after a poll body completed an exchange (a real commit or a none intent on its own event). _apply returns that completion flag so bodies the SDK declined cannot lend an etag to a later 304.

agents.md documents the three coupled rules. Tests are updated for the old 304-boot behavior and add coverage for the unrecognized-intent → etag → false-healthy chain.

Reviewed by Cursor Bugbot for commit 346a552. Bugbot is set up for automated code reviews on this repo. Configure here.

A poll adopted the response `ETag` after any body that did not raise, and a
304 published a first payload unconditionally. Between them, a body this SDK
could not apply lent its etag to the next request, and the 304 that answered it
reported the empty committed set as current: initialized, healthy, and with
nothing in a 304 to notice it on.

`is_initialized()` is what authorizes `write_skills("*")` to prune, so that
store read as an environment whose every skill was revoked and deleted the last
known good `SKILL.md` files on disk — the widest version of the hazard the
probe exists to prevent.

Three claims narrowed to what actually happened:

- `payload-transferred` reports a commit only when it applied a pending set. A
  `none` intent builds none, nor does an `intentCode` this SDK does not
  recognise, and a foreign payload's contents are declined — those transfers
  now report nothing, where they reported a commit. They no longer adopt the
  selector of a payload that was never applied either, which on its own left a
  store resuming from content it did not hold while every diagnostic read
  healthy.
- A poll adopts the response `ETag` only from a body that completed an
  exchange: a committed payload, or a `none` intent, which is the server saying
  the content held is what the etag describes. An unrecognised intent says the
  opposite — the body carried objects this reader dropped — so leaving the poll
  unconditional is also what keeps that body arriving and visible instead of
  silenced behind a 304.
- A 304 no longer publishes a first payload. It confirms the payload the store
  holds; the exchange it stands in for, the `none` intent, does not publish one
  either.

TESTING.md §3.25 defines `is_initialized()` as true "once a payload has
committed", and §3.21/§3.22 turn prune suppression on it, so none of this is a
new rule — it is the existing one reaching the poll path. The test that pinned
the old behaviour justified it as "a reconnect with a cached basis", which this
transport has no mechanism for: `_basis` and `_etag` both start as `None` with
no injection point, so a 304 reaching a store that holds nothing takes a server
answering a request that carried no etag at all. It now fails closed, and the
test asserts that.

The over-cap poll test was leaning on the same path for its "delivery carries
on" assertion; it now gets a real payload on the retry, which proves more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three blocks the previous commit added were arguing the whole case inline —
the prune chain, the forward-compatibility reasoning, the cross-language note —
in 50-odd lines of comment on a 14-line change. This source gets read by
customers and their agents working out how to consume the SDK, and that much
nuance about internals they cannot reach is a wall to read past rather than
help.

Each block now states the rule and the one consequence that is visible from
outside: `is_initialized()` / `isInitialized()` goes true on a commit, and that
is what authorizes a prune of the files on disk. The long version already has
two homes it belongs in — `agents.md` in this package, and TESTING.md §3.25 —
so nothing is lost, and the clause that stops a plausible wrong fix ("not up to
date either: only the `none` intent says that") is kept.

Comments only; no behaviour change. Tests untouched, where the long-form
reasoning is the point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@XieX
XieX marked this pull request as ready for review October 2, 2026 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants