fix(sessions): tolerate integer and string boolean formats for pinned field (#966) (#967)

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-08-25 17:05:14 +03:00
1 parent ece0288ca8
commit 9217f9f8b6
2 files changed
+98

No files matched your search

@@ -1,5 +1,60 @@
package com.m57.hermescontrol.data.model
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.intOrNull
/**
* Lenient boolean serializer that decodes both JSON booleans (`true`/`false`)
* and raw SQLite numeric booleans (`1`/`0`), as well as string representations (`"true"`/`"1"`).
*
* This guards against backend versions (e.g. Hermes Agent <= 0.19.0) where database
* boolean flags like `pinned` are returned as raw integer values (issue #966).
*/
@OptIn(ExperimentalSerializationApi::class)
object LenientNullableBooleanSerializer : KSerializer<Boolean?> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("LenientNullableBoolean", PrimitiveKind.BOOLEAN)
override fun deserialize(decoder: Decoder): Boolean? {
if (decoder is JsonDecoder) {
val element = decoder.decodeJsonElement()
if (element is JsonPrimitive) {
element.booleanOrNull?.let { return it }
element.intOrNull?.let { return it != 0 }
val str = element.content.trim().lowercase()
return when (str) {
"true", "1" -> true
"false", "0" -> false
"null", "" -> null
else -> null
}
}
return null
}
return decoder.decodeBoolean()
}
override fun serialize(
encoder: Encoder,
value: Boolean?,
) {
if (value == null) {
encoder.encodeNull()
} else {
encoder.encodeBoolean(value)
}
}
}
@Serializable
data class SessionListResponse(
@@ -25,6 +80,8 @@ data class SessionInfo(
val terminal_backend: String? = null,
// Durable "keep" flag: pinned sessions are exempt from the auto-archive
// sweep and are surfaced first in the history list (sorted client-side).
// Uses LenientNullableBooleanSerializer to tolerate both JSON booleans and raw SQLite ints (issue #966).
@Serializable(with = LenientNullableBooleanSerializer::class)
val pinned: Boolean? = null,
)