Repository navigation
Conversation
PATCH /v3/channels/voice/{number}'s own docs give agent_id three
states: a value binds an agent, null unbinds it, and omitting the key
leaves the binding alone. VoiceService::update() builds its request
body through Util::removeNulls(), which strips every null field before
the request goes out - so calling update(agentID: null) produces the
same wire request as not passing agentID at all. There's no way to
unbind an agent through this method today.
Give agentID its own default, a new Omit marker (same role as the
Omit/omit sentinel the Python SDK already uses for this exact problem),
instead of null. A real null now survives into the request body;
leaving the parameter out still omits the key. Every other field on
this method keeps going through removeNulls() unchanged.
Confirmed live against the free voice-agent fields that have the same
null-is-meaningful contract (no phone number needed to test): the
typed update() method 400s today when passed an explicit null, while
raw->update() with a plain array already accepts and applies it. This
patch brings the typed method in line with what the API and its own
docblock already promise.
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. |
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.
What's wrong
PATCH /v3/channels/voice/{number}'s own docblock gives
agent_idthree distinct states:"agent_id": "agent_…""agent_id": nullVoiceService::update()builds its request body throughUtil::removeNulls(), which stripsevery null field before the request goes out. So
update(agentID: null)andupdate()(nottouching
agentIDat all) produce the identical wire request. There is no way to unbind anagent through this method today, despite the method's own docs promising one.
Before (current behavior), calling with an explicit null:
After this fix:
Confirmed live (against the free voice-agent fields, which have the identical null-is-meaningful
contract, so no phone number was needed): the typed
update()method 400s("Name at least one field to change") when the only field passed is an explicit null, while
raw->update()with a plain PHP array already accepts and applies an explicit null correctly.The bug is isolated to the typed convenience wrapper, not the underlying request machinery.
This isn't PHP-specific, it's already solved in the Python SDK
Checked the other Sent SDKs before writing this. The Python SDK has the identical
named-parameter architecture (and the identical problem, since Python's
Nonecollapses"omitted" and "explicit None" the same way PHP's
nulldoes) and already solves it with adedicated sentinel:
agent_id: Optional[str] | Omit = omit(sent-dm-python,src/sent_dm/_types.py). This PR gives the PHP SDK the equivalent: a newSentDm\Core\Omitmarker, named to match.
The TypeScript, Ruby, Go, Java, and C# SDKs don't share this failure mode, each for its own
reason:
JSON.stringify, which alreadydistinguishes
undefinedfromnull.update(number, params = {})takes a hash rather than individual namedparameters, so an explicit
nilkey and an absent key aren't the same thing going in.param.Opt[T]has three states (omitted/null/included), withparam.Null[T]()as the explicit-null constructor.
JsonField.ofNullable()returns aJsonNullinstance, distinct from the field'sdefault
JsonMissing.AgentIDis aninit-only property on a record; the setter only runs if the callerexplicitly assigns it, so "never touched" and "assigned null" are different code paths before
serialization even happens.
What changed
agentIDnow defaults toSentDm\Core\Omitinstead ofnull:Omitmarker) → key left out of the body, binding unchangednull→"agent_id": nullsent explicitly, unbinds the agentEvery other field on this method is untouched and still goes through
removeNulls().Is this a breaking change?
Flagging explicitly per this repo's compatibility rule rather than deciding it myself: the
signature's type changes (
?string→string|null|Omit) and, more importantly, the runtimebehavior for an existing caller passing
agentID: nullchanges. Today it's a silent no-op,after this it actually unbinds. No caller could have been relying on the old behavior to do
anything useful (it did nothing), but it is still a different result for the same call, so
this may warrant a minor/major version call rather than a patch. Leaving that to whoever
triages it.
How I verified it
Added
VoiceTest::testUpdateSendsExplicitNullToUnbindAgent(), which inspects the actualoutgoing request body against a mock transporter and confirms
agent_idis present andnull.Confirmed it fails against the code before this change (missing key) and passes after. Added
testUpdateOmitsAgentIdWhenNotPassed()to confirm the common case still omits the key../scripts/lintand./scripts/testboth pass.Note for review
This hand-edits existing generated lines in
VoiceService.php(the$paramsarray and themethod signature) rather than only adding to them, which I know this repo's
CLAUDE.mdflagsas the riskier kind of patch. Given the fix brings generated behavior in line with what the
same generated docblock already documents, and matches a pattern already used elsewhere in
this SDK family, I think it's a reasonable patch. But if the preferred fix is at the generator
config level (teaching it to emit the same
Omit-style sentinel PHP-side that Python alreadygets), that's outside what I can reach from here.