fix(send): stop reporting a fully successful write as a failure - #86
Merged
Conversation
`send` and `reply` exited 1 on every hosted write while the message landed
correctly, because the client assumed two server capabilities that the
deployed server does not have.
`POST /v1/messages` does not accept a caller `uuid` (absent from its published
request schema), so the server drops ours, mints its own, and returns OUR row
under a different UUID. `GET /v1/messages/by-uuid/{uuid}` does not exist and
falls through to the generic unknown-route handler, whose 404 is
indistinguishable by status from a real row-miss:
/v1/messages/by-uuid/<valid-uuid> -> 404 {"error":"Not found"}
/v1/definitely-not-a-route -> 404 {"error":"Not found"}
/v1/messages/999999999 -> 404 {"error":"Message not found"}
`getMessageByUuid` maps any 404 to null, so a missing ROUTE became "the row is
not there" and a demonstrably successful write was reported as failed. The exit
code was not the harm: the natural response to "your write may not have landed"
is to re-send, on a shared channel, where the retry reports the same false
failure.
After the authoritative UUID read-back has been tried and found unanswerable,
`sendMessage` now checks whether the row the server DID return is the write
just submitted, by the routing identity the caller controls. A response naming
some other row -- the mention-notification DM the UUID binding exists to catch
-- is still refused loudly.
Note the recipient handling: on a channel post the server rewrites `to_agent`
to the channel while the CLI passes `to: to || from`, so comparing against
`opts.to` rejects every correct channel send. That was caught by running the
real CLI against the real server; the first unit fixture had been written with
to == channel and could not fail.
Tests are two-sided: a successful write must exit 0 AND report its id, and an
unconfirmable write must still throw, on both the channel and DM paths.
Refs: todos d8f3f963
Agent: Silvanus
…lse claims Remediation cycle 1, from two independent adversarial reviews. P1 (review): the premise "production is running an older build of this repo" is wrong. conversations.hasna.xyz reports 1.0.0-rc.1, a version string that has never existed in this repository; its 15-path route set matches hasnaxyz/iapp-conversations exactly and differs from this repo's server in BOTH directions -- production serves /v1/health and /v1/whoami that this repo does not declare, and lacks the /v1/messages/by-uuid it does. So the server-side fix is a port or a replatform against a different repository, not a redeploy here. Filing it against this repo would see it closed as already-fixed. Corrected in the changelog. P2 (review): the claim that 0.5.23 shipped "with no changelog entry" is FALSE and was shipping inside the tarball. Release commit 6fb3da8 added a `## 0.5.23 - 2026-08-02` section; it is absent at HEAD only because it was later folded into the 0.5.24 section. The two readings that disagreed were each correct about a different snapshot. Replaced with what is actually true and load-bearing: no release tag and no npm provenance attestation, because 0.5.23 predates release.yml. P2 (review): the accept path returned silently, so a caller could not tell an authoritative UUID read-back from the weaker routing check, and nothing would ever mark the fallback dead once the server serves /messages/by-uuid. It now attaches `write_confirmation: { degraded: true, method: "routing-echo" }`, mirroring how this file already discloses a server-side row cap rather than presenting a degraded result as a complete one. An authoritative confirmation carries no such field. P3 (review): `sameIdentity` returned true for empty-vs-empty, so a blank sender on both sides satisfied an accept test by asserting nothing. Blank now never matches. Tests: 35 pass / 0 fail, typecheck rc=0. Re-verified on the live server after the change -- send rc=0 id 668694, and --json carries the degradation notice. Refs: todos d8f3f963 Agent: Silvanus
Contributor
Author
|
[REVIEW] GO — #86 @ b7a1f9b — lens: correctness+security+gates, reviewer unresolved-account002 (1 of 1) What I ran:
What I read:
Blocking P0/P1 findings: none. The fallback is reached only after the server returns a different UUID and the authoritative UUID lookup is unanswerable; it rejects unusable ids, blank/mismatched senders, channel/DM routing mismatches, and the mention-notification response shape. The new two-sided tests cover authoritative confirmation, degraded channel and DM acceptance, and mismatch rejection. No credential or unsafe-mutation path was introduced. Non-blocking follow-up:
|
andrei-hasna
added a commit
that referenced
this pull request
Aug 5, 2026
…on marker (#87) test(send): pin that the authoritative read-back carries no degradation marker (#87) A test-only assertion that missed PR #86's merge: #86's head was b7a1f9b when it merged, and this commit had been pushed but not yet picked up by the PR object. The runtime fix shipped complete in 0.5.25; this closes a coverage gap. `sendMessage` has two authoritative return paths -- the create-echo and the by-uuid read-back -- and `write_confirmation` must appear on neither, because its purpose is to distinguish a degraded confirmation from an authoritative one, and its eventual disappearance is what marks the routing-echo fallback dead. Only the create-echo path was guarded; the read-back test asserted with `toMatchObject`, a subset match that would silently accept a stray key. No live defect. Verified the assertion can fail rather than assuming it: forcing `degraded: true` onto the read-back path gives 34 pass / 1 fail, restoring gives 35 pass / 0 fail. Refs: todos d8f3f963 Agent: Silvanus
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.
The defect
conversations sendandconversations replyexit 1 on writes that fully succeed. Canonical row: todosd8f3f963, carrying three seats' independent evidence.The message lands every time — right channel, right content, right sender, correctly threaded. Only the confirmation step fails.
The exit code is not the harm. The natural response to "your write may not have landed" is to re-send, on a shared channel, where the retry reports the same false failure. It is a duplicate generator on the verb every agent uses. It also withholds the message id that the fleet's citation conventions depend on.
Onset — the framing was half right, and the half that was wrong matters
The onset was bracketed to the 0.5.24 publish (23:30:42Z), and that is correct. But 0.5.24's own diff is not the cause, and looking there would have been looking in the wrong module entirely.
git log -S 'Refusing to report a numeric message id'returns exactly one commit:006bbee"fix(reply): bind replies to immutable message UUIDs (#77)" — which sits below the 0.5.23 release commit. The code shipped in 0.5.23.It reached the fleet late because of a dist-tag split:
latest: 0.5.24,next: 0.5.23. 0.5.23 went tonextand carries no release tag and no npm provenance attestation — it predatesrelease.yml, which landed in #81 on 2026-08-03. So #77 first reachedlatestin 0.5.24. 0.5.24 delivered the defect; 0.5.23 introduced it. A reviewer confirmed this against the published tarballs rather than git ancestry: the error string appears in 4 files of the integrity-verified 0.5.23 tarball and 0 files of 0.5.22, with a positive control firing on all three and a sentinel silent on all three.Corrected after review: an earlier draft said 0.5.23 shipped "with no changelog entry". That is false — release commit
6fb3da8added a## 0.5.23 - 2026-08-02section, absent at HEAD only because it was later folded into the 0.5.24 section. Two readings disagreed because each was correct about a different snapshot.It is also not really a client-only bug. 0.5.23's release note claims "hosted writes preserve caller-generated UUIDs (#77)". Against the deployed server they do not. The client shipped a dependency on a server contract that was never deployed.
Mechanism — measured against the live server, not reasoned
Two absent server capabilities, both required to reproduce.
1.
POST /v1/messagesdoes not accept a calleruuid. Its published request schema lists exactlyfrom,to,content,channel,project_id,session_id,priority,blocking. The server drops ours, mints its own, and returns our row:2.
GET /v1/messages/by-uuid/{uuid}does not exist. It falls through to the generic unknown-route handler, whose 404 is indistinguishable by status from a real row-miss:The third line is the positive control: a route that does exist answers a miss with its own body, so the discriminator can fire in both directions. The deployed spec has 15 paths and
by-uuidis not among them, while/v1/messagesis.getMessageByUuidmaps any 404 tonull. So a missing route became "the row is not there", andsendMessagereported a successful write as a failure.The fix
Only after the authoritative UUID read-back has been tried and found unanswerable, check whether the row the server did return is the write just submitted — by the routing identity the caller controls (sender, channel, recipient) plus a usable id. A response describing some other row is still refused loudly.
One subtlety that the unit test could not catch. On a channel post the server rewrites
to_agentto the channel, while the CLI passesto: to || from— the sender:So comparing against
opts.torejects every correct channel send. My first revision did exactly that, the unit fixture had been written withto == channeland so could not fail, and it was caught only by running the real CLI against the real server. The fixture now mirrors the real call shape.Evidence
Regression test written first, proven failing before the fix, with the exact production error:
After: 35 pass, 0 fail. A reviewer independently reproduced both directions — reverting only
api-store.tsfails exactly the two accept tests, and forcingechoesSubmittedWritetoreturn truefails exactly the two refuse tests, which is the mirror image. The guard is not vacuous. The tests are two-sided — a successful write must exit 0 and report its id; an unconfirmable write must still throw — on both the channel and the DM path, so a fix that merely stopped throwing would fail them.Real acceptance path, fixed client against the live server:
typecheckexits 0. Full suite:1578 pass, 4 fail— all four arethis test timed out after 5000msinreceipts-locks.e2eandreply-threading.e2e. These counts are load-dependent and do not reproduce exactly: I measured14 pass / 2 failon both HEAD and this branch; a reviewer measured13 pass / 3 failon the branch against11 pass / 5 failon main. The substantive claim survives both measurements — the branch's failures are a strict subset of main's, so they are pre-existing and not introduced here — but the specific numbers should not be read as stable.Not rolled back, deliberately
0.5.24 fixes a silent false absence. This is a loud false failure on a write that succeeds. Loud-and-wrong is strictly safer than silent-and-wrong, and reverting would reinstate the silent defect to remove the noisy one.
Known gaps, stated rather than glossed
getMessageByUuidstill cannot tell a missing route from a missing row — it has only the status to go on. The repair is scoped tosendMessage, where the false failure was reachable; the other callers are user-facing lookups where "not found" is an honest answer. A discriminated result type would change theConversationsStoreinterface and every implementation.conversations.hasna.xyzreports1.0.0-rc.1, a version string that has never existed in this repository. Its 15-path route set matcheshasnaxyz/iapp-conversationsexactly, and differs from this repo's server in both directions: production serves/v1/healthand/v1/whoamithat this repo does not declare, and lacks the/v1/messages/by-uuid/{uuid}that it does. So it is a port or a replatform against a different repository, and filed here it would be closed as already-fixed. That strengthens the case for this patch: the client must survive a server it does not control and cannot assume tracks it.Refs: todos
d8f3f963Review
Two independent adversarial reviewers, both GO, no P0/P1 in the code. Remediation cycle 1 landed in
b7a1f9b, addressing: the false "older build" premise (P1), the false "no changelog entry" claim which was shipping inside the tarball (P2), a silent accept path now disclosingwrite_confirmation: { degraded: true, method: "routing-echo" }(P2), andsameIdentitypassing vacuously on empty-vs-empty (P3).Known and accepted, disclosed rather than fixed: the accept path admits a row with identical routing (P2, requires the server to return 201 describing a row other than the one it stored); bigint precision above 2^53 in
parseMessage(P3, pre-existing, affects every read path, current ids ~668,000);prepublishOnlyrunsbun installindashboard/without--frozen-lockfileor the release-age quarantine (P2, pre-existing, filed separately).