Skip to content

Add chatroom API and real-time CLI viewer#177

Merged
jlia0 merged 2 commits intomainfrom
jlia0/chatroom
Mar 9, 2026
Merged

Add chatroom API and real-time CLI viewer#177
jlia0 merged 2 commits intomainfrom
jlia0/chatroom

Conversation

@jlia0
Copy link
Copy Markdown
Collaborator

@jlia0 jlia0 commented Mar 9, 2026

Summary

Adds user-facing access to team chat rooms with REST API endpoints and a real-time CLI viewer with type-to-send support. Users can now view chat room messages and post to them directly.

Changes

  • API Endpoints: GET /api/chatroom/:teamId to query messages, POST /api/chatroom/:teamId to post messages
  • TUI Viewer: Real-time Ink-based tinyclaw chatroom <team_id> command with message polling and type-to-send
  • Database: Added getChatMessages() query function to fetch chat room messages
  • Server Integration: Mounted chatroom routes in API server
  • Documentation: Updated TEAMS.md with chat room viewing instructions and API endpoint docs

All changes relate to chat room viewing/posting from the user's perspective — the agent-side chat room implementation (via [#team_id: message] tags) remains unchanged.

Type of Change

  • New feature
  • Documentation update

Testing

  • API endpoints tested via curl
  • TUI viewer builds and loads without runtime errors
  • All type errors are pre-existing (missing @types/node, @types/react)

Checklist

  • All changes committed and pushed
  • Documentation updated (TEAMS.md, QUEUE.md, MESSAGE-PATTERNS.md)
  • No new errors introduced (only pre-existing type declaration issues)

## Summary
Adds user-facing access to team chat rooms with REST API endpoints and a real-time CLI viewer with type-to-send support. Users can now view chat room messages and post to them directly.

## Changes
- Add `GET /api/chatroom/:teamId` and `POST /api/chatroom/:teamId` API endpoints for querying and posting chat room messages
- Create `chatroom-viewer.tsx` — Ink-based real-time TUI that polls the API and displays messages with type-to-send support
- Add `tinyclaw chatroom <team_id>` CLI command to launch the viewer
- Add `getChatMessages()` query function to `db.ts` for fetching chat room messages
- Mount chatroom routes in the API server
- Update documentation with chat room viewing and API endpoint info

All changes relate to chat room viewing only — no changes to the chat room implementation itself (agents still post via `[#team_id: message]`).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 9, 2026

Greptile Summary

This PR adds user-facing access to the team chat room system through REST API endpoints (GET/POST /api/chatroom/:teamId), a real-time Ink-based TUI (tinyclaw chatroom <team_id>), a new chat_messages SQLite table, and batch message claiming so that queued chat room broadcasts can be delivered in a single agent invocation. The documentation in AGENTS.md, TEAMS.md, QUEUE.md, and MESSAGE-PATTERNS.md is updated accordingly.

Key findings:

  • Error handling gap in batch processing — When processMessage throws, only the primary message is passed to failMessage; the additional batched messages remain in processing state. After the stale-message recovery window they will be redelivered, potentially causing duplicate agent invocations. All messages in the batch should be failed together.
  • postJson silently swallows HTTP errors — The postJson helper in chatroom-viewer.tsx never checks res.statusCode, so a 400 Bad Request or 404 Not Found from the API resolves the promise rather than rejecting it. The sendMessage error handler therefore never fires, leaving the user with no feedback when a post fails.
  • Chat-room regex truncates messages with ] — The lazy [\s\S]*? quantifier in extractChatRoomMessages stops at the first ] inside the message body. Agent responses containing array indexing, Markdown links, or bracketed examples will be silently truncated when posted to the chat room.
  • Dynamic IN clause SQLite variable limitclaimAllPendingMessages constructs WHERE id IN (?,?,…) with one bind slot per message. SQLite's default limit is 999 variables; a large backlog (e.g., after a crash/recovery) can exceed this and cause a runtime error.

Confidence Score: 3/5

  • Mostly safe but has a logic bug in batch-error handling that can cause stale message re-delivery, and a silent failure path in the TUI's HTTP client.
  • The core chat room persistence and fan-out logic is correct. However, the missing failMessage calls for batched messages is a real correctness issue — errors during processing leave secondary messages stuck and eventually re-delivered, which could trigger unwanted duplicate agent invocations. The postJson status-code gap is a UX bug that silently drops error feedback. Neither issue is catastrophic, but they should be fixed before this ships to avoid confusing operational behaviour.
  • src/queue-processor.ts (batch error handling) and src/visualizer/chatroom-viewer.tsx (postJson status check) need the most attention before merging.

