Skip to content

feat(kilo-chat): chat backend service#2410

Merged
iscekic merged 22 commits intofeat/kiloclaw-kilo-chat-pluginfrom
feat/kilo-chat-service
Apr 14, 2026
Merged

feat(kilo-chat): chat backend service#2410
iscekic merged 22 commits intofeat/kiloclaw-kilo-chat-pluginfrom
feat/kilo-chat-service

Conversation

@iscekic
Copy link
Copy Markdown
Contributor

@iscekic iscekic commented Apr 14, 2026

Summary

New Cloudflare Worker service at services/kilo-chat/ — the external chat backend that the kiloclaw kilo-chat channel plugin (PR #2361) talks to.

  • Durable Objects: ConversationDO (per-conversation state — messages, members, SSE fan-out, typing) and MembershipDO (per-user conversation index with denormalized recency)
  • REST API: Full CRUD for conversations and messages, cursor-paginated message history, typing indicators
  • Real-time: SSE streaming with Last-Event-ID replay, 30s keepalive pings via DO alarms
  • Webhook delivery: Queue-based HMAC-SHA256 signed webhook delivery to kiloclaw (bot never receives own messages)
  • Auth: Dual — kilo JWTs for human users, API keys with x-kilo-sandbox-id for bot/service callers
  • Message model: ULID IDs (monotonic via ulid package), JSON content blocks, optimistic versioning for edits (409 on stale), soft deletes
  • Data model supports multi-party conversations (future); currently creates 1:1 user + bot per sandbox

API Surface

Method Path Description
POST /v1/conversations Create conversation (user + bot for sandbox)
GET /v1/conversations List my conversations
GET /v1/conversations/:id Get conversation details
POST /v1/messages Create message
GET /v1/conversations/:id/messages List messages (cursor paginated)
PATCH /v1/messages/:id Edit message (optimistic versioning)
DELETE /v1/messages/:id Soft-delete message
GET /v1/conversations/:id/events SSE stream
POST /v1/conversations/:id/typing Typing indicator

Test plan

  • 67 tests across 9 test files (DO unit tests + route integration tests + webhook delivery)
  • pnpm typecheck — clean (tsgo)
  • pnpm lint — 0 errors (oxlint)
  • pnpm format:changed — clean (oxfmt)
  • End-to-end with kiloclaw plugin (deferred — needs deployment + public route)

iscekic added 14 commits April 14, 2026 15:39
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.
@iscekic iscekic self-assigned this Apr 14, 2026
… 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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@kilo-code-bot
Copy link
Copy Markdown
Contributor

kilo-code-bot bot commented Apr 14, 2026

Code Review Summary

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 0

Fix these issues in Kilo Cloud

Issue Details (click to expand)

WARNING

File Line Issue
services/kilo-chat/src/routes/messages.ts 104 Negative limit values disable the intended pagination cap and can return the full conversation history.
services/kilo-chat/src/do/conversation-do.ts 304 SSE replay skips edits or deletes to the last seen message because reconnect filtering only includes rows with id > lastEventId.
services/kiloclaw/plugins/kilo-chat/src/client.ts 11 The client now requires content, but services/kiloclaw/plugins/kilo-chat/src/channel.ts still calls createMessage({ conversationId, text }), leaving the outbound send path out of sync with the new contract.
Other Observations (not in diff)

None.

Files Reviewed (4 files)
  • services/kilo-chat/src/routes/messages.ts - 1 issue
  • services/kilo-chat/src/do/conversation-do.ts - 1 issue
  • services/kiloclaw/plugins/kilo-chat/src/client.ts - 1 issue
  • services/kiloclaw/plugins/kilo-chat/src/channel.ts - 0 issues

Reviewed by gpt-5.4-20260305 · 958,066 tokens

iscekic added 3 commits April 14, 2026 16:35
…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();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

iscekic added 4 commits April 14, 2026 17:18
… 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[];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@iscekic iscekic merged commit c1b64d6 into feat/kiloclaw-kilo-chat-plugin Apr 14, 2026
1 check passed
@iscekic iscekic deleted the feat/kilo-chat-service branch April 14, 2026 15:51
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.

1 participant