11package ai.openclaw.app
22
3+ import ai.openclaw.app.chat.ChatCacheDatabase
34import ai.openclaw.app.chat.ChatCacheScope
45import ai.openclaw.app.chat.ChatCommandEntry
56import ai.openclaw.app.chat.ChatController
67import ai.openclaw.app.chat.ChatMessage
8+ import ai.openclaw.app.chat.ChatOutboxItem
79import ai.openclaw.app.chat.ChatPendingToolCall
810import ai.openclaw.app.chat.ChatSessionEntry
911import ai.openclaw.app.chat.OutgoingAttachment
12+ import ai.openclaw.app.chat.RoomChatCommandOutbox
1013import ai.openclaw.app.chat.RoomChatTranscriptCache
1114import ai.openclaw.app.gateway.DeviceAuthEntry
1215import ai.openclaw.app.gateway.DeviceAuthStore
@@ -840,8 +843,14 @@ class NodeRuntime(
840843 }
841844 }
842845
846+ // One Room handle owns both chat stores (disposable transcript cache + durable command outbox).
847+ private val chatCacheDatabase: ChatCacheDatabase = ChatCacheDatabase .open(appContext)
848+
843849 private val chatTranscriptCache: RoomChatTranscriptCache =
844- RoomChatTranscriptCache (context = appContext)
850+ RoomChatTranscriptCache (database = chatCacheDatabase)
851+
852+ private val chatCommandOutbox: RoomChatCommandOutbox =
853+ RoomChatCommandOutbox (database = chatCacheDatabase)
845854
846855 private val chat: ChatController =
847856 ChatController (
@@ -850,6 +859,7 @@ class NodeRuntime(
850859 json = json,
851860 transcriptCache = chatTranscriptCache,
852861 cacheScope = ::chatCacheScope,
862+ commandOutbox = chatCommandOutbox,
853863 ).also {
854864 it.applyMainSessionKey(_mainSessionKey .value)
855865 }
@@ -1268,14 +1278,16 @@ class NodeRuntime(
12681278 fun setGatewayPassword (value : String ) = prefs.setGatewayPassword(value)
12691279
12701280 /* * Clears setup credentials plus paired device tokens for both Android gateway roles. */
1271- fun resetGatewaySetupAuth () {
1281+ suspend fun resetGatewaySetupAuth () {
12721282 prefs.clearGatewaySetupAuth()
12731283 val deviceId = identityStore.loadOrCreate().deviceId
12741284 deviceAuthStore.clearToken(deviceId, " node" )
12751285 deviceAuthStore.clearToken(deviceId, " operator" )
12761286 // A pairing/auth reset can precede pairing a different gateway principal at the same
1277- // endpoint id; purge offline transcripts so they cannot surface under the new pairing.
1278- scope.launch { runCatching { chatTranscriptCache.clearAll() } }
1287+ // endpoint id. The purge must complete before pairing continues: queued commands are
1288+ // replayed on reconnect, so an async purge could flush stale rows to the new principal.
1289+ runCatching { chatTranscriptCache.clearAll() }
1290+ runCatching { chatCommandOutbox.clearAll() }
12791291 }
12801292
12811293 /* * Persists onboarding state; callers decide whether runtime startup is needed first. */
@@ -1310,6 +1322,11 @@ class NodeRuntime(
13101322 val chatSessions: StateFlow <List <ChatSessionEntry >> = chat.sessions
13111323 val pendingRunCount: StateFlow <Int > = chat.pendingRunCount
13121324 val chatCommands: StateFlow <List <ChatCommandEntry >> = chat.commands
1325+ val chatOutboxItems: StateFlow <List <ChatOutboxItem >> = chat.outboxItems
1326+
1327+ fun retryChatOutboxCommand (id : String ) = chat.retryOutboxCommand(id)
1328+
1329+ fun deleteChatOutboxCommand (id : String ) = chat.deleteOutboxCommand(id)
13131330
13141331 init {
13151332 if (prefs.voiceWakeMode.value != VoiceWakeMode .Off ) {
0 commit comments