From 5c95d7cdf6a3218276e29743486502f9146746b4 Mon Sep 17 00:00:00 2001 From: clawbot Date: Tue, 10 Feb 2026 18:22:08 -0800 Subject: [PATCH] fix: CLI poll loop used UUID instead of queue cursor (last_id) The poll loop was storing msg.ID (UUID string) as afterID, but the server expects the integer queue cursor from last_id. This caused the CLI to re-fetch ALL messages on every poll cycle. - Change PollMessages to accept int64 afterID and return PollResult with LastID - Track lastQID (queue cursor) instead of lastMsgID (UUID) - Parse the wrapped MessagesResponse properly --- cmd/chat-cli/api/client.go | 30 ++++++++++++------------------ cmd/chat-cli/api/types.go | 7 +++++++ cmd/chat-cli/main.go | 21 +++++++++++---------- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/cmd/chat-cli/api/client.go b/cmd/chat-cli/api/client.go index a20298c..42334d2 100644 --- a/cmd/chat-cli/api/client.go +++ b/cmd/chat-cli/api/client.go @@ -97,21 +97,18 @@ func (c *Client) SendMessage(msg *Message) error { return err } -// PollMessages long-polls for new messages. -func (c *Client) PollMessages(afterID string, timeout int) ([]Message, error) { +// PollMessages long-polls for new messages. afterID is the queue cursor (last_id). +func (c *Client) PollMessages(afterID int64, timeout int) (*PollResult, error) { // Use a longer HTTP timeout than the server long-poll timeout. client := &http.Client{Timeout: time.Duration(timeout+5) * time.Second} params := url.Values{} - if afterID != "" { - params.Set("after", afterID) + if afterID > 0 { + params.Set("after", fmt.Sprintf("%d", afterID)) } params.Set("timeout", fmt.Sprintf("%d", timeout)) - path := "/api/v1/messages" - if len(params) > 0 { - path += "?" + params.Encode() - } + path := "/api/v1/messages?" + params.Encode() req, err := http.NewRequest("GET", c.BaseURL+path, nil) if err != nil { @@ -134,18 +131,15 @@ func (c *Client) PollMessages(afterID string, timeout int) ([]Message, error) { return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(data)) } - // The server may return an array directly or wrapped. - var msgs []Message - if err := json.Unmarshal(data, &msgs); err != nil { - // Try wrapped format. - var wrapped MessagesResponse - if err2 := json.Unmarshal(data, &wrapped); err2 != nil { - return nil, fmt.Errorf("decode messages: %w (raw: %s)", err, string(data)) - } - msgs = wrapped.Messages + var wrapped MessagesResponse + if err := json.Unmarshal(data, &wrapped); err != nil { + return nil, fmt.Errorf("decode messages: %w (raw: %s)", err, string(data)) } - return msgs, nil + return &PollResult{ + Messages: wrapped.Messages, + LastID: wrapped.LastID, + }, nil } // JoinChannel joins a channel via the unified command endpoint. diff --git a/cmd/chat-cli/api/types.go b/cmd/chat-cli/api/types.go index 1655d79..9e7f9a0 100644 --- a/cmd/chat-cli/api/types.go +++ b/cmd/chat-cli/api/types.go @@ -71,6 +71,13 @@ type ServerInfo struct { // MessagesResponse wraps polling results. type MessagesResponse struct { Messages []Message `json:"messages"` + LastID int64 `json:"last_id"` +} + +// PollResult wraps the poll response including the cursor. +type PollResult struct { + Messages []Message + LastID int64 } // ParseTS parses the message timestamp. diff --git a/cmd/chat-cli/main.go b/cmd/chat-cli/main.go index 6dfa1f3..95713a6 100644 --- a/cmd/chat-cli/main.go +++ b/cmd/chat-cli/main.go @@ -19,7 +19,7 @@ type App struct { nick string target string // current target (#channel or nick for DM) connected bool - lastMsgID string + lastQID int64 // queue cursor for polling stopPoll chan struct{} } @@ -142,7 +142,7 @@ func (a *App) cmdConnect(serverURL string) { a.client = client a.nick = resp.Nick a.connected = true - a.lastMsgID = "" + a.lastQID = 0 a.mu.Unlock() a.ui.AddStatus(fmt.Sprintf("[green]Connected! Nick: %s, Session: %s", resp.Nick, resp.SessionID)) @@ -462,27 +462,28 @@ func (a *App) pollLoop() { a.mu.Lock() client := a.client - lastID := a.lastMsgID + lastQID := a.lastQID a.mu.Unlock() if client == nil { return } - msgs, err := client.PollMessages(lastID, 15) + result, err := client.PollMessages(lastQID, 15) if err != nil { // Transient error — retry after delay. time.Sleep(2 * time.Second) continue } - for _, msg := range msgs { + if result.LastID > 0 { + a.mu.Lock() + a.lastQID = result.LastID + a.mu.Unlock() + } + + for _, msg := range result.Messages { a.handleServerMessage(&msg) - if msg.ID != "" { - a.mu.Lock() - a.lastMsgID = msg.ID - a.mu.Unlock() - } } } }