Conversation
…ls yet" Delivery now narrows a connection to the payload kinds it declares and defaults to flags (launchdarkly/streamer#4730), so the store has to ask for the agent-skill payload or receive the environment's flags and no skills at all. The declaration goes on every request, before any basis exists as well as alongside one: it selects what the connection is served rather than describing what the store already holds. It also fixes something that was already wrong. A skill-enabled environment assigns two payloads, so the reader has been warning about the second and reading only the first intent, and the flag payload was re-downloaded and discarded on every reconnect because its basis was never adopted. Declaring one kind makes the connection single-payload, which is the shape the reader is built for. The 422 that comes with it is the interesting half. It is the answer when the credential is assigned no agent-skill payload, which is every project where no skill has ever been created -- gonfalon creates that row with the first skill and never lazily. As an ordinary recoverable failure it would spend max_consecutive_failures and then report "gave up after N consecutive failures: HTTP 422" for an ordinary configuration; as a fatal one, the skill created a minute later would never arrive without a process restart. So it is its own class: _NoSkillPayloadError, caught ahead of _RecoverableTransportError, said once, counted under the new payload_unavailable diagnostic, retried at max_backoff indefinitely, and kept off connection_failures, last_error and failed. The retry is at the cap because _failures deliberately never moves, so the exponential schedule would otherwise sit at the initial delay forever. Nine tests, each of which fails with the source reverted: the declaration on both endpoints and on a from-scratch retry, the two kind constants held apart, the 422's classification, that it never stops delivery and never counts, that it waits the cap and not the initial delay, that a skill arriving after it is picked up, and that it leaves the store uninitialised so a wildcard reconcile prunes nothing. The fake endpoint gained a standing default status, since "every request is answered 422" is not something a queue can express. Also corrects docs that described the payload as classified `generic` and the request as carrying `mv`. Gate from python/: make test (1888 passed, 11 skipped), typecheck, lint, format-check all clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"The answer for a project in which no skill has ever been created" appeared seven times in one file: on the diagnostic, on the exception class, in _classify_status, on the once-per-store flag, and twice in the delivery loop. _NoSkillPayloadError now owns the explanation, since it is what the other sites refer to, and each of those states only what is local to it: the diagnostic names the type and keeps the "not a connection_failures" distinction, the except block keeps why it is caught first and why it waits the cap, and _classify_status keeps nothing -- the type it returns and the message it builds already say it twice over. No behaviour change, and the user-facing 422 message is untouched. 15 lines of comment removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
XieX
added a commit
to launchdarkly/js-ai-sdk
that referenced
this pull request
Sep 24, 2026
Mirrors the Python trim (launchdarkly/python-ai-sdk#109): the same sentence had been repeated at every site that touches the 422, so NoSkillPayloadError now owns the explanation and the others state only what is local to them — the diagnostic links the type and keeps the "not a connectionFailures" distinction, the loop keeps why it waits the cap, and classifyStatus keeps nothing, since the type it returns and the message it builds already say it twice over. No behaviour change, and the user-facing 422 message is untouched. 12 lines of comment removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Base automatically changed from
xie/python-skills-revoke-by-omission
to
xie/python-agent-skills-review-fixes
September 24, 2026 19:33
Base automatically changed from
xie/python-agent-skills-review-fixes
to
xie/agent-skills
September 24, 2026 19:33
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.
Stacks on #94 -> #93 -> #87. Spec: ai-sdks-monorepo#23. TypeScript counterpart: js-ai-sdk#87. Server side: streamer#4730.
This is the SDK half of the release blocker: other SDKs retry forever when they see a skill payload. Flag Delivery's answer is
?kinds=, which narrows a connection to the payload kinds it declares, defaulting to{flagging}(so we need to opt-in to see skills).The declaration
Every request now carries
kinds=agent-skill— both endpoints, and on the first request as well as the ones after it, since it selects what the connection is served rather than describing what the store already holds. Without it the store receives the environment's flag payload and no skills at all, so this is a functional requirement, not for correctness/optimization.It also fixes something that was already wrong. A skill-enabled environment assigns two payloads, so
_ProtocolReaderhas been warning about the second on every connection, reading only the first intent, and never adopting a basis for the flag payload — re-downloading and discarding it on every reconnect. Declaring one kind makes the connection single-payload, which is the shape the reader is built for. (That is also why declaringflagging,agent-skillis not the safe-looking option it appears to be.)No
mv, still — but for a corrected reason. It selects the flag data model, andobjectQueryForCommandoverrides whatever a request asks for with the payload's own default for any non-flagging payload, so sending it would state a preference that is ignored. The old comment said the connection would be refused over it.The 422
Delivery answers 422
no_accepted_payloadswhen the credential's assignment holds no payload of the declared kind. That is every project in which no skill has ever been created: gonfalon'spayloadIDsForEnvironmentappends the agent-skill payload ID only "if one already exists" and never lazily creates it.Both obvious classifications are wrong. Reverting just the 422 case and rerunning the new tests prints exactly what the recoverable reading gives you:
...for an environment whose only problem is that nobody has made a skill yet. Fatal is no better: the skill created a minute later never arrives until the process restarts.
So
_NoSkillPayloadError, caught ahead of_RecoverableTransportError(which it subclasses, so nothing else has to change):StoreDiagnostics.payload_unavailable;max_backoffindefinitely — at the cap because_failuresdeliberately never moves, so the exponential schedule would otherwise sit at the initial delay forever;connection_failures,last_errorandfailed.It commits no payload, so
is_initialized()stays false andwrite_skills("*")still withholds the prune. That is the right answer: "LaunchDarkly has no skill payload for this environment" and "this environment's every skill was revoked" are the two readings of an empty answer, and only the second may delete a customer's files.Tests
Nine, each of which fails with the source reverted:
/sdk/poll(first request and the one carrying a basis) and on/sdk/stream, and on the from-scratch 400 retry — where the two existingquery == {}assertions became{"kinds": ...}, which is a better assertion: the declaration is not client state;FDV2_PAYLOAD_KINDandFDV2_OBJECT_KINDheld apart;max_consecutive_failures=1neither stops delivery nor counts, and is said exactly once;_stopso the assertion is on the interval asked for, not on wall-clock timing);The fake endpoint gained
default_poll_status, since "every request is answered 422" is not something a queue can express.Gate
From
python/:make test(1888 passed, 11 skipped),make typecheck(clean, 51 source files),make lint,make format-check— all clean.🤖 Generated with Claude Code, edited by @XieX
Note
Overview
FDv2 skill delivery now opts into the agent-skill payload and treats “no payload yet” as idle, not broken.
Every poll/stream URL includes
?kinds=agent-skill(withbasiswhen present) so connections receive the skill payload instead of default flag data, and stay single-payload for_ProtocolReader. HTTP 422 (no assigned agent-skill payload—typical before the first skill exists) maps to_NoSkillPayloadError: retry forever atmax_backoff, incrementStoreDiagnostics.payload_unavailable, log once, and do not touchconnection_failures,last_error, orfailed. Because 422 commits nothing,is_initialized()stays false and wildcardwrite_skillsstill avoids pruning stale files.Docs (
README.md,agents.md) describe the waiting state vs delivery failure; tests coverkindson poll/stream/400-retry, 422 classification and backoff, recovery after skills appear, and prune safety.Reviewed by Cursor Bugbot for commit b4f29a8. Bugbot is set up for automated code reviews on this repo. Configure here.