Important Files Changed

Filename Overview
src/queue-processor.ts Switched from single-message claiming to batch claiming (claimAllPendingMessages); chat room broadcast extraction added post-response — but the catch block only fails the primary message, leaving additional batched messages stuck in processing state on error.
src/visualizer/chatroom-viewer.tsx New Ink-based TUI for real-time chatroom viewing and posting; postJson does not check HTTP status codes so server-side errors (400/404/500) silently resolve, suppressing user-visible error feedback.
src/lib/db.ts Adds chat_messages table, claimAllPendingMessages, insertChatMessage, and getChatMessages; the dynamic IN clause in claimAllPendingMessages can exceed SQLite's variable limit under large backlogs.
src/lib/routing.ts New extractChatRoomMessages function extracts [#team_id: message] tags; the lazy regex quantifier truncates messages that contain ] characters.
src/server/routes/chatroom.ts New Hono routes for GET and POST chatroom endpoints; validates team existence and message presence, delegates cleanly to existing DB/conversation helpers.
src/lib/conversation.ts Adds postToChatRoom which persists to chat_messages and fans out queue entries to all teammates except the sender; logic is sound.
src/server/index.ts Mounts the new chatroom routes — minimal, clean change.
tinyclaw.sh Adds chatroom <team_id> subcommand with auto-build logic; correctly passes the team ID and falls through to the help text update.

Sequence Diagram

sequenceDiagram
    participant User
    participant TUI as chatroom-viewer (TUI)
    participant API as API Server /api/chatroom/:teamId
    participant DB as chat_messages (SQLite)
    participant QP as queue-processor
    participant Agent as Agent(s)

    Note over TUI,API: Poll for new messages (every 1s)
    TUI->>API: GET /api/chatroom/:teamId?since=lastId
    API->>DB: getChatMessages(teamId, limit, sinceId)
    DB-->>API: ChatMessageRow[]
    API-->>TUI: JSON array of messages

    Note over TUI,Agent: User posts a message
    User->>TUI: types message + Enter
    TUI->>API: POST /api/chatroom/:teamId {message}
    API->>DB: insertChatMessage(teamId, "user", message)
    API->>QP: postToChatRoom → enqueueMessage for each agent
    API-->>TUI: {ok: true}

    Note over QP,Agent: Agent processing (batch claim)
    QP->>DB: claimAllPendingMessages(agentId)
    DB-->>QP: [primaryMsg, ...chatRoomMsgs]
    QP->>Agent: invokeAgent(primaryMsg + batched context)
    Agent-->>QP: response (may contain [#team_id: ...] tags)
    QP->>QP: extractChatRoomMessages(response)
    QP->>DB: insertChatMessage + enqueue for teammates
    QP->>DB: completeMessage(primary + additionalMsgs)
Loading

Comments Outside Diff (3)

  1. src/queue-processor.ts, line 309-312 (link)

    Additional batched messages not failed on error

    When an exception is thrown during processMessage, only the primary message (dbMsg.id) is failed. The additionalMsgs are left in processing status indefinitely. recoverStaleMessages will eventually reassign them back to pending, causing them to be redelivered in a future batch — potentially leading to duplicate agent invocations.

    } catch (error) {
        log('ERROR', `Processing error: ${(error as Error).message}`);
        failMessage(dbMsg.id, (error as Error).message);
        for (const extra of additionalMsgs) {
            failMessage(extra.id, (error as Error).message);
        }
    }
  2. src/visualizer/chatroom-viewer.tsx, line 665-691 (link)

    postJson ignores HTTP status code

    Unlike fetchJson which rejects on non-200 responses, postJson unconditionally calls resolve(JSON.parse(data)) regardless of the response status code. If the POST fails with a 400 (e.g., empty message) or 404 (team not found), the promise resolves silently instead of rejecting. As a result, the catch in sendMessage never fires and the user never sees an error.

    }, (res) => {
        let data = '';
        res.setEncoding('utf8');
        res.on('data', (chunk: string) => { data += chunk; });
        res.on('end', () => {
            if (res.statusCode !== 200) {
                res.resume();
                try { reject(new Error(`HTTP ${res.statusCode}: ${JSON.parse(data)?.error ?? data}`)); }
                catch { reject(new Error(`HTTP ${res.statusCode}`)); }
                return;
            }
            try { resolve(JSON.parse(data)); }
            catch (e) { reject(e); }
        });
    });
  3. src/lib/routing.ts, line 376 (link)

    Regex truncates messages containing ]

    The lazy quantifier [\s\S]*? stops at the first ] it encounters. Any chat room message body containing a closing bracket — e.g. code snippets (items[0]), Markdown links ([link](url)), or inline examples — will be silently truncated at that character.

    For example, [#dev: check items[0] for the bug] would capture only check items[0 as the message, dropping for the bug.

    Consider using a greedy match anchored by a clear terminator, or requiring agents to use a multi-line block format, to avoid this ambiguity.

Last reviewed commit: 6862b04

Comment thread src/lib/db.ts
Comment on lines +230 to +233
d.prepare(`
UPDATE messages SET status = 'processing', claimed_by = ?, updated_at = ?
WHERE id IN (${ids.map(() => '?').join(',')})
`).run(agentId, now, ...ids);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dynamic SQL IN clause can exceed SQLite's variable limit

The prepared statement builds WHERE id IN (${ids.map(() => '?').join(',')}) with one bind parameter per message. SQLite's default SQLITE_MAX_VARIABLE_NUMBER is 999. If more than ~997 messages are pending for a single agent (the first two parameters in .run() are agentId and now), the statement will throw SQLITE_ERROR: too many SQL variables.

While unlikely under normal load, a backlog after a crash/recovery cycle could trigger this. Consider chunking the UPDATE into batches of 900 or using a temporary table approach.

@jlia0 jlia0 requested a review from mczabca-boop March 9, 2026 16:18
Replace regex-based tag parsing with a bracket-depth counter that
correctly handles nested brackets in message bodies. Previously,
`[@coder: fix arr[0]]` would truncate at the first `]`.

Affects all three tag extraction sites:
- extractTeammateMentions ([@agent: ...] tags)
- extractChatRoomMessages ([#team: ...] tags)
- completeConversation (tag-to-readable conversion)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mczabca-boop
Copy link
Copy Markdown
Collaborator

1. Batched messages can be stranded in processing on any uncaught error

claimAllPendingMessages() marks every pending row for the agent as processing, but the error path in processMessage() only calls failMessage(dbMsg.id) for the primary message.

That means if anything throws before the batch is fully completed, all additionalMsgs remain stuck in processing with claimed_by set, and they are never retried through the normal flow.

Relevant code:

  • src/queue-processor.ts: the batch is completed here for success, but only the primary message is failed in the catch path.
  • src/lib/db.ts: claimAllPendingMessages() updates all claimed rows to processing.

I reproduced this manually by inserting two pending messages for the same agent, making the first one fail early with invalid JSON in files, and then triggering queue processing. The result was:

  • primary message: reset to pending, retry_count = 1
  • secondary batched message: still processing, claimed_by = "assistant"

This is a real correctness issue for the new batching model. The failure path needs to release or fail all claimed rows, not just the primary one.

2. Chatroom viewer has no path to display agent replies

The new chatroom flow writes user posts into the normal queue using channel: 'chatroom', and the queue processor handles them as team conversations. On completion, the result is enqueued into the normal responses table as an outgoing response for the chatroom channel.

However, the viewer only polls:

  • GET /api/chatroom/:teamId

and never consumes:

  • GET /api/responses/pending?channel=chatroom

So the viewer can display persisted chat_messages, but it cannot display the agent’s actual reply. Those replies just accumulate in the outgoing response queue.

Relevant code:

  • src/server/routes/chatroom.ts: posts are enqueued with channel: 'chatroom'
  • src/lib/conversation.ts: completed team conversations call enqueueResponse(...)
  • src/visualizer/chatroom-viewer.tsx: the UI only polls /api/chatroom/:teamId

I reproduced this manually:

  1. POST /api/chatroom/solo-team succeeded
  2. the message appeared in /api/chatroom/solo-team
  3. the queue logs showed Response ready [chatroom] user
  4. the pending response appeared in /api/responses/pending?channel=chatroom
  5. the TUI viewer still only showed the you messages and no agent reply

So the current implementation has a split data model:

  • viewer reads chat_messages
  • agent replies are written to responses

Those two flows are not connected, which makes the chatroom UI incomplete and causes chatroom responses to pile up unacked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants