- Introduced a new column `display_name` to the `models` table for optional user-facing display names.
- Created migration scripts for both adding and removing the column, ensuring backward compatibility.
- Added a check to skip regex processing for non-image content, improving performance when no inline base64 payload is present.
- Updated regex pattern to ensure it correctly matches base64 image data URIs.
- SSL verification now defaults to enabled; set WEKNORA_VERIFY_SSL=false to
opt out (with a logged warning). Fixes MITM risk from default-off TLS.
- WEKNORA_CHAT_TIMEOUT parse is now guarded with try/except ValueError so a
bad env value falls back to 300s instead of crashing at import.
- SSE streaming response is now closed via context manager (with response:)
to guarantee connection pool return even on early break.
- Replace asyncio.get_event_loop() (deprecated) with asyncio.get_running_loop()
in both chat and agent_chat handlers.
- create_session now calls resolve_kb_id() so KB names are accepted in addition
to UUIDs (consistent with chat / hybrid_search).
- knowledge_base_ids description changed from REQUIRED to Strongly recommended
to match actual schema optionality.
- run_sse() handle_sse rewritten as raw ASGI callable (scope, receive, send) to
avoid accessing Starlette private _send attribute.
- Fix main.py comment: http transport is Streamable HTTP (MCP spec), not long-polling.
Restore parameters that were inadvertently removed during refactoring.
- kb_id: Required knowledge base ID (architectural shift from KB-agnostic back to KB-bound sessions)
- max_rounds, enable_rewrite, fallback_response: Session strategy configuration
- summary_model_id: Model for response summarization
- title, description: Optional session metadata
These parameters enable AI agents to fully configure session behavior.
Add 3 read-only wiki tools (wiki_search, wiki_read_page,
wiki_index_view) to the Python MCP server, enabling external agents
like Claude Code and Codex to query WeKnora's LLM-generated wiki
pages following the LLM Wiki pattern.
Closes#1501
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 3 (#1440) gate flip. PR 1 (#1445) + PR 2a (#1481) + PR 2b (#1482)
laid the type prep + driver skeleton + read/write paths as gated dead
code; this PR wires every activation surface so opensearch becomes a
registerable VectorStore engine.
Activation wiring
- internal/types: validEngineTypes / GetVectorStoreTypes (with HNSW
bounds + knn_engine enum + Immutable hints) / retrieverEngineMapping /
buildEnvStoreForDriver — every gated surface now recognises
"opensearch". IndexConfig grows four omitempty HNSW fields (HNSWM /
HNSWEFConstruction / HNSWEFSearch / KNNEngine), keeping other engines'
serialised config byte-identical.
- internal/container: createOpenSearchEngine + the switch case in
createEngineServiceFromStore; the RETRIEVE_DRIVER=opensearch env path
in initRetrieveEngineRegistry; NewEngineFactory now closes over the
AuditLogService (the EngineFactory type itself is unchanged).
- internal/application/service/vectorstore_healthcheck.go: a
testOpenSearchConnection case so CreateStore's connectivity probe
accepts opensearch instead of returning 400.
- internal/application/repository/retriever/opensearch/transport.go:
NewOpenSearchClient is exported so the factory and env path can build
the TLS-hardened client; healthcheck.go reuses the unexported
probeVersion / probeKNNPlugin for the service-layer probe.
Service-layer validation
- validateOpenSearchIndexConfig validates the HNSW caps (m 2-100,
ef_construction 2-4096, ef_search 1-10000, knn_engine ∈ lucene|faiss).
Shards/replicas continue to be enforced by the flat ValidateIndexConfig.
Create-only: UpdateStore mutates the name only.
- validateConnectionConfig requires addr for opensearch.
Sync implementations (stubs.go shrinks)
- CopyIndices (copy.go) mirrors the Elasticsearch / Qdrant pattern —
search → BatchSave with the source_id remap for generated questions —
so dim/keyword routing and the source_id contract come from BatchSave
for free. embeddingMap is keyed by the *target* SourceID because
OpenSearch's BatchSave looks up embeddings by SourceID
(lookupEmbedding), not by chunk_id (the ES driver's convention).
Pagination is from/size; copies larger than max_result_window
(default 10000) need the scroll-based async path that lands later.
- BatchUpdateChunkEnabledStatus / BatchUpdateChunkTagID (bulk_update.go)
group the input by target value and issue one _update_by_query per
group over the cross-dim <base>_* pattern. Caller values flow through
bound script params only — never string-interpolated into the Painless
source — closing the script-injection surface.
- inspectByQueryResponse (byquery.go) mirrors inspectBulkResponse: the
full failure reason goes to the debug log only; the returned error
carries the bounded id + type.
- UpdateByQueryParams.Refresh is *bool in opensearch-go v4.6.0 (the same
shape as DeleteByQuery's quirk), so refresh=wait_for is not
expressible; we use refresh=true.
Driver-owned audit (DIP)
- A new opensearch.AuditSink interface (with nopSink + WithAuditSink
functional option) lets the driver emit opensearch.index_created and
opensearch.reindex_executed events without importing any service
package — the service layer implements the interface. NewRepository
takes opts, so existing 4-arg test call sites keep compiling unchanged.
- internal/container/audit_sink.go bridges AuditSink to AuditLogService.
When the context carries no tenant (the env-path registration ctx
during boot, for example) the adapter skips the emit with a warning
rather than silently writing tenant_id=0, which would collide with the
system-scope sentinel.
Frontend + polish
- FieldSchema (frontend/src/api/vector-store.ts) gains min/max/enum/
immutable. VectorStoreSettings.vue is now schema-driven: a closed
`enum` renders a t-select; number inputs use the schema's `:min`/`:max`
and fall back to the legacy replica-vs-shard heuristic only when the
schema does not pin them; a danger-coloured warning fires when
insecure_skip_verify is toggled on (the switch and warning are wrapped
in a vertical stack so the warning sits on its own row below the switch).
- i18n: labels for hnsw_m / hnsw_ef_construction / hnsw_ef_search /
knn_engine / insecure_skip_verify plus the warning copy in en-US,
ko-KR, zh-CN, ru-RU.
- docker-compose.dev.yml: an opensearch profile (single-node 3.3.2 with
security plugin disabled for dev only). OpenSearch Dashboards lives in a
separate, opt-in opensearch-ui profile so the heavy UI container is not
forced up alongside the cluster (the driver e2e is fully curl-verifiable
against :9200). The new docs/dev/opensearch-integration-test.md covers the
end-to-end exercise and the single-node guidance (set replicas=0 to keep
the cluster Green).
Gating-guard tests flipped
- The "OpenSearch is NOT in validEngineTypes / mapping / types list /
env builder / stubs" guard tests from PR 1 / PR 2 are replaced by
their positive counterparts in this PR. The test suite was the
activation checklist; the activation flip is its diff.
Backward compatibility
- Additive everywhere. IndexConfig's new HNSW fields are omitempty so
other engines' serialised config is byte-identical. Existing
Elasticsearch / Qdrant / Milvus / Weaviate / Doris / TencentVectorDB
stores are untouched. No migrations.
Test plan
- go build ./... clean
- go vet ./... clean
- gofmt -l clean on touched files
- go test ./... — only TestOssEnsureBucket_CreateFails (Aliyun OSS
endpoint), the docreader gRPC tests, and the doris SQL-shape tests
fail; all three are pre-existing on upstream/main and untouched by
this PR.
- New tests across internal/types, opensearch, service and container —
including a full end-to-end env-path test that exercises
initRetrieveEngineRegistry with RETRIEVE_DRIVER=opensearch against an
httptest cluster.
Split knowledge list/update queries to avoid GORM UPDATE...FROM
duplicate-table errors after Find, and use sync_logs started_at/
finished_at column names instead of start_time/end_time.
- Updated the loadTags function to prevent unnecessary calls when tags are already loading, enhancing performance and user experience.
- Modified tag loading calls in various tag-related functions to ensure the reset parameter is consistently set to true, ensuring the tag list is refreshed correctly after operations like create, edit, and delete.
- Improved the FAQEntryManager component to handle tag loading more efficiently during scrolling and batch operations.
This update improves the layout and user experience of the IMChannelsOverviewPanel component. Key changes include:
- Added tooltips for subtitles and agent names for better accessibility.
- Refactored channel and agent display logic to improve clarity and consistency.
- Adjusted styling for better visual hierarchy and responsiveness.
- Enhanced toggle functionality for IM channels to ensure state consistency during updates.
These changes aim to provide a more intuitive interface for users managing instant messaging channels.
This commit introduces a new validation mechanism to ensure that file access paths include the correct tenant segment, preventing cross-tenant access. The `ValidateStoragePathTenant` function has been added to enforce this rule, and the `serveFiles` function has been updated to return a forbidden status for invalid paths. Additionally, new tests have been added to verify the behavior of the file service under various tenant scenarios, ensuring robust handling of file access permissions.
This commit introduces functionality to utilize the tenant's default storage provider when creating a Knowledge Base. It includes updates to the frontend to load the default provider from settings and apply it during Knowledge Base initialization. Additionally, the backend has been enhanced to ensure that the storage provider is set correctly based on tenant configuration, improving consistency across the application. Tests have been added to verify the correct application of the default storage provider in various scenarios.
This commit introduces a new test suite for the IM file service, including a stub implementation for testing purposes. It adds tests for resolving file services based on storage providers and ensures proper fallback mechanisms for MinIO URLs. Additionally, the `rewriteStorageURLs` and `cleanIMContent` functions have been refactored to utilize a resolver for improved caching and efficiency. These changes enhance the robustness of file service handling and improve test coverage for various storage scenarios.
This commit introduces a new test, `TestFindIncompleteMarkdownImage`, to validate the detection of incomplete Markdown images in various scenarios. Additionally, it enhances the `holdbackCutoff` function to prioritize handling incomplete Markdown images, ensuring that they are correctly managed during stream flush operations. The changes improve the robustness of image processing in the application, addressing potential issues with unclosed image URLs in Markdown content.
This commit introduces unit tests for the `parseCosObjectName` method in the `cosFileService`, ensuring it correctly rejects local scheme URLs and properly parses COS scheme URLs. Additionally, the `parseCosObjectName` method has been updated to return an error for unsupported schemes, improving error handling in the `GetFile` and `DeleteFile` methods. This enhancement ensures more robust handling of file paths in the application.
Expose copyable resource IDs in edit modals and replace the intent prompt
dropdown with independent toggle buttons so multi-intent selection wraps cleanly.
This commit introduces the CancelOpenSpansByName method in the KnowledgeSpanRepository, allowing for the cancellation of open spans by their name for a specific knowledge ID and attempt. This functionality is crucial for managing spans during retries or server restarts, preventing duplicate entries in the trace tree. Additionally, a new test case, TestKnowledgeSpanRepo_CancelOpenSpansByName, has been added to ensure the correct behavior of this method, verifying that only the intended spans are cancelled while others remain unaffected. This enhancement improves the robustness of span management in the application.
This commit refines the language used in the knowledge parsing documentation and user interface. Key changes include:
- Updated the description of the `finalizing` state to clarify that it refers to ongoing optimization tasks rather than just completion.
- Modified the confirmation message for canceling parsing to replace "enhancement" with "optimization" for consistency across multiple languages.
- Enhanced the UI to better reflect the current parsing status, including a new function to display appropriate status messages during in-flight parsing.
These changes aim to improve user understanding and experience when interacting with the knowledge parsing features.
Lets users stop an in-flight document parse to free up LLM / worker
resources without losing the chunks and index already written. The
core insight is that the previous parse_status=completed flipped as
soon as primary chunks landed, while the most expensive subtasks
(graph extract = N LLM calls per chunk, plus summary, question
generation) were still running in the background — so "completed"
wasn't actually terminal from a resource standpoint.
State machine
pending -> processing -> finalizing -> completed
|
+-> cancelled (any of the three
in-flight states)
+-> failed
+-> deleting
`finalizing` is the new post-process fan-out window. parse_status
only promotes to `completed` once pending_subtasks_count (a new
column tracking summary + question + per-chunk graph extract)
drains to zero via atomic FinalizeSubtask. Wiki ingest is
intentionally excluded from the counter — it's a KB-scoped
debounced batch and would otherwise pin parse_status in
`finalizing` for the wiki batch window.
Backend
- New ParseStatusFinalizing + pending_subtasks_count column with
migration 000056.
- knowledgeRepository.SetFinalizing transitions processing -> finalizing
conditionally so a racing cancel cannot be clobbered.
- knowledgeRepository.FinalizeSubtask atomically decrements the
counter and self-promotes the row to completed when it hits zero.
- KnowledgePostProcess restructured to compute expected subtask
count up front, flip to finalizing (or completed when no
enrichment is enabled), and only then fan out subtasks. Subtask
handlers (summary, question, graph extract) defer-decrement on
terminal exit using the existing isFinalAsynqAttempt convention.
- New POST /api/v1/knowledge/{id}/cancel-parse handler accepting
pending / processing / finalizing. Marks the row cancelled,
zeroes the counter, best-effort dequeues asynq tasks via a new
TaskInspector abstraction (asynq-mode walks pending/scheduled/
retry queues; Lite-mode noop), and scrubs wiki ingest pending op.
- SpanTracker.AbortAttempt flat-sweeps every still-running span
for the attempt via a new repo.CancelAllOpenSpans helper so the
trace viewer's striped bars all flip to cancelled, even leaf
generations whose parent stage already EndSpan'd (multimodal
fan-out pattern). knowledge_post_process closes its postSpan
via SkipSpan on the cancel/deleting entry guard so a worker
that opens a span AFTER the cancel sweep doesn't leak it.
- Housekeeping and resetPendingTasks sweep finalizing rows
identically to processing so a crash/restart can't strand them.
- DeleteKnowledge/DeleteKnowledgeList proactively dequeue
downstream tasks via the same TaskInspector path.
- ChunkExtractService gets a cancel entry guard so the most
expensive enrichment (graph extract) bails immediately when the
parent knowledge is aborted.
Frontend
- New cancelKnowledgeParse API client + "Stop parsing" entry in
both list view and card view more menus, gated on
pending/processing/finalizing.
- Polling predicate refactored to a shared isParseInFlight helper
that recognises `finalizing` (previously the doc list silently
stopped polling once parse_status flipped from processing).
- Knowledge processing timeline: isPolling includes finalizing,
new isHardTerminal short-circuits LIVE for cancelled/failed/
completed so stranded child spans cannot pin LIVE on.
- DocumentListView.computeStatus distinguishes finalizing
("增强中") from completed and shows the previous "生成摘要中"
copy when summary_status is still pending under finalizing.
Added cancelled badge as well.
- i18n: statusFinalizing / statusCancelled / cancelParse* keys
across zh-CN, en-US, ko-KR, ru-RU.
Docs / SDK
- docs/api/knowledge.md: documents the new finalizing state,
cancel-parse semantics, and which statuses accept cancel.
- client (Go SDK): CancelKnowledgeParse with docstring listing
the cancellable statuses.
AGENTS.md gains three sections for the v0.8 surfaces:
- Stream recovery — session continue-stream replay-from-0 semantics
and the dedupe contract agents must implement
- Dry-run contract — when --dry-run applies, the meta.{dry_run,plan}
envelope shape, exit-code semantics (no exit 10
on destructive + --dry-run), the GET-reject rule
for `weknora api`, and the validation-parity
guarantee with the live path
- Risk metadata — what the Risk: prefix in --help means and how
cobra.Annotations["risk.{level,action}"] are
populated
README.md gains user-facing Dry-run preview and Resuming streams
sections.
CHANGELOG.md adds the v0.8 entry covering the new --dry-run flag,
MCP Tool.Annotations, session continue-stream, and the Risk: line.
Adds `weknora session continue-stream <session-id> --message <id>` for
re-attaching to an in-progress or already-completed SSE event buffer.
Server semantics (replay-from-0 + tail):
- Every connection replays the full stored event log from index 0,
then tails any new events. NOT cursor-from-disconnect. Agents that
already consumed events on the original stream MUST dedupe by
message_id + event hash to avoid double-processing.
- Buffer TTL: redis mode 1h hardcoded; memory mode = process lifetime.
After expiry the CLI surfaces local.sse_stream_aborted.
Output is NDJSON: one CLI-injected init line carrying
{session_id, message_id, profile} at stream head, then raw SDK
StreamResponse events verbatim. The init line lets agents thread the
resume to the original message in their dedupe table before the first
SDK frame arrives — output.InitEvent gains an omitempty MessageID field
for this purpose; non-resume init events stay unchanged.
The command always emits NDJSON regardless of --format — there is no
human-text use case for raw event-log replay (operator scenarios are
incident response / debugging). --dry-run is excluded for the same
reason streaming commands always are: a buffered plan makes no sense
for an event stream.
Two intertwined agent safety nets that share the same files:
1. --dry-run flag for offline preview of mutation commands
2. Risk: metadata + SetRisk helper for destructive command surfaces
Coverage (19 mutation commands with --dry-run):
kb.create/edit/delete agent.create/edit/delete
doc.create/upload/fetch/delete doc.delete_all (special variant)
session.delete chunk.delete profile.add/remove
auth.refresh/logout link unlink
api.{post,put,patch,delete} (api.get + --dry-run rejected, exit 2)
Envelope additions (omitempty in non-dry-run paths):
meta.dry_run: bool true when --dry-run was used
meta.plan: map {action, args} per the per-command taxonomy
Risk: metadata
--------------
cmdutil.SetRisk(cmd, action) stamps cobra.Annotations with
risk.level=destructive + risk.action=<action> on the 9 destructive
commands. The SetAgentHelp wrapper prepends a "Risk: <action>
(destructive)" line in the default text help path so agents see a clear
warning before parsing Usage. WEKNORA_AGENT_HELP=1 JSON path stays
unchanged — structured agent-help already carries warnings[].
Validation parity with the live path
------------------------------------
Every pure-local validation (flag presence, mutual exclusion, enum
bounds, URL/regex format, ResolveKBLocal for KB resolution that does not
require an SDK call) runs BEFORE the dry-run gate. This matches the
industry-standard "preview shows what live would do" contract:
--dry-run accepts exactly the same invocations the live path accepts and
rejects exactly the same invocations the live path rejects, modulo the
side-effecting work itself.
The side-effecting work (SDK calls, file writes, keyring writes, server-
side name → id resolution) is what --dry-run actually gates. Each
mutation file pairs its RunE validation block with a regression test
under *_dry_run_test.go / dryrun_validation_test.go so future refactors
don't reintroduce the gap.
Helper surface
--------------
- HandleDryRun(cmd, dryRun, plan) extracts the early-return so the
19 RunE call sites stay 3 lines each.
- EmitDryRun routes through FormatOptions.Emit, inheriting _notice /
TTY indent / --jq filtering for free.
- ResolveKBLocal mirrors ResolveKB but never calls the SDK; dry-run
paths use it so the plan reports the raw --kb value (UUID or name)
without a name → id lookup.
Streaming commands (chat, session ask, session continue-stream) are
deliberately excluded: a buffered plan makes no sense for an event
stream.
Lock semantics in the dry-run path:
- destructive + --dry-run: exit 0, no exit-10 confirmation prompt
- --dry-run + -y: byte-identical envelope to --dry-run alone
- --dry-run + --jq: filter applies to the preview envelope normally
Bumps modelcontextprotocol/go-sdk to v1.6.1 and populates Tool.Annotations
on every registered MCP tool per the per-tool hint table:
Read tools (8): destructiveHint=false, readOnlyHint=true,
idempotentHint=true, openWorldHint=false
Invoke tools (2): destructiveHint=false, readOnlyHint=false,
idempotentHint=false, openWorldHint=true
Invoke-class tools (chat, agent_invoke) carry openWorldHint=true because
the server may dispatch external skills (web_search etc.). Read tools are
sealed: idempotent + read-only + closed-world.
TestToolAnnotations_AllToolsHaveExpectedHints asserts the matrix so any
future drift surfaces in CI rather than at first client integration.
This commit introduces a new utility function, `knowledgeSpansPayloadHasTrace`, to determine if the knowledge spans data contains a valid trace. Key changes include:
1. Updated the `KnowledgeBase` component to utilize the new trace availability checks, improving the logic for displaying trace-related UI elements.
2. Enhanced the `fetchSpans` function in the `knowledge-processing-timeline` component to emit trace availability based on the new utility.
3. Implemented caching for trace availability to optimize performance and reduce unnecessary API calls.
These changes aim to improve user experience by providing accurate trace information and enhancing the overall responsiveness of the UI.
This commit introduces a resizable trace drawer in the `doc-content` component, allowing users to adjust the width for better visibility. Key changes include:
1. Added functionality to save and load the drawer width from local storage.
2. Implemented mouse events for resizing the drawer, enhancing user interaction.
3. Updated the UI to reflect the new drawer width dynamically.
4. Enhanced the trace entry button for improved accessibility and clarity.
These changes aim to improve user experience by providing a more flexible and user-friendly interface for trace inspection.
This commit enhances the polling mechanism in the KnowledgeProcessingTimeline component by introducing a new function, `shouldPollNow`, which clarifies the conditions under which polling should occur based on the `gracePoll` prop. The documentation for `gracePoll` has been expanded to provide clearer semantics for both user-visible and background mounts. Additionally, minor formatting adjustments were made to improve code readability. These changes aim to streamline the polling behavior and enhance the overall user experience.
This commit introduces several improvements to the knowledge processing timeline and related components. Key changes include:
1. Added a `gracePoll` prop to the `KnowledgeProcessingTimeline` component to manage polling behavior more effectively.
2. Enhanced the UI by displaying the document title in the drawer, improving user visibility of the current document context.
3. Implemented new CSS classes for better styling of the drawer title bar, ensuring a more polished appearance.
4. Updated the backend to support the new `WikiSpan` tracking, allowing for detailed monitoring of document processing stages.
These changes aim to improve user experience and provide better insights into the document processing workflow.
This commit introduces a new method to open the trace drawer directly from the card menu, enhancing user experience by allowing immediate access to trace details without navigating through the document detail drawer. The implementation includes updates to the `handleViewTrace` function to ensure the correct knowledge ID and parse status are set before opening the trace drawer. Additionally, minor adjustments were made to the UI for better consistency and clarity.
This commit introduces the `knowledge_processing_spans` table to track the progress of document parsing stages, enabling better visibility and error handling in the frontend. The schema includes fields for span details, status, and timestamps, along with necessary indexes for efficient querying. A rollback script is also provided to drop the table and its associated indexes if needed.
Three fixes in response to user feedback:
1. Span input disappearing on End/Fail
The Upsert's DoUpdates always listed input/output/metadata, so calls
that only set output (EndSpan) or only set error_* (FailSpan) wrote
NULL into input/metadata, clobbering whatever Begin had recorded.
Build the column list dynamically: skip input/output/metadata when
the incoming row's value is nil. nil now means "preserve existing"
(matches user's intuition "Begin recorded it, End shouldn't erase it").
2. Subspans not auto-expanded
Stages with children (multimodal.image[*], postprocess.summary,
postprocess.question, postprocess.graph.chunk[*]) required a click
on the ▸ caret to surface — easy to miss. On the FIRST successful
fetch per (knowledgeId × attempt), auto-expand any stage that has
children. Subsequent polls honor whatever the user has collapsed,
so manual collapse mid-parse stays collapsed.
3. Auto-poll still not firing
Force-arm the polling interval in onMounted regardless of state.
The per-tick callback decides whether to actually fetch based on
current parse_status — so the loop can never get stranded waiting
on a status transition that already happened. Added a console.debug
when the interval arms, so we can verify from DevTools console that
polling is actually running.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report: opened a parsing-in-progress doc, /spans was hit ONCE on
mount and then never again — but parse_status said 'processing' and
the LIVE badge was lit. The setTimeout-based recursive scheduler had
a fragile property: any unexpected throw or skipped tick between the
finally block and the schedule check could silently strand the loop
with no way to re-arm.
Refactor to a self-healing pattern:
- setInterval polling: a single 2s tick checks "should we be polling?
and is no fetch in flight?" — if both, fire fetchSpans. Decision is
re-evaluated every tick from current reactive state, so flipping
status from 'processing' to 'completed' between ticks naturally
stops further fetches.
- ensurePolling() helper is idempotent — calling it from fetchSpans's
finally, from a parse_status watcher, and from onMounted all just
arm the same single interval if not already running.
- watch(data.value.parse_status) acts as a watchdog: any time the
status flips into 'pending'/'processing', re-arm. Any time it flips
out, tear down. This is the belt-and-suspenders that ensures the
loop can't get stranded even if a future regression re-introduces
a missed-schedule path.
- fetchInFlight guard prevents overlapping fetches if a slow backend
takes longer than POLL_INTERVAL_MS to respond.
- Console warns on fetch failure — silent rejections is exactly what
hid this bug. With this in place we can verify the loop's health
from DevTools console.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report: opening an already-completed doc still shows "更新于 X 秒前"
ticking up forever, which is misleading — the data IS final, no
freshness concern exists. Re-gate the caption on isLive (was always
on) so it only appears while pending/processing. Failures pill is
still gated on the same isLive condition, which is the only state
where polling-failure feedback is meaningful.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User reported: spinner spinning + "更新于 X 秒前" continuously growing
without bound while parsing is in progress. The auto-poll loop IS
firing every 2s, but lastFetchedAt only updated on success — so when
the API returns success=false or errors, the caption silently aged
while the visible state suggested live polling.
- Move lastFetchedAt update into the finally block so it tracks
attempts (success or failure). The caption now bounces back to
"刚刚 / 1s 前 / 刚刚" every poll cycle, matching the spinner's
visual cadence.
- Track failedAttempts and lastFetchOk: when the last attempt failed
the timestamp shows in muted italic; when 2+ consecutive attempts
fail a small "⚠ 刷新失败" pill appears next to it with a tooltip
explaining the situation.
- New i18n keys fetchFailed / fetchFailedShort in zh-CN/en-US/ko-KR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Trace trigger was a giant card-style row with a green left strip
sitting above 文件名/摘要 — visually competed with document content
and felt out of place vs. the rest of the app's quiet density.
Trace entry:
- Replaces the big card with a compact rounded pill in the drawer
header, next to the file-type t-tag.
- Reads as a quiet secondary action — status dot + label + duration
+ chevron — at the same visual weight as the file-type tag.
- Status dot still pulses while parsing is in progress so the LIVE
signal survives the size cut.
- Hidden mount preserved so the pill's status/duration stays live
even before the user opens the secondary drawer.
Refresh UX:
- "更新于 X 秒前" caption now always visible (was hidden once polling
stopped). After parsing completes it tells the user how stale the
data is when they reopen the drawer hours later — the original
reason it was hidden ("ticks up forever") is exactly the staleness
signal one needs.
- LIVE badge tooltip simplified from "解析中,每 2 秒自动刷新({n}s 后)"
(cryptic countdown) to "解析进行中 — 每 2 秒自动刷新".
- Manual refresh icon now slow-spins in brand color while auto-poll
is active — users can tell at a glance "auto-refresh is on" without
needing the LIVE badge to interpret. Manual click still triggers
the fast spin.
- Drops the unused nextRefreshIn computed.
i18n: liveTooltip rewritten in zh-CN/en-US/ko-KR; new keys
autoRefreshOn / autoRefreshOff for the button title states.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The per-image multimodal subspan only captured image_url / enable_ocr /
enable_caption on input and chunk_id on output, so the trace viewer
could not answer "what did THIS image actually produce?" without
joining back to the chunks table.
Adds to the per-image span output:
- vlm_model_id (or "legacy_inline" for inline-config KBs)
- image_bytes (read size)
- ocr_prompt: "default" | "scanned_pdf"
- ocr_chars + ocr_preview (sanitized text, capped at 200 runes)
- caption_chars + caption_preview
- chunks_created (count of OCR/caption child chunks)
- indexed (true after BatchIndex completes)
- per-step error fields (read_error / ocr_error / caption_error /
skipped reason) when something fails
Also adds parent_chunk_id to the span input so the trace links back to
the text chunk this image hangs off — useful when a doc has hundreds
of inline images and you need to know WHERE in the text this one came
from.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The post-process stage closes in ~9ms (just enqueue work) but its async
subspans (postprocess.summary, postprocess.question, postprocess.graph)
keep producing rows for tens of seconds AFTER the root finalizes. The
old timeline used trace.duration_ms as the time axis maximum, which
clipped those subspan bars past the right edge.
Timeline:
- totalMs now always takes max(trace.duration_ms, observed-tail), so
the axis stretches to fit the latest descendant end regardless of
parse_status.
- Render a faint dashed wrapping outline behind a parent span when
its descendants extend past its own finished_at, so the postprocess
stage row visibly spans the full window without overloading the
9ms self-time bar.
- Tree expand/collapse caret bumped from 10 to 14px in a 16x16 hit
area; copy icons in detail panel bumped from 11/14 to 14/18px;
.kp-kv-copy button grown from 18 to 22px.
- Short input/output payloads (<= 8 entries / <= 600 bytes JSON)
auto-expand inline so users see the actual data without an extra
click; longer payloads keep the click-to-expand summary.
Span payloads (subspans only - root keeps the canonical identity, no
duplicate knowledge_id/kb_id/tenant_id on every child):
- extract.go: graph subspan output gains chunk_chars, chunk_preview,
sample_nodes, sample_relations.
- summary subspan output gains model_id, summary_preview.
- question subspan output gains model_id and a sample_question
captured from the first non-empty LLM response.
i18n: new key knowledgeStages.detail.includingChildren for the
wrapping-bar tooltip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend: summary, question, and graph-extract async tasks now record
real processing time as subspans hanging off the (closed) postprocess
stage, so the trace viewer no longer caps the postprocess row at the
~10 ms enqueue duration. Carries Attempt through SummaryGeneration /
QuestionGeneration / ExtractChunk payloads so cross-process workers
can resolve the right parent attempt.
Frontend: drawer now uses attach="body" so the secondary 820px detail
drawer escapes the 654px container of the main drawer; timeline
timestamps include date prefix (MM-DD, or YYYY-MM-DD across years);
"updated X ago" caption only shows during live polling.
Tests: 4 new cases covering postprocess subspan attaching under a
closed parent, missing-parent fallthrough, and Attempt JSON round-trip
on the three task payloads.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-tracker historical knowledge has zero rows in
knowledge_processing_spans but parse_status correctly reads
"completed" or "failed". The /spans handler was synthesizing five
"pending" placeholders unconditionally, so legacy completed documents
rendered as if they were stuck waiting in the queue forever.
buildSpanTree now takes parse_status and chooses the placeholder
status accordingly:
- ParseStatusCompleted -> done
- ParseStatusFailed -> failed
- everything else -> pending (existing behaviour)
Real rows always take precedence; this only changes what we put in
the gaps. So healthy in-flight parses (parse_status=processing,
some real rows, some still pending) keep showing pending placeholders
exactly as before — the synthesized "completed" inference only fires
when the parse already hit terminal state.
Adds TestBuildSpanTree_LegacyCompletedRendersAsDone covering both
the completed-legacy and failed-legacy branches.
The previous Langfuse-style waterfall had concrete UX problems visible
in the field: the status text column wrapped Chinese characters across
two lines, the root span name truncated mid-identifier, the inline
detail expansion shifted sibling rows out of order, and the attempt
selector was a plain HTML <select>.
This commit rebuilds the visual layer around three corrections:
- Status column removed entirely. Status is conveyed by the 8 px dot
and the bar color; the redundant Chinese label that was wrapping
vertically is gone. Header subtitle now reads "总耗时 6.2s · ✓ 已完成"
via knowledgeStages.total + knowledgeStages.status.<value>.
- Inline row-detail replaced by a right-side drawer absolutely
positioned inside the timeline shell (overflow: hidden). Clicking a
row no longer reorders later rows beneath it. The drawer slides via
CSS transform, ESC and × close it, clicking the selected row toggles.
- Attempt <select> dropdown replaced by a horizontal pill tab strip
with a per-attempt status glyph (✓ / ✗ / ●). attemptStatuses Map
populates lazily — one /spans?attempt=n fetch per missing tab on
mount — so multi-attempt history doesn't block the initial render.
Drawer body shows the rich span data the backend just started
emitting: timing (started / finished / duration), an error block when
failed/cancelled, and Input / Output / Metadata as a 2-column
humanized key/value table. Numbers use toLocaleString, booleans render
as ✓/✗, arrays/objects collapse to a summary with a per-key "Show
JSON" toggle. Existing copySpan retains the raw JSON export path.
Root span name is now localized via knowledgeStages.root ("Knowledge
processing" / "知识处理") instead of leaking the raw identifier
"knowledge_processing" — which previously truncated as
"knowledge_process…".
i18n: added knowledgeStages.total and knowledgeStages.detail.{started,
finished, duration, input, output, metadata, error, showJson, hideJson}
to en-US, zh-CN, ko-KR, ru-RU. Compact mode (used by the card hover
popover) is untouched.
The root span created by OpenAttempt was never closed: PostProcess only
ended the postprocess stage, so the root row stayed at status=running
forever even after parse_status flipped to completed/failed. The
timeline rendered "进行中" indefinitely on the root, defeating the
whole "is the document done" question the timeline is meant to answer.
- SpanTracker.FinalizeAttempt(kid, attempt, status, output, code, msg):
closes the root row idempotently. Re-closing a terminal root no-ops
so success / cascade-fail / dead-letter paths can fire without
coordination.
- PostProcess.Handle calls FinalizeAttempt(done) after EndSpan(postprocess)
on the success path. Async downstream work (summary/question/wiki/
graph) still records its own spans; their completion extends the
trace's wall-clock end-time but doesn't reopen the root.
- FailSpan auto-closes the root when a MAIN pipeline stage fails
(docreader / chunking / embedding / multimodal / postprocess).
Cascade-cancelled siblings stay closed-with-the-cascade as before.
- Dead-letter callback (router/task.go) accepts the SpanTracker via
DI and calls FinalizeAttempt(failed, TASK_TIMEOUT) when a
document-related task exhausts retries. The probe payload now
extracts the Attempt field that Document/Manual/PostProcess
payloads already thread through.
Stage spans were also being recorded with nil input/output, leaving
the new detail panel with timestamps only. Each Begin/End site now
emits useful work metrics:
- DocReader input: file_name, file_type, is_url, url
output: text_length, images_found, is_audio, pages
- Chunking input: chunks_planned
output: chunks_written, total_text_chars
- Embedding input: chunks_to_embed, model_id, dim
output: vectors_written, storage_bytes
- Multimodal input: image_count, enable_ocr, enable_caption
- PostProcess output: chunks_total, enqueued_summary, _question, _wiki, _graph
i18n: add knowledgeStages.root ("Knowledge processing") so the UI
can render a localized name instead of the raw span identifier.
The five-circle pipeline conveyed only "which stage we're on" — operators
asked for the actual span tree (root → stages → batches/images), the time
each span took relative to the whole run, and the recorded JSON in/out.
This commit rebuilds the visual layer to match Langfuse's trace view
without touching the data layer (polling, attempt selector, retry,
copy-span and watchers all unchanged).
- Three-column grid (name | status+duration | bar): each span is one
row, indented by depth. Status dot 8px, name monospace, duration
right-aligned. Bars are absolutely-positioned inside a shared
timeline lane, left/width computed from each span's
started_at / duration_ms relative to trace.started_at and the run's
end (or "now" while running).
- Time ruler with five ticks (0% → 100%) above the bars; total
duration label on the right.
- Sub-spans (embedding.batch[i], multimodal.image[i]) collapsed
under each stage by default behind a chevron toggle. Click any
row to expand an inline detail panel showing ISO timestamps,
status, error_code/message when failed, and input/output/metadata
JSON blocks plus a "Copy span JSON" action.
- Removed the parse_status gate in doc-content: the timeline is now
always rendered when a knowledge detail is open, so completed
documents also expose their recorded pipeline for audit. The
KnowledgeBase hover popover stays compact-mode-only and still
gates on parse_status (popover height matters there).
- Running bars grow visibly via a 1s ticker (cleared on unmount).
- Last-error block restricted to parse_status === failed.
- Added knowledgeStages.totalDuration to en-US / zh-CN / ko-KR / ru-RU.
Renders a five-segment timeline (DocReader → Chunking → Embedding →
Multimodal → PostProcess) above each document so users can see exactly
which stage a document is in and which one failed, instead of a flat
"Parsing..." spinner.
- New KnowledgeProcessingTimeline component (full + compact mode):
full mode lives at the top of doc-content for any document not yet
in `completed`; compact mode replaces the parsing/failed lines in
the card hover popover with a five-dot mini-progress.
- Polls GET /api/v1/knowledge/:id/spans every 2s while
parse_status in {pending, processing}; stops on terminal status or
unmount. Hover popover instances opt out of polling to avoid
amplifying requests on grid hover.
- Failed / cancelled steps expand into an error card with the raw
error_message, an error_code -> localized title + suggestion mapping
for the twelve canonical codes (DOCREADER_TIMEOUT, EMBEDDING_RATE_LIMIT,
...), and a "Copy details" button that pastes the failed span JSON
for support tickets.
- Attempt selector renders only when latest_attempt > 1 so reparse
history is browsable; the latest attempt is selected by default.
- Last-error block under the timeline includes a Retry button that
calls reparseKnowledge() and immediately re-fetches.
- i18n strings added under knowledgeStages for en-US, zh-CN, ko-KR,
ru-RU. ja-JP locale file does not exist in this repository.
- Introduced a new migration (000054) to create the knowledge_processing_spans table, which captures detailed progress of document parsing stages.
- The table includes fields for span hierarchy, status tracking, and metadata to enhance visibility into the parsing pipeline.
- Added indexes to optimize queries related to knowledge_id, attempt, and span relationships.
- Implemented rollback migration to safely drop the table and associated indexes if needed.