#2289·WeKnora

[Bug]: res:// orphan resource aliases leak into streaming responses, causing broken images in chat

Author: lisheguoCreated Jul 24, 2026Updated Sep 18, 2026

Affected Component

Backend (Go), Frontend

Bug Description

In knowledge-base Q&A, the streaming response (SSE) sometimes contains undecoded res://XXXX resource aliases (e.g. res://0007), causing broken images in the chat UI. The browser cannot resolve the res:// protocol, so affected images render as broken.

Some images load correctly (those referenced as images/imageFileXXXX.png or properly expanded /files/xxx paths), while others fail (referenced as raw res://XXXX aliases that survived decoding).

Root Cause

WeKnora uses a two-layer registry system for resource references:

  1. llmresource.Registry - compacts resource:// handles into short aliases res://XXXX (e.g. res://0001) before sending to the LLM, and restores them via StreamDecoder in the response.
  2. llmreference.Registry - handles <ref/> citation tags and expands resource:// handles to accessible /files/xxx URLs.

The pipeline in chat_completion_stream.go:

go
chatMessages, sourceRefs := prepareMessagesWithReferences(ctx, chatManage)
resourceRefs := llmresource.NewRegistry()
chatMessages = sourceRefs.EncodeMessages(chatMessages)
chatMessages = resourceRefs.EncodeMessages(chatMessages)
// ...
answerDecoder := llmresource.NewStreamDecoder(resourceRefs)
answerRefExpander := llmreference.NewStreamExpander(sourceRefs)
response.Content = answerRefExpander.Feed(answerDecoder.Feed(response.Content))

The bug: The LLM sometimes emits res://XXXX aliases that do not exist in the request-local registry (hallucination). StreamDecoder.DecodeText() cannot map them back to resource:// handles, so these orphan aliases pass through as literal text into the SSE stream. The browser cannot resolve res:// protocol -> broken images.

Key Code Locations

Non-streaming path (chat_completion.go:74):

go
if orphans := resourceRefs.OrphanAliases(chatResponse.Content); len(orphans) > 0 {
    pipelineWarn(ctx, "Completion", "orphan_resource_aliases", ...)
}

Only logs a warning - does not strip orphan aliases from the content.

Streaming path (chat_completion_stream.go:114-222):

go
answerDecoder := llmresource.NewStreamDecoder(resourceRefs)
answerRefExpander := llmreference.NewStreamExpander(sourceRefs)

No OrphanAliases check at all - orphan aliases flow directly into SSE events.

Agent path (agent/think.go:119):

go
if orphans := e.resourceRefs.OrphanAliases(result.Content); len(orphans) > 0 {
    logger.Warnf(ctx, "[Agent][Stream] Model emitted %d unresolvable resource alias(es): %v", ...)
}

Also only logs a warning, does not strip.

Reproduction

  1. Deploy WeKnora v0.7.0 with local storage (STORAGE_TYPE=local)
  2. Upload a PDF document containing images to a knowledge base
  3. Ask a question that causes the model to reference document images in the answer
  4. Observe that some images in the response are broken
  5. Inspect the DOM - broken images have src="res://0007" (or similar) instead of a proper URL

Reproduction Rate

Approximately 30-50% of image-containing responses. Longer context and more resource references increase the likelihood.

Expected Behavior

Orphan res://XXXX aliases should be stripped from the response content before sending to the frontend, or at minimum replaced with a placeholder. The streaming path should have the same OrphanAliases guard as the non-streaming path.

Suggested Fixes

  1. Add OrphanAliases check to the streaming path: After flushDecoders and each ResponseTypeAnswer chunk, detect and strip orphan aliases.
  2. Strip in DecodeText / StreamDecoder.Feed: Replace unresolvable res://XXXX tokens with empty string or placeholder text instead of passing them through.
  3. Frontend fallback: Markdown renderer should gracefully degrade res:// URLs (hide image or show "image load failed").

Environment

  • WeKnora v0.7.0
  • Docker deployment (5 containers: frontend/app/postgres/redis/docreader)
  • Storage type: local
  • LLM: doubao-seed-2.0-lite (Volcengine Ark)

Related

  • PR #2075 introduced the resource registry and alias system
  • PR #2278 (fix(agent): resolve request-local source handles) addresses a related but different alias resolution issue in the Agent path
  • Issue #2179 (think tags leak into chat responses) is a similar pattern - LLM output not being sanitized in the streaming pipeline