feat(kilo-chat): chat backend service#2410
feat(kilo-chat): chat backend service#2410iscekic merged 22 commits intofeat/kiloclaw-kilo-chat-pluginfrom
Conversation
Per-user/bot Durable Object storing a conversation membership list with CRUD ops (add, list, update lastMessageId, remove). Includes vitest tests and updated wrangler types.
Implements POST /v1/conversations, GET /v1/conversations, and GET /v1/conversations/:id behind auth middleware, with full integration test coverage.
Implements POST /v1/messages, GET /v1/conversations/:id/messages, PATCH /v1/messages/:id, and DELETE /v1/messages/:id with membership checks, webhook queue enqueue for bot members, and MembershipDO lastMessageId updates.
- Add in-memory SSE client tracking and broadcast() to ConversationDO - Broadcast message.created, message.updated, message.deleted events after each DB write - Add fetch() handler on ConversationDO to handle /subscribe with member auth - Support Last-Event-ID replay by querying messages with id > lastEventId - Add alarm() keepalive that pings all connected clients every 30s - Add /v1/conversations/:id/events route that forwards to DO's fetch handler - Tests: access control (403/404), broadcast no-crash, streaming header checks, replay verification - Streaming tests placed last in file to work around miniflare SQLite WAL isolated storage limitation
Implements the WEBHOOK_QUEUE consumer that delivers HMAC-SHA256 signed payloads to the kiloclaw webhook endpoint, with per-message ack/retry and graceful handling when secrets are not configured.
…monotonicFactory for message IDs
… replay, writer cleanup - Fix webhook queue message shape to match WebhookMessage type (was sending wrong fields) - Use constant-time comparison for API key auth via crypto.subtle.timingSafeEqual - SSE replay always sends message.created for missed messages (client never saw them) - Close dead SSE writers on disconnect to prevent resource leaks - Remove redundant callerKind guard in message creation
|
|
||
| const beforeParam = c.req.query('before'); | ||
| const limitParam = c.req.query('limit'); | ||
| const limit = Math.min(limitParam ? parseInt(limitParam, 10) || 50 : 50, 100); |
There was a problem hiding this comment.
WARNING: Negative limit values bypass the intended 100-message cap
parseInt(limitParam, 10) || 50 leaves values like -1 intact, and SQLite treats LIMIT -1 as no limit. A request such as ?limit=-1 can therefore return the entire conversation history instead of being clamped to 100 messages.
| const missed = this.db | ||
| .select() | ||
| .from(messages) | ||
| .where(gt(messages.id, lastEventId)) |
There was a problem hiding this comment.
WARNING: Replay misses edits and deletes for the last seen message
This query only replays rows with id > lastEventId, but message.updated and message.deleted reuse the original message ULID as their SSE id. If a client last saw message.created for message X and disconnects before a later edit or delete to X, reconnecting with Last-Event-ID: X excludes that row here, so the missed update is never replayed.
Code Review SummaryStatus: 3 Issues Found | Recommendation: Address before merge Overview
Fix these issues in Kilo Cloud Issue Details (click to expand)WARNING
Other Observations (not in diff)None. Files Reviewed (4 files)
Reviewed by gpt-5.4-20260305 · 958,066 tokens |
…ersioning, single webhook - Remove redundant string-length check in timingSafeEqual (keep byte-length only) - Server now controls message version (increments from current), not client-supplied - Send one webhook per message (not one per bot member) since payloads are identical - Allow version 0 in edit schema for stale clients
| } | ||
|
|
||
| // Try API key auth first (constant-time comparison) | ||
| const apiKey = await c.env.KILOCHAT_API_TOKEN.get(); |
There was a problem hiding this comment.
CRITICAL: Auth middleware still reads the old secret binding name
wrangler.jsonc, worker-configuration.d.ts, the E2E script, and the updated auth tests all rename this binding to KILOCHAT_API_TOKEN, but the middleware still calls c.env.KILOCHAT_API_KEY.get(). In deployed Workers that property is undefined, so this throws before either API-key or JWT auth runs and turns every /v1/* request into a 500.
… of flat text
Replace `text: string` with `content: ContentBlock[]` in CreateMessageParams and
EditMessageParams, wrap text strings in `[{ type: 'text', text }]` blocks at all
call sites (preview-stream and webhook deliver), and update all three test files
to match. Also change the e2e script SANDBOX_ID default to 'e2e-test-sandbox'.
|
|
||
| export type CreateMessageParams = { | ||
| conversationId: string; | ||
| content: ContentBlock[]; |
There was a problem hiding this comment.
WARNING: The new content payload shape is not propagated to all callers
CreateMessageParams now requires content, but services/kiloclaw/plugins/kilo-chat/src/channel.ts:75 still calls client.createMessage({ conversationId, text }). That leaves the main outbound sendText path out of sync with this client contract, so the plugin will either fail typechecking or keep sending the old request body shape if this slips through.
Summary
New Cloudflare Worker service at
services/kilo-chat/— the external chat backend that the kiloclaw kilo-chat channel plugin (PR #2361) talks to.ConversationDO(per-conversation state — messages, members, SSE fan-out, typing) andMembershipDO(per-user conversation index with denormalized recency)Last-Event-IDreplay, 30s keepalive pings via DO alarmsx-kilo-sandbox-idfor bot/service callersulidpackage), JSON content blocks, optimistic versioning for edits (409 on stale), soft deletesAPI Surface
POST/v1/conversationsGET/v1/conversationsGET/v1/conversations/:idPOST/v1/messagesGET/v1/conversations/:id/messagesPATCH/v1/messages/:idDELETE/v1/messages/:idGET/v1/conversations/:id/eventsPOST/v1/conversations/:id/typingTest plan
pnpm typecheck— clean (tsgo)pnpm lint— 0 errors (oxlint)pnpm format:changed— clean (oxfmt)