* feat(knowledge): flag documents that stop making progress
A document stuck in processing looked the same as one still working: a
spinner that polled forever, no hint of where it stopped, and, hours
later, a generic housekeeping failure.
- The batch and spans endpoints return last_activity_at for in-flight
rows: the later of updated_at and the newest span write. updated_at
only moves at stage transitions; span writes keep moving while a long
stage still works.
- After 20 minutes with no progress, the list and card views show
"may be stuck" with a tooltip. The timeline shows the stage it
stopped at and a stop-parsing button, plus a "last progress" stat.
Both pollers back off to 15s while everything in flight is stalled.
- When housekeeping fails a row, the error names the stalled stage and
the time of the last progress. It closes the row's open spans with
TASK_STALLED (the running stage fails, the rest are cancelled), so
the timeline shows where the run stopped instead of a spinner.
* fix(knowledge): locate stalls in finalizing and tell backlog from stuck
- Housekeeping reads the stall site from the latest attempt. It takes
the running stages, or, when none runs (post-process closes its stage
once enrichment is fanned out), the innermost running spans, mapped
back to their stage. Those spans fail with TASK_STALLED and are named
in the message; every stalled stage is listed in pipeline order. The
other open spans are cancelled. Every closed span gets a duration, and
the message leaves out the time when the heartbeat query failed.
- The spans tree prefers a TASK_STALLED failure for last_error over a
later subtask failure. current_stage falls back to the stage owning a
running subspan.
- Batch and spans responses flag rows quiet past the stall hint whose
work is still queued (waiting_in_queue), using the sweep's own probes.
At most 20 rows are probed per request, cached for a minute. The UI
shows these as queued, with no stop button in the banner.
* fix(knowledge): answer stall verdicts from one shared queue scan
- Probing a quiet document for a backlog rescanned the whole queue once
per document. When the document was absent, which is exactly the
stuck case, the scan ran to the end, and it read a Redis error as
"nothing queued". QueuedKnowledgeIDs now collects every referenced
knowledge ID in one pass and returns list errors. HousekeepingService
keeps that snapshot for a minute, shared across requests and
single-flight. A failed scan is not cached.
- With the per-request cap of 20 gone, every quiet row on a page gets a
verdict. The response carries it as stall_state (queued or stalled),
replacing waiting_in_queue. The UI marks a row as stuck only on a
"stalled" verdict. A row with no verdict, because the probe failed,
shows as ordinary processing instead of inviting a stop.
Refuse truncated tool calls, compact-and-retry once on overflow, and let
sandbox files be appended, paged, and batch-edited instead of rewritten.
Models can set context_window (default 200K) so compaction sizes itself
from the real window; agent max_iterations of -1 means unlimited.
* fix(models): show usage details when deletion is blocked
* fix(models): open the matching agent section and cap usage lists
Agent "open configuration" now lands on the tab that owns the binding
(VLM/ASR vs follow-ups), and delete failures keep the server message when
structured details cannot be parsed. Listed KB/agent rows are capped at 50
while totals still drive the deletion guard and dialog headings.
---------
Co-authored-by: Leon <leon_hfut@foxmail.com>
Co-authored-by: wizardchen <wizardchen@tencent.com>
Align product copy, API docs, Swagger descriptions, and i18n strings
with workspace terminology while keeping internal tenant identifiers
and headers unchanged for API compatibility.
Allow deployments to register users without auto-creating a personal tenant, expose creation policy via /auth/me, and guide tenantless accounts through a workspace onboarding flow.
Adds a five-segment progress model for the document parsing pipeline so
the UI (PR③) can render a timeline showing where each document is
(DocReader → Chunking → Embedding → Multimodal → PostProcess) and
which stage failed with what error code.
- New table `knowledge_processing_stages` (migration 000052) with one
row per (knowledge_id, stage). UPSERT on Begin/Done/Fail bumps an
attempt counter so re-parses don't lose history.
- StageTracker service exposes Begin/Done/Fail/Skip; all calls are
best-effort and never break the pipeline if persistence fails.
- Stable error codes (DOCREADER_TIMEOUT / EMBEDDING_RATE_LIMIT /
VECTORSTORE_WRITE_FAILED / ...) the UI can map to localized
remediation hints.
- Tracker call sites added at the four meaningful failure points:
convert (DocReader), CreateChunks (Chunking), BatchIndex (Embedding),
enqueueImageMultimodalTasks (Multimodal start),
KnowledgePostProcess.Handle (Multimodal close + PostProcess).
- New endpoint `GET /api/v1/knowledge/:id/stages` returns the five
canonical stages — missing rows are synthesized as "pending" so
the timeline always renders five segments. Includes current_stage
and last_error block.
Added a new configuration option to limit the number of tenants a non-superuser can create via self-service. Introduced a new error type for handling cases where users exceed this limit, returning a 429 status code. Updated the tenant creation and update handlers to enforce this limit and provide appropriate feedback to users. Additionally, refactored the tenant update request structure to ensure only mutable fields are allowed.
Refs: #1303
Follow-up to the route-level KB-access guard introduced earlier in
this PR. Five reviewer findings, all fixed in middleware + tests
without changing handler call sites:
- Honour `?agent_id` in the agent-share fallback. The in-handler
resolution in knowledgebase.go branches on a specific agent's
KBSelectionMode (all / selected / none); the guard was only ever
calling TenantCanAccessKBViaSomeSharedAgent, which let some
requests pass at the guard layer that the handler would then
reject — and rewrote the request's tenant context to the source
tenant before the rejection happened. The guard now mirrors the
handler: a specific agent_id pins the resolution to that agent
(no fallback to any-agent), an empty agent_id keeps the existing
any-agent behaviour. The knowledge.go / knowledgebase.go handler
helpers stay in place as defence in depth until they're folded
into the guard in a follow-up.
- Distinguish not-found vs. transient on the knowledge / chunk
resolvers. A DB hiccup used to surface as a 404, both confusing
clients and hiding the underlying failure from monitoring. The
resolvers now return apperror 404 only for the known not-found
sentinels (apprepo.ErrKnowledgeBaseNotFound, ErrResourceNotFound)
and let other errors propagate; the guard maps those to a fresh
NewServiceUnavailableError (503).
- Stop 500-ing on legacy chunks with empty knowledge_base_id. The
chunk is effectively unresolvable to a KB, so the client gets the
same 404 they'd get for a missing chunk instead of a 500 that
pollutes alerting. The warn log preserves the operator signal.
- Honour cfg.Tenant.EnableRBAC. The guard now mirrors RequireRole /
RequireOwnershipOrRole: with enforcement off, 401/403 paths log
the would-be rejection and pass through. 404 still fires either
way (a missing resource is not an authorisation event). Keeps the
rollout window safe — operators can flip enforcement on globally
without code changes elsewhere.
- Document the c.Keys vs. c.Request.Context() split. The guard
intentionally rewrites only the request context (handlers that
still run their own share resolution, currently knowledge.go /
knowledgebase.go, would otherwise see kb.TenantID == c.Keys
tenant and misclassify shared-Viewer access as Admin). The
package-level comment now spells this out so the migration of the
remaining handlers in a follow-up doesn't accidentally regress
`my_permission` rendering.
Tests:
- Existing four cases keep working; runGuard signature gained a
guardOpts knob.
- Eight new cases cover the agent-share resolution paths (any-agent
viewer, Editor required → must reject, specific agent in modes
all / selected-match / selected-miss / none, cross-tenant
mismatch) plus the EnableRBAC=false fail-open + 404-still-fires
pair.
Wires KnowledgeBase.VectorStoreID and the ownership-aware retrieve factory
into the user-facing knowledge-base lifecycle:
- POST /knowledge-bases validates the requested vector_store_id against
the caller's tenant scope and the engine registry. New error codes
ErrVectorStoreBindingInvalid (2200) and ErrVectorStoreUnavailable (2201)
distinguish the typed branches without echoing UUIDs to the client.
- GET / POST / PUT / PUT-pin responses embed the bound store's display
metadata (name, source, engine_type, status) without exposing any
connection credentials. Cross-tenant shared KBs receive a suppressed
payload (vector_store_id stripped, source="shared") so operator-chosen
store names cannot be enumerated across tenants.
- POST /knowledge-bases/copy synchronously rejects clones whose target
has a different embedding model or vector store, before the async
clone task is enqueued. The async clone worker re-applies the same
checks for defense in depth.
- DELETE /vector-stores/:id refuses to remove a store with bound KBs,
inside a transaction that row-locks the store on PostgreSQL and
serializes via WAL on SQLite. unregister-from-registry is wrapped in
defer/recover so a panic surfaces as a structured warning instead of
silently leaking a stale engine.
- vector_store_id is immutable after creation. The GORM <-:create tag
blocks every ORM update path; the service-layer DTO omits the field
entirely; a reflection-based regression test catches any future
maintainer who adds it back to either layer.
- Empty-string vector_store_id is normalized to nil at both the create
path and inside SharesStoreWith, so rows persisted by callers that
did not run Normalize first cannot trip false same-store comparisons.
Part of #993. Depends on #994 and #1310.