fix(android): honor session control reset semantics
This commit is contained in:
@@ -53,6 +53,12 @@ intentionally remain outside that code batch:
|
||||
`docs/upstream-contributions.md`. Until then, document the narrow race where a
|
||||
disconnect or Stop after `/model --once` succeeds but before prompt submission
|
||||
can leave the override armed for a later prompt.
|
||||
- Keep HRUI-052 (`/new` session-control reset parity) blocked until upstream
|
||||
exposes a reset on the active gateway session or an authoritative reset event.
|
||||
`slash.exec` runs the command in a separate worker today, and the mirrored
|
||||
slash side effects do not reset the active TUI session's agent. Relay must not
|
||||
clear local model, reasoning, or Fast pins from a successful command response
|
||||
that did not mutate the agent those controls describe.
|
||||
- Upstream the first-class commentary event and profile-scoped cron execution
|
||||
attempts API before adding Relay client parsers for those surfaces.
|
||||
- Keep Standard voice labeled host-global until upstream exposes a stable
|
||||
|
||||
@@ -314,7 +314,9 @@ data class GatewayModelOptions(
|
||||
*
|
||||
* [model] is the model id (e.g. `grok-4.3`); [provider] is the authenticated
|
||||
* provider slug (e.g. `xai`). [reasoningEffort] is the upstream effort string
|
||||
* (`low`/`medium`/`high`/…). [fast] pins the priority service tier when true.
|
||||
* (`low`/`medium`/`high`/…). [fast] follows the contract-v4 tri-state: `true`
|
||||
* pins priority, `false` explicitly pins normal, and `null` omits the field so
|
||||
* the profile's service tier is inherited.
|
||||
* Note `yolo` is intentionally absent — upstream `session.create` does NOT
|
||||
* accept it as a per-session override, so it is applied post-create instead.
|
||||
*/
|
||||
|
||||
@@ -1092,10 +1092,11 @@ class ChatViewModel : ViewModel() {
|
||||
val model = _selectedModelOverride.value?.takeIf { it.isNotBlank() }
|
||||
val provider = _selectedProviderOverride.value?.takeIf { it.isNotBlank() }
|
||||
val effort = _selectedReasoningEffort.value?.takeIf { it.isNotBlank() }
|
||||
// Only pin fast when explicitly ON (upstream treats fast=false the
|
||||
// same as omitting → profile default tier), so a null/false leaves
|
||||
// the profile's own service tier intact.
|
||||
val fast = _fastEnabled.value?.takeIf { it }
|
||||
// Contract v4 distinguishes all three states: null omits the field
|
||||
// and inherits the profile tier, true pins priority, and false pins
|
||||
// normal. Do not filter false here or a user's explicit Fast-off
|
||||
// pick would silently inherit a priority-by-default profile.
|
||||
val fast = _fastEnabled.value
|
||||
if (model != null || effort != null || fast != null) {
|
||||
GatewaySessionModel(
|
||||
model = model,
|
||||
|
||||
@@ -234,6 +234,10 @@ class GatewayClientHarness(
|
||||
else -> JsonObject(emptyMap())
|
||||
}
|
||||
"config.set" -> when ((params["key"] as? JsonPrimitive)?.contentOrNull) {
|
||||
"model" -> buildJsonObject {
|
||||
put("key", "model")
|
||||
put("value", (params["value"] as? JsonPrimitive)?.contentOrNull ?: "")
|
||||
}
|
||||
"reasoning" -> {
|
||||
reasoningEffort = (params["value"] as? JsonPrimitive)?.contentOrNull ?: reasoningEffort
|
||||
buildJsonObject {
|
||||
@@ -241,6 +245,10 @@ class GatewayClientHarness(
|
||||
put("value", reasoningEffort)
|
||||
}
|
||||
}
|
||||
"fast" -> buildJsonObject {
|
||||
put("key", "fast")
|
||||
put("value", (params["value"] as? JsonPrimitive)?.contentOrNull ?: "normal")
|
||||
}
|
||||
else -> JsonObject(emptyMap())
|
||||
}
|
||||
else -> JsonObject(emptyMap())
|
||||
@@ -1194,6 +1202,20 @@ class GatewayChatClientTest {
|
||||
assertEquals(true, (create["fast"] as? JsonPrimitive)?.booleanOrNull)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `session create binds explicit normal fast tier`() {
|
||||
val r = Recorder()
|
||||
client.sessionModelProvider = {
|
||||
GatewaySessionModel(model = null, provider = null, reasoningEffort = null, fast = false)
|
||||
}
|
||||
client.sendTurn(null, "hi", null, r.callbacks) { r.preflightFailures += it }
|
||||
harness.awaitServerSocket()
|
||||
harness.awaitRpc("prompt.submit")
|
||||
|
||||
val create = harness.awaitRpc("session.create")
|
||||
assertEquals(false, (create["fast"] as? JsonPrimitive)?.booleanOrNull)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `session create omits reasoning_effort and fast when unset`() {
|
||||
val r = Recorder()
|
||||
@@ -1503,6 +1525,43 @@ class GatewayChatClientTest {
|
||||
assertEquals("live-resumed", (rpc["session_id"] as? JsonPrimitive)?.contentOrNull)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `model update targets live session without global scope`() {
|
||||
val r = Recorder()
|
||||
client.sendTurn("stored-1", "hi", null, r.callbacks) { r.preflightFailures += it }
|
||||
harness.awaitServerSocket()
|
||||
harness.awaitRpc("session.resume")
|
||||
harness.awaitRpc("prompt.submit")
|
||||
|
||||
val result = runBlocking { client.setModel("grok-4.3 --provider xai") }
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
val rpc = harness.awaitRpc("config.set")
|
||||
assertEquals("model", (rpc["key"] as? JsonPrimitive)?.contentOrNull)
|
||||
assertEquals("grok-4.3 --provider xai", (rpc["value"] as? JsonPrimitive)?.contentOrNull)
|
||||
assertEquals("live-resumed", (rpc["session_id"] as? JsonPrimitive)?.contentOrNull)
|
||||
assertFalse(rpc.containsKey("scope"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fast update targets live session without global scope`() {
|
||||
val r = Recorder()
|
||||
client.sendTurn("stored-1", "hi", null, r.callbacks) { r.preflightFailures += it }
|
||||
harness.awaitServerSocket()
|
||||
harness.awaitRpc("session.resume")
|
||||
harness.awaitRpc("prompt.submit")
|
||||
|
||||
val result = runBlocking { client.setFast(false) }
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
assertFalse(result.getOrThrow())
|
||||
val rpc = harness.awaitRpc("config.set")
|
||||
assertEquals("fast", (rpc["key"] as? JsonPrimitive)?.contentOrNull)
|
||||
assertEquals("normal", (rpc["value"] as? JsonPrimitive)?.contentOrNull)
|
||||
assertEquals("live-resumed", (rpc["session_id"] as? JsonPrimitive)?.contentOrNull)
|
||||
assertFalse(rpc.containsKey("scope"))
|
||||
}
|
||||
|
||||
// --- Edit & regenerate ---
|
||||
|
||||
@Test
|
||||
|
||||
+19
@@ -139,6 +139,25 @@ class ChatViewModelGatewayInboundTurnTest {
|
||||
assertEquals("victor", gatewayClient.sessionProfileProvider())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun gatewaySessionCreateProviderPreservesExplicitFastFalse() {
|
||||
serverWs.send(
|
||||
gatewayHarness.eventFrame(
|
||||
"session.info",
|
||||
buildJsonObject { put("fast", false) },
|
||||
"live-resumed",
|
||||
),
|
||||
)
|
||||
val deadline = System.currentTimeMillis() + 5_000
|
||||
while (viewModel.fastEnabled.value != false) {
|
||||
shadowOf(Looper.getMainLooper()).idle()
|
||||
if (System.currentTimeMillis() >= deadline) error("fast=false did not reconcile")
|
||||
Thread.sleep(20)
|
||||
}
|
||||
|
||||
assertEquals(false, gatewayClient.sessionModelProvider()?.fast)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dashboardOnlyConnectionCanSendWithoutApiClient() {
|
||||
viewModel.updateGatewayClient(null)
|
||||
|
||||
Reference in New Issue
Block a user