fix(claw): read a blank since as unset instead of failing the request

The phone's notification poll sent `since` unconditionally, and before
the first ack there is no cursor at all, so it went out as `?since=`.
Axum's `Query` hands a present-but-empty parameter through as an empty
string, and `Option<i64>` only forgives a *missing* key, so the whole
poll answered `400 Failed to deserialize query string: since: cannot
parse integer from empty string` and the phone showed an error instead
of an empty queue.

Both ends: the client omits `since` unless it has a positive id to send,
and a `blank_as_none` deserializer makes a blank value mean "not set"
for `NotificationQuery` and `EventStreamQuery` alike — an empty `since`
reads as "from the start" however it got there.
This commit is contained in:
kosmos committed 2026-11-06 20:39:53 +00:00
1 parent 3283d92227
commit 9e132fdf71
2 files changed
+48 -4

No files matched your search

@@ -138,12 +138,21 @@ class ClawApi(
return execute(builder.delete().build()) { }
}
// GET /v1/notifications?device=&since=N. `since` is the last id the
// device has acked; before the first ack there is no cursor at all,
// and an empty `since=` came back
// "HTTP 400 — Failed to deserialize query string: since: cannot
// parse integer from empty string", which failed the whole poll.
// So the parameter is sent only when there is a positive id to send:
// absent and blank mean the same thing to the claw, and the shorter
// request is the honest one.
suspend fun notifications(deviceId: String, since: String): ClawResult<List<RhemaNotification>> {
val base = baseOrNull() ?: return ClawResult.Network("no base URL configured")
val cursor = since.trim().toLongOrNull()?.takeIf { it > 0 }
val url = base.toHttpUrlOrNull()?.newBuilder()
?.addPathSegments("v1/notifications")
?.addQueryParameter("device", deviceId)
?.addQueryParameter("since", since)
?.apply { cursor?.let { addQueryParameter("since", it.toString()) } }
?.build()
?: return ClawResult.Network("bad base URL")
val request = Request.Builder().url(url).header("Authorization", bearer()).build()