diff --git a/android/core/data/src/main/java/io/picoclaw/android/core/data/repository/AssistantConnectionImpl.kt b/android/core/data/src/main/java/io/picoclaw/android/core/data/repository/AssistantConnectionImpl.kt index 3640da070..d4e5e09af 100644 --- a/android/core/data/src/main/java/io/picoclaw/android/core/data/repository/AssistantConnectionImpl.kt +++ b/android/core/data/src/main/java/io/picoclaw/android/core/data/repository/AssistantConnectionImpl.kt @@ -51,7 +51,6 @@ class AssistantConnectionImpl( "status_end" -> _statusText.value = null "tool_request" -> handleToolRequest(dto.content) else -> { - _statusText.value = null _messages.emit(AssistantMessage(content = dto.content, type = dto.type)) } } diff --git a/android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/assistant/AssistantManager.kt b/android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/assistant/AssistantManager.kt index 72a427499..26921e7fe 100644 --- a/android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/assistant/AssistantManager.kt +++ b/android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/assistant/AssistantManager.kt @@ -190,13 +190,14 @@ class AssistantManager( val statusJob = launch { connection.statusText.collect { label -> - if (label != null) heartbeat.trySend(Unit) + heartbeat.trySend(Unit) } } try { _state.update { it.copy(phase = VoicePhase.THINKING, statusText = null) } + var hasSpoken = false while (true) { val result = select { speechQueue.onReceive { WaitResult.Message(it) } @@ -206,15 +207,21 @@ class AssistantManager( when (result) { WaitResult.Timeout -> { - _state.update { - it.copy(phase = VoicePhase.ERROR, errorMessage = "Response timed out") + if (!hasSpoken) { + _state.update { + it.copy(phase = VoicePhase.ERROR, errorMessage = "Response timed out") + } + delay(2000) } - delay(2000) return@coroutineScope } - WaitResult.Heartbeat -> continue + WaitResult.Heartbeat -> { + if (hasSpoken && connection.statusText.value == null) return@coroutineScope + continue + } is WaitResult.Message -> { speakAndDrain(result.content, speechQueue) + hasSpoken = true val currentStatus = connection.statusText.value if (currentStatus == null) return@coroutineScope _state.update { @@ -231,7 +238,7 @@ class AssistantManager( private suspend fun speakAndDrain(firstContent: String, speechQueue: Channel) { val chunks = mutableListOf(firstContent) try { - _state.update { it.copy(phase = VoicePhase.SPEAKING, responseText = firstContent) } + _state.update { it.copy(phase = VoicePhase.SPEAKING, responseText = firstContent, statusText = null) } ttsWrapper.speak(firstContent) while (true) { val next = speechQueue.tryReceive().getOrNull() ?: break diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 17552b545..7a9cd73b3 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -296,17 +296,9 @@ func (al *AgentLoop) Run(ctx context.Context) error { return } if response != "" { - alreadySent := false - if tool, ok := al.tools.Get("message"); ok { - if mt, ok := tool.(*tools.MessageTool); ok { - alreadySent = mt.HasSentInRound() - } - } - if !alreadySent { - al.bus.PublishOutbound(bus.OutboundMessage{ - Channel: m.Channel, ChatID: m.ChatID, Content: response, - }) - } + al.bus.PublishOutbound(bus.OutboundMessage{ + Channel: m.Channel, ChatID: m.ChatID, Content: response, + }) } }(msg, sessionKey) } diff --git a/pkg/tools/message.go b/pkg/tools/message.go index 21dd332a4..20e1c1a8d 100644 --- a/pkg/tools/message.go +++ b/pkg/tools/message.go @@ -18,7 +18,6 @@ type MessageTool struct { sendCallback SendCallback defaultChannel string defaultChatID string - sentInRound bool // Tracks whether a message was sent in the current processing round enabledChannels []string stateResolver StateResolver } @@ -74,12 +73,6 @@ func (t *MessageTool) SetStateResolver(sr StateResolver) { func (t *MessageTool) SetContext(channel, chatID string) { t.defaultChannel = channel t.defaultChatID = chatID - t.sentInRound = false // Reset send tracking for new processing round -} - -// HasSentInRound returns true if the message tool sent a message during the current round. -func (t *MessageTool) HasSentInRound() bool { - return t.sentInRound } func (t *MessageTool) SetSendCallback(callback SendCallback) { @@ -146,12 +139,6 @@ func (t *MessageTool) Execute(ctx context.Context, args map[string]interface{}) } } - // Only mark as "sent in round" when the message went to the originating channel. - // Cross-channel sends (e.g. WS→Discord) must NOT suppress the response - // back to the sender's channel. - if channel == t.defaultChannel { - t.sentInRound = true - } // Silent: user already received the message directly return &ToolResult{ ForLLM: fmt.Sprintf("Message sent to %s:%s", channel, chatID),