Surfacing the stream error via useChat().error alone was not enough: on a
brand-new chat the errored turn still fires onFinish -> onTurnFinished, which
adopts the freshly-created chat id and changes the <ChatThread> key, remounting
it with a fresh useChat whose transient `error` is gone. The thread re-seeds
from persisted history, where the assistant row has empty parts and the error
lives only in metadata.error — which was never rendered. Result: an empty
"AI agent" row and no visible error.
- Render the persisted metadata.error inline in MessageItem, so the error
survives the remount and is also shown in reopened chat history.
- Carry metadata.error onto the rebuilt UIMessage in rowToUiMessage.
- Extract the error formatter into utils/error-message.ts (describeChatError)
and reuse it for both the live Alert and the persisted error.
- Add metadata.error to the IAiChatMessageRow type.
Client-only; the server already persists metadata.error. No new i18n keys.
The AI chat UI previously collapsed every non-403/503 failure into a
generic "could not respond" message, hiding real provider errors such as
OpenRouter HTTP 402 "requires more credits". The backend already forwards
the real "<status>: <message>" via pipeUIMessageStreamToResponse onError,
so the fix is client-side.
- describeError now returns the provider message verbatim for any error
that is not one of our own gating responses, so 402 (credits), 429
(rate limit) and similar causes are visible to the user.
- Match gating responses by the NestJS JSON "statusCode" field instead of
loose substring/word checks, so a provider message that merely contains
"403"/"503"/"disabled" is no longer misclassified and hidden.
- Add a providerDetail() helper that filters empty text and the opaque
"An error occurred." / "Internal server error" placeholders, falling
back to the generic message only then.
No backend changes; no new i18n keys.
so the v6 hook stops re-creating its store every render on a new chat
(which wiped the optimistic user message + streamed deltas, so nothing
showed until the turn finished). Also send X-Accel-Buffering:no + flushHeaders.
- context: client sends the currently-open page {id,title}; the system prompt
tells the agent which page 'this page' refers to (it reads it via its
CASL-scoped getPage tool; id is prompt-context only, no server-side fetch).
- embeddings: make page_embeddings.embedding dimension-agnostic (drop the
HNSW index + ALTER to vector), remove the hard 1536 guard, filter search by
model_dimensions — so 3072-dim (and any) models index instead of being
skipped. Seq-scan <=> search (wiki scale); existing pages reindex on next edit.
- Add reversible write tools to the per-user agent toolset (page create/update/
move/soft-delete; comment reply + resolve), exposed under the user's JWT and
enforced by Docmost CASL; no permanent/force delete (D3).
- Non-spoofable agent provenance: sign actor/aiChatId into the access and collab
tokens (TokenService), propagate via jwt.strategy onto the request, and set
pages.last_updated_source/last_updated_ai_chat_id on REST create/update/move and
comments.created_source/resolved_source/ai_chat_id.
- packages/mcp: add an optional getCollabToken provider (content-edit provenance)
and guard against empty tokens; service-account /mcp path unchanged.
Frontend:
- Admin 'AI / Models' settings section: provider/model/embedding/base URL, a
write-only API key field, system prompt, and Test connection.
- AI chat panel (useChat + DefaultChatTransport): conversation list, streamed
messages, tool-call action log and page citations; header entry point gated on
settings.ai.chat.
Compile-verified (server nest build + client tsc/vite); not yet live-tested.
Known gaps: history 'AI agent' badge (C3), vector RAG (D), external MCP (E);
chat tool-card citation links pending a fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>