feat(chat): direct-chat PWA (v1) — sessions, SSE streaming, /chat app - #3
feat(chat): direct-chat PWA (v1) — sessions, SSE streaming, /chat app#3tariqismail wants to merge 24 commits into
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gress Includes smoke-test fixes: flex min-width for text truncation, poll sequence guard against duplicate logs, back button hidden on desktop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…l, retry 409 follow-ups The backend writes final_response ~15s before the job flips to completed (wrap-up work). The UI previously stopped polling at the reply and kept the composer locked on 'Arqis is working' forever. Now: polling continues until a terminal status, the composer and working indicators clear as soon as the reply text exists, and follow-ups sent during wrap-up quietly retry past the API's 409 instead of failing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement data model foundation for direct-chat feature: two new Postgres tables (chat_sessions, chat_messages) via 003_chat_sessions.sql migration, plus ChatStore class wrapping raw SQL access to them. All 8 methods specified by Task 5 interface are implemented. Covered by 6 passing unit tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add three new configuration knobs under agent.chat for the direct-chat fast path: model (string, empty default), max_history_messages (int, default 20), and rate_limit_per_minute (int, default 20). Environment variable overrides and documentation included. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements the LLM fast-path for direct chat: turns session history + new user message into a stream of reply events (delta/escalated/done/error). Makes streamed OpenRouter chat-completion calls with escalate_to_job tool. Includes full SSE parsing, tool-call accumulation across chunks, and fallback to non-streamed LlmClient on stream setup failure. Includes 11 unit tests covering plain text, split tool args, malformed JSON, SSE parsing, message building, transcript condensing, and fallback path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires Tasks 1-4 (ChatStore, agent.chat config, chat_responder,
create_workspace_job source tagging) together behind three endpoints:
POST /api/chat/sessions/{session_id}/messages (streaming SSE, rate-limited,
escalates to a workspace job when chat_responder signals it), GET
/api/chat/sessions, and GET /api/chat/sessions/{session_id}/messages.
Also folds chat_messages costs into usage_cost_summary()'s lifetime/month
totals alongside task_logs and deep_research_events.
- parseSSE async generator consumes ReadableStream body and yields JSON events
- Correctly handles SSE frames split across multiple stream reads
- Skips blank/comment lines per SSE spec
- streamChatMessage POSTs to /api/chat/sessions/{sessionId}/messages
- Throws ApiError on non-2xx responses, matching existing request() pattern
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Appended listChatSessions() and getChatSessionMessages(sessionId) to agent/frontend/src/chat/api.js for listing chat sessions and fetching transcript messages. Both use the existing request() helper, matching the style of listJobs and pollJob. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds mergeConversations(sessions, jobs) to combine chat sessions and job threads into a unified, activity-sorted conversation list. Includes session helpers (sessionTitle, sessionProcessing, sessionSnippet, sessionStatus) mirroring job-thread accessors. Sorts by most-recent activity using each session's last_message_at or job thread's latest job created_at. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…reads
New conversations now always go through streamChatMessage (POST
/api/chat/sessions/{id}/messages) instead of the old createJob/sendFollowUp
workspace-job endpoints, which are no longer called from the UI at all.
- Unified thread list merges chat sessions and legacy job threads via
mergeConversations, sorted newest-activity-first.
- Legacy job threads (metadata.source === "workspace") render read-only:
full history + live-poll-to-terminal as before, composer replaced with a
static notice.
- Chat session messages render as plain bubbles (kind: "chat") or job turns
with progress steps + reply (kind: "job_ref"), reusing the existing
/api/jobs/{id}/poll loop.
- Single pollingJobId derivation covers both a legacy thread's active job
and a session's active job_ref, feeding one poll effect instead of two.
- Composer disables only while the latest job_ref in the active session is
still processing without a final_response (awaitingSessionReply).
- Streaming replies show token-by-token via Bubble's streaming prop while
accumulating, then settle into a persisted bubble once the transcript is
refetched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…take them for its own Prefix job final_response content with "[Completed by the full task pipeline]" when folding it into job_ref history rows, and tell the quick-chat model (via QUICK_CHAT_INSTRUCTIONS) to treat such prefixed turns as real completed actions rather than something it said. Fixes a live bug where the tool-less quick-chat model saw an unmarked assistant turn describing a tool outcome, concluded it had hallucinated, apologized, and needlessly re-escalated a duplicate job.
Chat already links back to Admin via its panel header — this completes the round trip so you can jump to /chat from the dashboard too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
@tariqismail - I've gone through the chat PWA PR in detail — really solid work. The architecture is clean: quick replies stay fast, escalation to the job pipeline is seamless, and the job results folding back into conversation context is a nice touch. Code quality is high across the board (streaming state machine, SSE edge-case handling, DOMPurify for rendering, proper rate limiting, conflict detection on active jobs). A few small things I noticed during review:
None of these are blockers. The tests are thorough, the feature is well-scoped, and the known gaps (no archive/delete, no attachments, no cancel) are all reasonable v1 trade-offs. When you're ready to take it out of draft, I'm happy to merge it. Just let me know if there's anything else you're still iterating on. Nice one 👍 |
Addresses review feedback on rb81#3: - condense_transcript() hardcoded Arqis as the assistant label. It now takes an optional config and derives the label from app_display_name(), falling back to a neutral Assistant when no config is passed. - The chat rate-limit query (ChatStore.count_recent_user_messages) filters on role + created_at, which no index covered — it seq-scanned. Adds chat_messages(role, created_at) to migration 003 while it is still unreleased. Verified against Postgres: seq scan -> index-only scan. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
All three looked at — pushed in cd887c4. 1. Hardcoded assistant label — fixed. 2. Index — fixed, and it was measurable. Added Worth noting for anyone already running this branch: migrations are tracked by filename in 3. Blocking urllib — agreed it's real, deliberately not changing it here. Making the streaming generator non-blocking means either an async HTTP client or moving the read to a thread, and both ripple into the SSE endpoint and its tests. That's a meaningful refactor to carry for a concurrency requirement that doesn't exist yet at single-user scale, so I'd rather it land on its own merits when concurrent chat streams are actually needed than bolt it onto this PR. Tracked on my side. Test suite: 220 passing, up from 218, no regressions. Separately, while verifying the above I hit a pre-existing failure that also reproduces on clean Still marked draft — say the word and I'll flip it to ready. |
|
Thanks for the chat work. I’m not merging this — two of these will break a fresh deploy or leak every chat transcript. High —
Fresh deployments will fail as soon as chat endpoints are hit. The API process needs to apply file migrations (same path as the other roles), and that needs a test so it doesn’t regress. High — no ownership or auth on chat
The rest of the API being unauthenticated does not make this acceptable. Chat is a persistent inbox: anyone who can reach the port can read and append every conversation. Please add an ownership model with tests that two clients cannot list, read, or append each other’s sessions. Unknown sessions should 404, not leak existence. I don’t need a specific design — cookie-scoped owner token, shared API token, or something else — as long as that invariant holds. Medium — truncated upstream SSE is treated as a completed reply
A dropped upstream connection must yield Medium — frontend clears the draft after streamed errors
Treat Frontend tests passed here; I could not run the backend suite in the review environment. GitHub shows the PR as mergeable with no check runs — please make sure CI actually runs these tests. Happy to re-review once those four are addressed. |
|
All four reproduce. I checked each against the branch before writing this, and I'm not disputing any of them:
One nuance on (1), which I think supports your point rather than softening it: in the full compose stack the tables do get created, because every non-api role falls through to Before I start, four things I'd rather settle than guess at: Migrations (1). Inline Ownership (2). The one where guessing costs most:
Truncated stream (3). "Must not insert a completed row" — discard the partial text entirely, or persist it as an explicitly incomplete row so the user can see what arrived and retry? And on the client: leave the partial text on screen alongside the error, or clear it? CI (4). There's no workflow in the repo at all — no Holding implementation until I hear back. Three of these are small enough that I'd have pushed them already, but (2) is a schema plus a trust boundary and I'd rather not build the wrong one twice. |
|
Good questions. In order: 1 — Migrations. Inline Leave 2 — Ownership. This is a single-owner personal assistant. One user, multiple devices. A shared secret token is the right fit — no per-identity scoping, no user accounts.
3 — Truncated stream. Discard the partial text — do not persist an incomplete row. On the client: keep the partial text visible so the user can see what arrived, show the error alongside it, and preserve the draft in the composer so they can retry. The key constraint is that the database should not contain a row that looks like a completed reply when it isn't one. 4 — CI. Not this PR. It's repo-wide and I'll set it up separately. Just make sure the existing test suites pass locally before pushing. |
Why
The email interface is great for most tasks, but there are times you want to work side by side with the assistant or quickly run something by it without composing an email and waiting on the job round-trip. This adds a direct-chat fast path for exactly those moments, while leaving the email/job workflow as the path for heavier work.
Summary
Adds an optional, installable
/chatPWA with live streaming responses. Lightweight turns are answered directly; anything that needs real work escalates into the existing workspace-job pipeline and the reply folds back into the conversation.What's included
Backend (by module boundary)
chat_store.py—chat_sessions/chat_messagestables + store (migration003_chat_sessions.sql)chat_responder.py— fast-path streaming responderapi.py— SSE session/message endpoints; escalation wiring (create_workspace_jobgains asourcetag,create_manual_joban optionalthread_id) so job replies fold back into chat contextconfig.py/config/agent.yaml—agent.chatconfig block (documented indocs/env-overrides.md)Frontend (
agent/frontend/src/chat/, second Vite entry)/chatapp: thread grouping, conversation view, composer, live progress, streaming cursor, markdown rendering, design tokensScope — this is v1
Deliberately kept focused. Known gaps, likely follow-ups:
Tests
test_config/test_ui_pagesadditions — 51 passed viapytest.--testsuite (stream,threads) — 18 passed.npm run build(vite) builds cleanly.Notes
/chatroute + admin link.chat.bundle.*are gitignored and not included (mirrors the existingworkspace.bundle.jsconvention).