feat: persist WebSocket session across reconnections

Android generates a stable client ID (UUID) stored in SharedPreferences
and sends it as a query parameter on WS connect. The server uses this
ID as the chat session key, so AI conversation context survives app
restarts. On reconnection, the server closes the stale connection and
replaces it with the new one.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kohei 2026-02-18 22:31:36 +09:00
parent 96f559abbb
commit b466e3116f
3 changed files with 20 additions and 4 deletions

View file

@ -23,6 +23,7 @@ import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext
import org.koin.core.module.dsl.viewModel
import org.koin.dsl.module
import java.util.UUID
import java.util.concurrent.TimeUnit
val appModule = module {
@ -52,7 +53,13 @@ val appModule = module {
}
// WebSocketClient
single { WebSocketClient(get(), get()) }
single {
val prefs = androidContext().getSharedPreferences("picoclaw", android.content.Context.MODE_PRIVATE)
val clientId = prefs.getString("client_id", null) ?: UUID.randomUUID().toString().also {
prefs.edit().putString("client_id", it).apply()
}
WebSocketClient(get(), get(), clientId)
}
// ImageFileStorage
single { ImageFileStorage(androidContext()) }

View file

@ -25,7 +25,8 @@ import kotlinx.serialization.json.Json
class WebSocketClient(
private val client: HttpClient,
private val scope: CoroutineScope
private val scope: CoroutineScope,
private val clientId: String
) {
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
@ -47,7 +48,8 @@ class WebSocketClient(
while (isActive) {
try {
_connectionState.value = ConnectionState.CONNECTING
client.webSocket(wsUrl) {
val url = "$wsUrl?client_id=$clientId"
client.webSocket(url) {
session = this
_connectionState.value = ConnectionState.CONNECTED
retryDelay = INITIAL_DELAY

View file

@ -166,7 +166,10 @@ func (c *WebSocketChannel) handleWS(w http.ResponseWriter, r *http.Request) {
return
}
clientID := uuid.New().String()
clientID := r.URL.Query().Get("client_id")
if clientID == "" {
clientID = uuid.New().String()
}
logger.InfoCF("websocket", "New WebSocket connection", map[string]interface{}{
"client_id": clientID,
@ -176,6 +179,10 @@ func (c *WebSocketChannel) handleWS(w http.ResponseWriter, r *http.Request) {
chatID := fmt.Sprintf("ws:%s", clientID)
c.mu.Lock()
if oldConn, ok := c.chatConns[chatID]; ok {
delete(c.clients, oldConn)
oldConn.Close()
}
c.clients[conn] = clientID
c.chatConns[chatID] = conn
c.mu.Unlock()