From 049a4c514ec0757dc68675314f687b5fa987f5ff Mon Sep 17 00:00:00 2001 From: Jyong Date: Wed, 2 Sep 2026 01:43:16 -0400 Subject: [PATCH] fix(knowledge-fs): show task object titles --- api/knowledge-fs-contract.lock.json | 4 +- api/services/knowledge_fs/product_dto.py | 1 + .../services/test_knowledge_fs_product_dto.py | 25 +++++++ ...026-09-02-background-task-object-titles.md | 30 ++++++++ .../api/src/background-task-handlers.test.ts | 72 ++++++++++++++++++- .../api/src/background-task-handlers.ts | 59 ++++++++++++++- .../api/src/background-task-routes.ts | 1 + .../packages/api/src/background-task.ts | 1 + .../api/src/document-write-handlers.ts | 1 + .../api/src/gateway-document-write.test.ts | 13 ++++ knowledge-fs/packages/api/src/index.ts | 1 + .../api/src/source-repository.test.ts | 50 +++++++++++++ .../packages/api/src/source-repository.ts | 46 ++++++++++++ .../api/console/knowledge-fs/types.gen.ts | 13 ++++ .../api/console/knowledge-fs/zod.gen.ts | 13 ++++ .../generated/api/service/types.gen.ts | 12 ++++ .../generated/api/service/zod.gen.ts | 12 ++++ .../new-rag/documents/__tests__/page.spec.tsx | 13 +++- .../documents/detail/__tests__/page.spec.tsx | 6 +- .../documents/detail/tasks/task-row.tsx | 10 ++- web/features/new-rag/documents/models.ts | 2 + .../documents/tasks/documents-drawer.tsx | 18 +++-- .../new-rag/documents/tasks/drawer-state.ts | 3 +- .../new-rag/knowledge-fs-task-error.ts | 3 + 24 files changed, 387 insertions(+), 22 deletions(-) create mode 100644 knowledge-fs/.harness/changes/2026-09-02-background-task-object-titles.md diff --git a/api/knowledge-fs-contract.lock.json b/api/knowledge-fs-contract.lock.json index 91050084398..83f637adfdd 100644 --- a/api/knowledge-fs-contract.lock.json +++ b/api/knowledge-fs-contract.lock.json @@ -1,7 +1,7 @@ { "schemaVersion": 5, - "subtreeTree": "4c8a98b2c31329e0dbaf7c5e05233afc05f2611b", - "openapiSha256": "e147709323f72bfb2296b03ee4535eedb3365bbd232847faef49467ea12a5416", + "subtreeTree": "b28f85a3efed562af70e5061cf5a7552394cd908", + "openapiSha256": "1214898a307c01993690cf239b0989be748cbf9b91437670f270220223407e06", "capabilityV2AuthManifestSha256": "e322a2fa779d1f40b95c54c1021cffecaec77abbd7b34899573dcdf4ff353109", "capabilityV2AuthTestVectorSha256": "ae0de37b1ff05c40f905cf17a7b410d8971acacf64db07d5ee3d6fecfa559ce3", "productOperationManifestSha256": "5d1241a83bcca12ebbd848928dd3cdda0d2ecaeb5f5336e955eb24a8c8db175b", diff --git a/api/services/knowledge_fs/product_dto.py b/api/services/knowledge_fs/product_dto.py index 1af08904532..2d76ce6847d 100644 --- a/api/services/knowledge_fs/product_dto.py +++ b/api/services/knowledge_fs/product_dto.py @@ -2066,6 +2066,7 @@ class KnowledgeFSBackgroundTaskResponse(ResponseModel): progress_percent: int = Field(ge=0, le=100, validation_alias=AliasChoices("progress_percent", "progressPercent")) progress_total: int = Field(ge=0, validation_alias=AliasChoices("progress_total", "progressTotal")) source_id: str | None = Field(default=None, validation_alias=AliasChoices("source_id", "sourceId")) + source_title: str | None = Field(default=None, validation_alias=AliasChoices("source_title", "sourceTitle")) state: Literal["canceled", "completed", "failed", "queued", "running"] task_kind: Literal["document", "document_bulk", "source"] = Field( validation_alias=AliasChoices("task_kind", "taskKind") diff --git a/api/tests/unit_tests/services/test_knowledge_fs_product_dto.py b/api/tests/unit_tests/services/test_knowledge_fs_product_dto.py index 54bf3f2b1b8..0c8005dbaf6 100644 --- a/api/tests/unit_tests/services/test_knowledge_fs_product_dto.py +++ b/api/tests/unit_tests/services/test_knowledge_fs_product_dto.py @@ -8,6 +8,7 @@ from pydantic import ValidationError from services.knowledge_fs.product_dto import ( KnowledgeFSBackgroundTaskListQuery, KnowledgeFSBackgroundTaskListResponse, + KnowledgeFSBackgroundTaskResponse, KnowledgeFSBadCaseCreatePayload, KnowledgeFSBadCaseUpdatePayload, KnowledgeFSBulkJobResponse, @@ -70,6 +71,30 @@ from services.knowledge_fs.product_dto import ( ) +def test_background_task_response_accepts_source_title_alias() -> None: + task = KnowledgeFSBackgroundTaskResponse.model_validate( + { + "canCancel": False, + "canRetry": False, + "createdAt": "2026-09-02T12:00:00Z", + "id": "018f0d60-7a49-7cc2-9c1b-5b36f18f2c50", + "knowledgeSpaceId": "018f0d60-7a49-7cc2-9c1b-5b36f18f2c40", + "operation": "source_sync", + "progressCompleted": 1, + "progressFailed": 0, + "progressPercent": 100, + "progressTotal": 1, + "sourceId": "018f0d60-7a49-7cc2-9c1b-5b36f18f2c60", + "sourceTitle": "Notion support SOP", + "state": "completed", + "taskKind": "source", + "updatedAt": "2026-09-02T12:01:00Z", + } + ) + + assert task.source_title == "Notion support SOP" + + def test_quality_replay_payload_requires_exactly_one_selection_mode() -> None: assert KnowledgeFSQualityReplayPayload(selection="all-active").selection == "all-active" assert KnowledgeFSQualityReplayPayload(golden_question_ids=["question-1"]).golden_question_ids == ["question-1"] diff --git a/knowledge-fs/.harness/changes/2026-09-02-background-task-object-titles.md b/knowledge-fs/.harness/changes/2026-09-02-background-task-object-titles.md new file mode 100644 index 00000000000..c522bf2d0a5 --- /dev/null +++ b/knowledge-fs/.harness/changes/2026-09-02-background-task-object-titles.md @@ -0,0 +1,30 @@ +# Background Task Object Titles + +## Problem + +The Task drawer displayed a single-document re-index as `Re-index · 1` because progress count +took precedence over the document title. Source sync rows depended on the Source list page already +being loaded; otherwise their one-item progress count was also presented as the object name. + +## Changes + +- Persist the logical-document title or asset filename in newly created re-index bulk-operation + items, so a single re-index has a durable display title. +- Add an optional `sourceTitle` to the background-task contract. The task handler resolves all + selected Source task names in one bounded, space-scoped repository query, rechecks content + grants, and treats title lookup as non-critical enrichment. +- Return the same Source title from cancel and retry responses. Deleting, unavailable, or + unauthorized Sources never disclose a name and do not make task listing unavailable. +- Prefer object names in both task drawer implementations. A true multi-document re-index may + still show its item count, while a single task without a resolvable name no longer labels itself + as `1`. + +## Verification + +- Focused KnowledgeFS background-task, Source repository, and re-index persistence tests cover + PostgreSQL and TiDB placeholders, one-query enrichment, permission filtering, enrichment + degradation, and durable filename capture. +- Focused document-list and document-detail UI tests cover single re-index filenames and Source + task-provided names when the Source list is not loaded. +- The Dify DTO test covers camel-case `sourceTitle` validation and the generated TypeScript API + contract. diff --git a/knowledge-fs/packages/api/src/background-task-handlers.test.ts b/knowledge-fs/packages/api/src/background-task-handlers.test.ts index 431dc8023fd..d38788d2664 100644 --- a/knowledge-fs/packages/api/src/background-task-handlers.test.ts +++ b/knowledge-fs/packages/api/src/background-task-handlers.test.ts @@ -1,4 +1,4 @@ -import type { AuthSubject } from "@knowledge/core"; +import type { AuthSubject, Source } from "@knowledge/core"; import { describe, expect, it, vi } from "vitest"; import { @@ -72,18 +72,24 @@ describe("background task handlers", () => { const listRecentRuns = vi.fn(async () => ({ items: [sourceRun({ createdAt: "2026-07-23T12:04:00.000Z" })], })); + const getMany = vi.fn(async () => [sourceRecord()]); const app = backgroundTaskApp({ bulkOperations, compilationJobs, documentTasks, sourceRepository: { listRecentRuns }, + sources: { getMany }, }); const response = await app.request(`/knowledge-spaces/${SPACE_ID}/background-tasks?limit=2`); expect(response.status).toBe(200); const body = await response.json(); expect(body.items).toEqual([ - expect.objectContaining({ id: SOURCE_RUN_ID, taskKind: "source" }), + expect.objectContaining({ + id: SOURCE_RUN_ID, + sourceTitle: "Notion support SOP", + taskKind: "source", + }), expect.objectContaining({ id: BULK_ID, taskKind: "document_bulk" }), ]); expect(body.nextCursor).toEqual(expect.any(String)); @@ -98,6 +104,42 @@ describe("background task handlers", () => { expect(listRecentRuns).toHaveBeenCalledWith( expect.objectContaining({ candidateGrants: ["scope:visible"], limit: 2 }), ); + expect(getMany).toHaveBeenCalledOnce(); + expect(getMany).toHaveBeenCalledWith({ ids: [SOURCE_ID], knowledgeSpaceId: SPACE_ID }); + }); + + it("does not disclose a source title outside the current content grants", async () => { + const app = backgroundTaskApp({ + sourceRepository: { listRecentRuns: vi.fn(async () => ({ items: [sourceRun()] })) }, + sources: { + getMany: vi.fn(async () => [sourceRecord({ permissionScope: ["scope:hidden"] })]), + }, + }); + + const response = await app.request(`/knowledge-spaces/${SPACE_ID}/background-tasks?limit=10`); + expect(response.status).toBe(200); + const body = await response.json(); + expect(body.items).toEqual([ + expect.objectContaining({ id: SOURCE_RUN_ID, sourceId: SOURCE_ID }), + ]); + expect(body.items[0]).not.toHaveProperty("sourceTitle"); + }); + + it("keeps task listing available when source title enrichment is unavailable", async () => { + const app = backgroundTaskApp({ + sourceRepository: { listRecentRuns: vi.fn(async () => ({ items: [sourceRun()] })) }, + sources: { + getMany: vi.fn(async () => { + throw new Error("source catalog unavailable"); + }), + }, + }); + + const response = await app.request(`/knowledge-spaces/${SPACE_ID}/background-tasks?limit=10`); + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual({ + items: [expect.objectContaining({ id: SOURCE_RUN_ID, sourceId: SOURCE_ID })], + }); }); it("does not re-emit a grouped document after its bulk task leaves the page", async () => { @@ -288,6 +330,7 @@ describe("background task handlers", () => { ); const retry = vi.fn(async () => sourceRun({ state: "queued" })); const app = backgroundTaskApp({ + sources: { getMany: vi.fn(async () => [sourceRecord()]) }, sourceWorkflows: { cancel, retry }, }); @@ -298,6 +341,7 @@ describe("background task handlers", () => { expect(canceled.status).toBe(200); await expect(canceled.json()).resolves.toMatchObject({ canRetry: true, + sourceTitle: "Notion support SOP", state: "canceled", taskKind: "source", }); @@ -306,7 +350,10 @@ describe("background task handlers", () => { { method: "POST" }, ); expect(retried.status).toBe(200); - await expect(retried.json()).resolves.toMatchObject({ state: "queued" }); + await expect(retried.json()).resolves.toMatchObject({ + sourceTitle: "Notion support SOP", + state: "queued", + }); expect(cancel).toHaveBeenCalledWith( expect.objectContaining({ knowledgeSpaceId: SPACE_ID, runId: SOURCE_RUN_ID }), ); @@ -730,6 +777,7 @@ function backgroundTaskApp(overrides: { readonly durableDeletions?: RegisterBackgroundTaskHandlersOptions["durableDeletions"]; readonly space?: { readonly id: string } | null; readonly sourceRepository?: object; + readonly sources?: object; readonly sourceWorkflows?: object; }) { const app = createKnowledgeGatewayApp(); @@ -763,6 +811,7 @@ function backgroundTaskApp(overrides: { ...(overrides.sourceRepository ? { sourceRepository: overrides.sourceRepository as never } : {}), + ...(overrides.sources ? { sources: overrides.sources as never } : {}), ...(overrides.sourceWorkflows ? { sourceWorkflows: overrides.sourceWorkflows as never } : {}), spaces: { get: vi.fn(async ({ id, tenantId }) => { @@ -892,6 +941,23 @@ function sourceRun(patch: Partial = {}): SourceWorkflowRun { }; } +function sourceRecord(patch: Partial = {}): Source { + return { + createdAt: "2026-07-23T12:00:00.000Z", + id: SOURCE_ID, + knowledgeSpaceId: SPACE_ID, + metadata: {}, + name: "Notion support SOP", + permissionScope: ["scope:visible"], + status: "active", + type: "web", + updatedAt: "2026-07-23T12:01:00.000Z", + uri: "https://example.com", + version: 1, + ...patch, + }; +} + function authorizationDecision() { return { accessContext: {} as never, diff --git a/knowledge-fs/packages/api/src/background-task-handlers.ts b/knowledge-fs/packages/api/src/background-task-handlers.ts index 616ccc0ea9f..4879af22f7b 100644 --- a/knowledge-fs/packages/api/src/background-task-handlers.ts +++ b/knowledge-fs/packages/api/src/background-task-handlers.ts @@ -16,7 +16,10 @@ import { } from "./background-task-routes"; import { type BulkOperationRepository, canReadBulkOperation } from "./bulk-operation"; import { summarizeBulkOperation } from "./bulk-operation-summary"; -import { currentCandidateGrants } from "./candidate-content-authorization"; +import { + candidatePermissionScopeAllows, + currentCandidateGrants, +} from "./candidate-content-authorization"; import { issueKnowledgeSpaceDurablePermission } from "./derived-result-authorization"; import type { DocumentCompilationJobStateMachine, @@ -40,6 +43,7 @@ import type { SourceProductWorkflowService, SourceWorkflowPrincipal, } from "./source-product-workflow"; +import type { SourceRepository } from "./source-repository"; type CandidateSource = "bulk" | "document" | "source"; @@ -59,6 +63,7 @@ export interface RegisterBackgroundTaskHandlersOptions { readonly durableDeletionJobs?: Pick | undefined; readonly durableDeletions?: DurableDeletionService | undefined; readonly sourceRepository?: SourceProductWorkflowRepository | undefined; + readonly sources?: Pick | undefined; readonly sourceWorkflows?: SourceProductWorkflowService | undefined; readonly spaces: KnowledgeSpaceRepository; } @@ -73,6 +78,7 @@ export function registerBackgroundTaskHandlers({ durableDeletionJobs, durableDeletions, sourceRepository, + sources, sourceWorkflows, spaces, }: RegisterBackgroundTaskHandlersOptions): void { @@ -176,6 +182,12 @@ export function registerBackgroundTaskHandlers({ compareCandidates, ); const selected = candidates.slice(0, query.limit); + const selectedTasks = await attachSourceTitles( + selected.map((candidate) => candidate.task), + sources, + grants, + params.id, + ); let next = advanceCursor(cursor, selected); if (documentCandidates.length === 0 && documentPage.items.length > 0) { const last = documentPage.items.at(-1); @@ -191,7 +203,7 @@ export function registerBackgroundTaskHandlers({ Boolean(documentPage.nextCursor || bulkPage.nextCursor || sourcePage.nextCursor); return context.json( { - items: selected.map((candidate) => candidate.task), + items: selectedTasks, ...(hasMore ? { nextCursor: encodeBackgroundTaskCursor(next) } : {}), }, 200, @@ -247,7 +259,9 @@ export function registerBackgroundTaskHandlers({ : await controlSourceTask({ action, context, + grants, knowledgeSpaceId: params.id, + sources, sourceWorkflows, taskId: params.taskId, }); @@ -444,7 +458,9 @@ async function controlSourceTask(input: { readonly action: "cancel" | "retry"; // biome-ignore lint/suspicious/noExplicitAny: bounded OpenAPI handler context readonly context: any; + readonly grants: readonly string[]; readonly knowledgeSpaceId: string; + readonly sources?: Pick | undefined; readonly sourceWorkflows?: SourceProductWorkflowService | undefined; readonly taskId: string; }): Promise { @@ -463,7 +479,44 @@ async function controlSourceTask(input: { knowledgeSpaceId: input.knowledgeSpaceId, runId: input.taskId, }); - return run ? sourceBackgroundTask(run) : null; + if (!run) return null; + const task = sourceBackgroundTask(run); + return ( + (await attachSourceTitles([task], input.sources, input.grants, input.knowledgeSpaceId))[0] ?? + task + ); +} + +async function attachSourceTitles( + tasks: readonly BackgroundTask[], + sources: Pick | undefined, + candidateGrants: readonly string[], + knowledgeSpaceId: string, +): Promise { + if (!sources) return [...tasks]; + const sourceIds = [...new Set(tasks.flatMap((task) => (task.sourceId ? [task.sourceId] : [])))]; + if (sourceIds.length === 0) return [...tasks]; + + try { + const visibleSourceTitles = new Map( + ( + await sources.getMany({ + ids: sourceIds, + knowledgeSpaceId, + }) + ) + .filter((source) => candidatePermissionScopeAllows(source.permissionScope, candidateGrants)) + .map((source) => [source.id, source.name] as const), + ); + return tasks.map((task) => { + const sourceTitle = task.sourceId ? visibleSourceTitles.get(task.sourceId) : undefined; + return sourceTitle ? { ...task, sourceTitle } : task; + }); + } catch { + // A title is display enrichment only; task control and visibility must remain available if the + // source catalog is temporarily unavailable. + return [...tasks]; + } } async function controlPermission( diff --git a/knowledge-fs/packages/api/src/background-task-routes.ts b/knowledge-fs/packages/api/src/background-task-routes.ts index 17e58dc6549..8c29ecd2415 100644 --- a/knowledge-fs/packages/api/src/background-task-routes.ts +++ b/knowledge-fs/packages/api/src/background-task-routes.ts @@ -80,6 +80,7 @@ export const BackgroundTaskSchema = z.object({ progressPercent: z.number().int().min(0).max(100), progressTotal: z.number().int().nonnegative(), sourceId: z.string().uuid().optional(), + sourceTitle: z.string().min(1).optional(), state: BackgroundTaskStateSchema, semanticEnrichment: z .object({ diff --git a/knowledge-fs/packages/api/src/background-task.ts b/knowledge-fs/packages/api/src/background-task.ts index 466fb18fccc..0432cf068de 100644 --- a/knowledge-fs/packages/api/src/background-task.ts +++ b/knowledge-fs/packages/api/src/background-task.ts @@ -55,6 +55,7 @@ export interface BackgroundTask { readonly progressPercent: number; readonly progressTotal: number; readonly sourceId?: string | undefined; + readonly sourceTitle?: string | undefined; readonly state: BackgroundTaskState; readonly semanticEnrichment?: DocumentSemanticEnrichmentProgress | undefined; readonly taskKind: BackgroundTaskKind; diff --git a/knowledge-fs/packages/api/src/document-write-handlers.ts b/knowledge-fs/packages/api/src/document-write-handlers.ts index 272d2ab0496..721ac9b3f9c 100644 --- a/knowledge-fs/packages/api/src/document-write-handlers.ts +++ b/knowledge-fs/packages/api/src/document-write-handlers.ts @@ -440,6 +440,7 @@ export function registerDocumentWriteHandlers({ bulkItems.push({ compilationJobId: compilationJob.id, documentId: logicalDocument?.id ?? asset.id, + documentTitle: logicalDocument?.title ?? asset.filename, requiredPermissionScope: requiredPermissionScopeForAsset(asset), status: "queued", }); diff --git a/knowledge-fs/packages/api/src/gateway-document-write.test.ts b/knowledge-fs/packages/api/src/gateway-document-write.test.ts index a4f56fbec97..d1d96f61b74 100644 --- a/knowledge-fs/packages/api/src/gateway-document-write.test.ts +++ b/knowledge-fs/packages/api/src/gateway-document-write.test.ts @@ -3627,9 +3627,14 @@ describe("document write gateway integration", () => { now: () => 1_777_777_000_000, repository: createInMemoryDocumentCompilationJobRepository({ maxJobs: 10 }), }); + const bulkOperations = createInMemoryBulkOperationRepository({ + maxItems: 10, + maxOperations: 10, + }); const app = createKnowledgeGateway({ adapter, auth: createTestAuthVerifier(), + bulkOperations, documentAssets: assets, documentCompilationJobs: compilationJobs, generateBulkUploadId: () => "bulk-reindex-1", @@ -3692,6 +3697,14 @@ describe("document write gateway integration", () => { ], total: 2, }); + const storedOperation = await bulkOperations.get({ + id: "bulk-reindex-1", + tenantId: "tenant-1", + }); + expect(storedOperation?.items[0]).toMatchObject({ + documentId: first.id, + documentTitle: "First.md", + }); const all = await app.request( "/knowledge-spaces/018f0d60-7a49-7cc2-9c1b-5b36f18f2c42/documents/bulk/reindex", diff --git a/knowledge-fs/packages/api/src/index.ts b/knowledge-fs/packages/api/src/index.ts index 0274e844cff..292e65dc6af 100644 --- a/knowledge-fs/packages/api/src/index.ts +++ b/knowledge-fs/packages/api/src/index.ts @@ -2219,6 +2219,7 @@ export function createKnowledgeGateway({ ...(durableDeletionService ? { durableDeletions: durableDeletionService } : {}), ...(sourceProduct ? { sourceRepository: sourceProduct.repository } : {}), ...(sourceProductWorkflows ? { sourceWorkflows: sourceProductWorkflows } : {}), + sources: sourceRepository, spaces, }); registerSourceHandlers({ diff --git a/knowledge-fs/packages/api/src/source-repository.test.ts b/knowledge-fs/packages/api/src/source-repository.test.ts index 65761a08c54..dfea00c1fab 100644 --- a/knowledge-fs/packages/api/src/source-repository.test.ts +++ b/knowledge-fs/packages/api/src/source-repository.test.ts @@ -54,6 +54,12 @@ describe("createInMemorySourceRepository", () => { await expect( repository.get({ id: created.id, knowledgeSpaceId: SPACE_A }), ).resolves.toMatchObject({ id: created.id }); + await expect( + repository.getMany({ ids: [created.id, "missing", created.id], knowledgeSpaceId: SPACE_A }), + ).resolves.toEqual([expect.objectContaining({ id: created.id, name: "Docs crawl" })]); + await expect( + repository.getMany({ ids: [created.id], knowledgeSpaceId: SPACE_B }), + ).resolves.toEqual([]); const updated = await repository.update({ id: created.id, @@ -190,6 +196,50 @@ describe("createDatabaseSourceRepository", () => { expect(calls[1]?.sql).not.toContain("<> 'deleting'"); }); + it.each(["postgres", "tidb"] as const)( + "batch-loads active sources in one space-scoped query for %s", + async (dialect) => { + const calls: DatabaseExecuteInput[] = []; + const firstId = "00000000-0000-4000-8000-000000000001"; + const secondId = "00000000-0000-4000-8000-000000000002"; + const repository = createDatabaseSourceRepository({ + database: createSchemaDatabaseAdapter({ + executor: async (input) => { + calls.push(input); + return { + rows: [ + sourceRow(firstId, { name: "First source" }), + sourceRow(secondId, { name: "Second source" }), + ], + rowsAffected: 2, + }; + }, + kind: dialect, + }), + }); + + await expect( + repository.getMany({ + ids: [secondId, firstId, secondId], + knowledgeSpaceId: SPACE_A, + }), + ).resolves.toEqual([ + expect.objectContaining({ id: firstId, name: "First source" }), + expect.objectContaining({ id: secondId, name: "Second source" }), + ]); + expect(calls).toHaveLength(1); + expect(calls[0]?.params).toEqual([SPACE_A, firstId, secondId]); + expect(calls[0]?.sql).toContain("status"); + expect(calls[0]?.sql).toContain("<> 'deleting'"); + expect(calls[0]?.sql).toContain("ORDER BY"); + if (dialect === "tidb") { + const call = calls[0]; + if (!call) throw new Error("Expected a batch source lookup query"); + expect(call.sql.match(/\?/g)).toHaveLength(call.params.length); + } + }, + ); + it.each(["postgres", "tidb"] as const)( "maps the lifecycle-only deleting status through getForDeletion for %s", async (dialect) => { diff --git a/knowledge-fs/packages/api/src/source-repository.ts b/knowledge-fs/packages/api/src/source-repository.ts index 8c69f8a8639..a3e24f5b0c6 100644 --- a/knowledge-fs/packages/api/src/source-repository.ts +++ b/knowledge-fs/packages/api/src/source-repository.ts @@ -41,6 +41,11 @@ export interface SourceLookupInput { readonly knowledgeSpaceId: string; } +export interface SourceBatchLookupInput { + readonly ids: readonly string[]; + readonly knowledgeSpaceId: string; +} + /** Internal row shape used only while a durable deletion is fenced. */ export type SourceForDeletion = Omit & { readonly status: Source["status"] | "deleting"; @@ -136,6 +141,8 @@ export interface SourceRepository { ): Promise; create(input: CreateSourceInput): Promise; get(input: SourceLookupInput): Promise; + /** Bounded, space-scoped lookup used to enrich task lists without per-row queries. */ + getMany(input: SourceBatchLookupInput): Promise; /** Internal durable-deletion lookup; includes a row already fenced as deleting. */ getForDeletion(input: SourceLookupInput): Promise; list(input: ListSourcesInput): Promise; @@ -246,6 +253,13 @@ export function createInMemorySourceRepository({ return source && source.knowledgeSpaceId === knowledgeSpaceId ? cloneSource(source) : null; }, + getMany: async ({ ids, knowledgeSpaceId }) => { + const requestedIds = normalizeSourceBatchLookupIds(ids); + return requestedIds.flatMap((id) => { + const source = sources.get(id); + return source?.knowledgeSpaceId === knowledgeSpaceId ? [cloneSource(source)] : []; + }); + }, getForDeletion: async ({ id, knowledgeSpaceId }) => { const source = sources.get(id); @@ -517,6 +531,30 @@ export function createDatabaseSourceRepository({ return result.rows[0] ? mapDatabaseSourceRow(result.rows[0]) : source; }, get: async (input) => databaseSourceGet(database, input), + getMany: async ({ ids, knowledgeSpaceId }) => { + const requestedIds = normalizeSourceBatchLookupIds(ids); + if (requestedIds.length === 0) return []; + const params = [knowledgeSpaceId, ...requestedIds] satisfies readonly DatabaseQueryValue[]; + const result = await database.execute({ + maxRows: requestedIds.length, + operation: "select", + params, + sql: `SELECT * FROM ${quoteDatabaseIdentifier(database, tableName)} WHERE ${quoteDatabaseIdentifier( + database, + "knowledge_space_id", + )} = ${databasePlaceholder(database, 1)} AND ${quoteDatabaseIdentifier( + database, + "id", + )} IN (${requestedIds + .map((_, index) => databasePlaceholder(database, index + 2)) + .join(", ")}) AND ${quoteDatabaseIdentifier( + database, + "status", + )} <> 'deleting' ORDER BY ${quoteDatabaseIdentifier(database, "id")} ASC;`, + tableName, + }); + return result.rows.map(mapDatabaseSourceRow).map(cloneSource); + }, getForDeletion: async (input) => databaseSourceGetForDeletion(database, input), list: async ({ cursor, knowledgeSpaceId, limit }) => { validateSourceListLimit(limit); @@ -790,3 +828,11 @@ function validateSourceListLimit(limit: number): void { throw new Error("Source list limit must be at least 1"); } } + +function normalizeSourceBatchLookupIds(ids: readonly string[]): string[] { + const uniqueIds = [...new Set(ids)].sort((left, right) => left.localeCompare(right)); + if (uniqueIds.length > 100) { + throw new Error("Source batch lookup exceeds 100 ids"); + } + return uniqueIds; +} diff --git a/packages/contracts/generated/api/console/knowledge-fs/types.gen.ts b/packages/contracts/generated/api/console/knowledge-fs/types.gen.ts index 73b0fd20abc..84a14214ae7 100644 --- a/packages/contracts/generated/api/console/knowledge-fs/types.gen.ts +++ b/packages/contracts/generated/api/console/knowledge-fs/types.gen.ts @@ -174,6 +174,7 @@ export type KnowledgeFsBackgroundTaskResponse = { progress_percent: number progress_total: number source_id?: string | null + source_title?: string | null state: 'canceled' | 'completed' | 'failed' | 'queued' | 'running' task_kind: 'document' | 'document_bulk' | 'source' updated_at: string @@ -866,6 +867,8 @@ export type KnowledgeFsSourceWorkflowResponse = { | 'RESEARCH_TASK_FAILED' | 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID' | 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID' + | 'RETRIEVAL_DELETION_IN_PROGRESS' + | 'RETRIEVAL_EXECUTION_LEASE_LOST' | 'SOURCE_BULK_ACTION_FAILED' | 'SOURCE_CREDENTIAL_CONFIG_INVALID' | 'SOURCE_CREDENTIAL_MUTATION_FAILED' @@ -886,6 +889,7 @@ export type KnowledgeFsSourceWorkflowResponse = { | 'SOURCE_SECRET_INTEGRITY_FAILED' | 'SOURCE_SECRET_REF_CONFLICT' | 'SOURCE_SYNC_FAILED' + | 'SOURCE_SYNC_SELECTION_MISMATCH' | 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID' | 'SOURCE_WEBSITE_CRAWL_FAILED' | 'SOURCE_WORKFLOW_FAILED' @@ -1093,6 +1097,8 @@ export type KnowledgeFsSourceCredentialTestResponse = { | 'RESEARCH_TASK_FAILED' | 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID' | 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID' + | 'RETRIEVAL_DELETION_IN_PROGRESS' + | 'RETRIEVAL_EXECUTION_LEASE_LOST' | 'SOURCE_BULK_ACTION_FAILED' | 'SOURCE_CREDENTIAL_CONFIG_INVALID' | 'SOURCE_CREDENTIAL_MUTATION_FAILED' @@ -1113,6 +1119,7 @@ export type KnowledgeFsSourceCredentialTestResponse = { | 'SOURCE_SECRET_INTEGRITY_FAILED' | 'SOURCE_SECRET_REF_CONFLICT' | 'SOURCE_SYNC_FAILED' + | 'SOURCE_SYNC_SELECTION_MISMATCH' | 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID' | 'SOURCE_WEBSITE_CRAWL_FAILED' | 'SOURCE_WORKFLOW_FAILED' @@ -1449,6 +1456,8 @@ export type KnowledgeFsPublicFailureResponse = { | 'RESEARCH_TASK_FAILED' | 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID' | 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID' + | 'RETRIEVAL_DELETION_IN_PROGRESS' + | 'RETRIEVAL_EXECUTION_LEASE_LOST' | 'SOURCE_BULK_ACTION_FAILED' | 'SOURCE_CREDENTIAL_CONFIG_INVALID' | 'SOURCE_CREDENTIAL_MUTATION_FAILED' @@ -1469,6 +1478,7 @@ export type KnowledgeFsPublicFailureResponse = { | 'SOURCE_SECRET_INTEGRITY_FAILED' | 'SOURCE_SECRET_REF_CONFLICT' | 'SOURCE_SYNC_FAILED' + | 'SOURCE_SYNC_SELECTION_MISMATCH' | 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID' | 'SOURCE_WEBSITE_CRAWL_FAILED' | 'SOURCE_WORKFLOW_FAILED' @@ -1985,6 +1995,8 @@ export type KnowledgeFsSourceImportFailureResponse = { | 'RESEARCH_TASK_FAILED' | 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID' | 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID' + | 'RETRIEVAL_DELETION_IN_PROGRESS' + | 'RETRIEVAL_EXECUTION_LEASE_LOST' | 'SOURCE_BULK_ACTION_FAILED' | 'SOURCE_CREDENTIAL_CONFIG_INVALID' | 'SOURCE_CREDENTIAL_MUTATION_FAILED' @@ -2005,6 +2017,7 @@ export type KnowledgeFsSourceImportFailureResponse = { | 'SOURCE_SECRET_INTEGRITY_FAILED' | 'SOURCE_SECRET_REF_CONFLICT' | 'SOURCE_SYNC_FAILED' + | 'SOURCE_SYNC_SELECTION_MISMATCH' | 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID' | 'SOURCE_WEBSITE_CRAWL_FAILED' | 'SOURCE_WORKFLOW_FAILED' diff --git a/packages/contracts/generated/api/console/knowledge-fs/zod.gen.ts b/packages/contracts/generated/api/console/knowledge-fs/zod.gen.ts index 325607e79de..03568aa9d09 100644 --- a/packages/contracts/generated/api/console/knowledge-fs/zod.gen.ts +++ b/packages/contracts/generated/api/console/knowledge-fs/zod.gen.ts @@ -952,6 +952,8 @@ export const zKnowledgeFsPublicFailureResponse = z.object({ 'RESEARCH_TASK_FAILED', 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID', 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID', + 'RETRIEVAL_DELETION_IN_PROGRESS', + 'RETRIEVAL_EXECUTION_LEASE_LOST', 'SOURCE_BULK_ACTION_FAILED', 'SOURCE_CREDENTIAL_CONFIG_INVALID', 'SOURCE_CREDENTIAL_MUTATION_FAILED', @@ -972,6 +974,7 @@ export const zKnowledgeFsPublicFailureResponse = z.object({ 'SOURCE_SECRET_INTEGRITY_FAILED', 'SOURCE_SECRET_REF_CONFLICT', 'SOURCE_SYNC_FAILED', + 'SOURCE_SYNC_SELECTION_MISMATCH', 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID', 'SOURCE_WEBSITE_CRAWL_FAILED', 'SOURCE_WORKFLOW_FAILED', @@ -1055,6 +1058,8 @@ export const zKnowledgeFsSourceWorkflowResponse = z.object({ 'RESEARCH_TASK_FAILED', 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID', 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID', + 'RETRIEVAL_DELETION_IN_PROGRESS', + 'RETRIEVAL_EXECUTION_LEASE_LOST', 'SOURCE_BULK_ACTION_FAILED', 'SOURCE_CREDENTIAL_CONFIG_INVALID', 'SOURCE_CREDENTIAL_MUTATION_FAILED', @@ -1075,6 +1080,7 @@ export const zKnowledgeFsSourceWorkflowResponse = z.object({ 'SOURCE_SECRET_INTEGRITY_FAILED', 'SOURCE_SECRET_REF_CONFLICT', 'SOURCE_SYNC_FAILED', + 'SOURCE_SYNC_SELECTION_MISMATCH', 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID', 'SOURCE_WEBSITE_CRAWL_FAILED', 'SOURCE_WORKFLOW_FAILED', @@ -1170,6 +1176,8 @@ export const zKnowledgeFsSourceCredentialTestResponse = z.object({ 'RESEARCH_TASK_FAILED', 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID', 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID', + 'RETRIEVAL_DELETION_IN_PROGRESS', + 'RETRIEVAL_EXECUTION_LEASE_LOST', 'SOURCE_BULK_ACTION_FAILED', 'SOURCE_CREDENTIAL_CONFIG_INVALID', 'SOURCE_CREDENTIAL_MUTATION_FAILED', @@ -1190,6 +1198,7 @@ export const zKnowledgeFsSourceCredentialTestResponse = z.object({ 'SOURCE_SECRET_INTEGRITY_FAILED', 'SOURCE_SECRET_REF_CONFLICT', 'SOURCE_SYNC_FAILED', + 'SOURCE_SYNC_SELECTION_MISMATCH', 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID', 'SOURCE_WEBSITE_CRAWL_FAILED', 'SOURCE_WORKFLOW_FAILED', @@ -1248,6 +1257,7 @@ export const zKnowledgeFsBackgroundTaskResponse = z.object({ progress_percent: z.int().gte(0).lte(100), progress_total: z.int().gte(0), source_id: z.string().nullish(), + source_title: z.string().nullish(), state: z.enum(['canceled', 'completed', 'failed', 'queued', 'running']), task_kind: z.enum(['document', 'document_bulk', 'source']), updated_at: z.iso.datetime(), @@ -1936,6 +1946,8 @@ export const zKnowledgeFsSourceImportFailureResponse = z.object({ 'RESEARCH_TASK_FAILED', 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID', 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID', + 'RETRIEVAL_DELETION_IN_PROGRESS', + 'RETRIEVAL_EXECUTION_LEASE_LOST', 'SOURCE_BULK_ACTION_FAILED', 'SOURCE_CREDENTIAL_CONFIG_INVALID', 'SOURCE_CREDENTIAL_MUTATION_FAILED', @@ -1956,6 +1968,7 @@ export const zKnowledgeFsSourceImportFailureResponse = z.object({ 'SOURCE_SECRET_INTEGRITY_FAILED', 'SOURCE_SECRET_REF_CONFLICT', 'SOURCE_SYNC_FAILED', + 'SOURCE_SYNC_SELECTION_MISMATCH', 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID', 'SOURCE_WEBSITE_CRAWL_FAILED', 'SOURCE_WORKFLOW_FAILED', diff --git a/packages/contracts/generated/api/service/types.gen.ts b/packages/contracts/generated/api/service/types.gen.ts index d6ba23ec2c7..1ef6fc305cd 100644 --- a/packages/contracts/generated/api/service/types.gen.ts +++ b/packages/contracts/generated/api/service/types.gen.ts @@ -1687,6 +1687,8 @@ export type KnowledgeFsPublicFailureResponse = { | 'RESEARCH_TASK_FAILED' | 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID' | 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID' + | 'RETRIEVAL_DELETION_IN_PROGRESS' + | 'RETRIEVAL_EXECUTION_LEASE_LOST' | 'SOURCE_BULK_ACTION_FAILED' | 'SOURCE_CREDENTIAL_CONFIG_INVALID' | 'SOURCE_CREDENTIAL_MUTATION_FAILED' @@ -1707,6 +1709,7 @@ export type KnowledgeFsPublicFailureResponse = { | 'SOURCE_SECRET_INTEGRITY_FAILED' | 'SOURCE_SECRET_REF_CONFLICT' | 'SOURCE_SYNC_FAILED' + | 'SOURCE_SYNC_SELECTION_MISMATCH' | 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID' | 'SOURCE_WEBSITE_CRAWL_FAILED' | 'SOURCE_WORKFLOW_FAILED' @@ -1977,6 +1980,8 @@ export type KnowledgeFsSourceCredentialTestResponse = { | 'RESEARCH_TASK_FAILED' | 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID' | 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID' + | 'RETRIEVAL_DELETION_IN_PROGRESS' + | 'RETRIEVAL_EXECUTION_LEASE_LOST' | 'SOURCE_BULK_ACTION_FAILED' | 'SOURCE_CREDENTIAL_CONFIG_INVALID' | 'SOURCE_CREDENTIAL_MUTATION_FAILED' @@ -1997,6 +2002,7 @@ export type KnowledgeFsSourceCredentialTestResponse = { | 'SOURCE_SECRET_INTEGRITY_FAILED' | 'SOURCE_SECRET_REF_CONFLICT' | 'SOURCE_SYNC_FAILED' + | 'SOURCE_SYNC_SELECTION_MISMATCH' | 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID' | 'SOURCE_WEBSITE_CRAWL_FAILED' | 'SOURCE_WORKFLOW_FAILED' @@ -2106,6 +2112,8 @@ export type KnowledgeFsSourceImportFailureResponse = { | 'RESEARCH_TASK_FAILED' | 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID' | 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID' + | 'RETRIEVAL_DELETION_IN_PROGRESS' + | 'RETRIEVAL_EXECUTION_LEASE_LOST' | 'SOURCE_BULK_ACTION_FAILED' | 'SOURCE_CREDENTIAL_CONFIG_INVALID' | 'SOURCE_CREDENTIAL_MUTATION_FAILED' @@ -2126,6 +2134,7 @@ export type KnowledgeFsSourceImportFailureResponse = { | 'SOURCE_SECRET_INTEGRITY_FAILED' | 'SOURCE_SECRET_REF_CONFLICT' | 'SOURCE_SYNC_FAILED' + | 'SOURCE_SYNC_SELECTION_MISMATCH' | 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID' | 'SOURCE_WEBSITE_CRAWL_FAILED' | 'SOURCE_WORKFLOW_FAILED' @@ -2308,6 +2317,8 @@ export type KnowledgeFsSourceWorkflowResponse = { | 'RESEARCH_TASK_FAILED' | 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID' | 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID' + | 'RETRIEVAL_DELETION_IN_PROGRESS' + | 'RETRIEVAL_EXECUTION_LEASE_LOST' | 'SOURCE_BULK_ACTION_FAILED' | 'SOURCE_CREDENTIAL_CONFIG_INVALID' | 'SOURCE_CREDENTIAL_MUTATION_FAILED' @@ -2328,6 +2339,7 @@ export type KnowledgeFsSourceWorkflowResponse = { | 'SOURCE_SECRET_INTEGRITY_FAILED' | 'SOURCE_SECRET_REF_CONFLICT' | 'SOURCE_SYNC_FAILED' + | 'SOURCE_SYNC_SELECTION_MISMATCH' | 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID' | 'SOURCE_WEBSITE_CRAWL_FAILED' | 'SOURCE_WORKFLOW_FAILED' diff --git a/packages/contracts/generated/api/service/zod.gen.ts b/packages/contracts/generated/api/service/zod.gen.ts index 8c7a90472d0..5c170947ace 100644 --- a/packages/contracts/generated/api/service/zod.gen.ts +++ b/packages/contracts/generated/api/service/zod.gen.ts @@ -1944,6 +1944,8 @@ export const zKnowledgeFsPublicFailureResponse = z.object({ 'RESEARCH_TASK_FAILED', 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID', 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID', + 'RETRIEVAL_DELETION_IN_PROGRESS', + 'RETRIEVAL_EXECUTION_LEASE_LOST', 'SOURCE_BULK_ACTION_FAILED', 'SOURCE_CREDENTIAL_CONFIG_INVALID', 'SOURCE_CREDENTIAL_MUTATION_FAILED', @@ -1964,6 +1966,7 @@ export const zKnowledgeFsPublicFailureResponse = z.object({ 'SOURCE_SECRET_INTEGRITY_FAILED', 'SOURCE_SECRET_REF_CONFLICT', 'SOURCE_SYNC_FAILED', + 'SOURCE_SYNC_SELECTION_MISMATCH', 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID', 'SOURCE_WEBSITE_CRAWL_FAILED', 'SOURCE_WORKFLOW_FAILED', @@ -2318,6 +2321,8 @@ export const zKnowledgeFsSourceCredentialTestResponse = z.object({ 'RESEARCH_TASK_FAILED', 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID', 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID', + 'RETRIEVAL_DELETION_IN_PROGRESS', + 'RETRIEVAL_EXECUTION_LEASE_LOST', 'SOURCE_BULK_ACTION_FAILED', 'SOURCE_CREDENTIAL_CONFIG_INVALID', 'SOURCE_CREDENTIAL_MUTATION_FAILED', @@ -2338,6 +2343,7 @@ export const zKnowledgeFsSourceCredentialTestResponse = z.object({ 'SOURCE_SECRET_INTEGRITY_FAILED', 'SOURCE_SECRET_REF_CONFLICT', 'SOURCE_SYNC_FAILED', + 'SOURCE_SYNC_SELECTION_MISMATCH', 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID', 'SOURCE_WEBSITE_CRAWL_FAILED', 'SOURCE_WORKFLOW_FAILED', @@ -2481,6 +2487,8 @@ export const zKnowledgeFsSourceImportFailureResponse = z.object({ 'RESEARCH_TASK_FAILED', 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID', 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID', + 'RETRIEVAL_DELETION_IN_PROGRESS', + 'RETRIEVAL_EXECUTION_LEASE_LOST', 'SOURCE_BULK_ACTION_FAILED', 'SOURCE_CREDENTIAL_CONFIG_INVALID', 'SOURCE_CREDENTIAL_MUTATION_FAILED', @@ -2501,6 +2509,7 @@ export const zKnowledgeFsSourceImportFailureResponse = z.object({ 'SOURCE_SECRET_INTEGRITY_FAILED', 'SOURCE_SECRET_REF_CONFLICT', 'SOURCE_SYNC_FAILED', + 'SOURCE_SYNC_SELECTION_MISMATCH', 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID', 'SOURCE_WEBSITE_CRAWL_FAILED', 'SOURCE_WORKFLOW_FAILED', @@ -2683,6 +2692,8 @@ export const zKnowledgeFsSourceWorkflowResponse = z.object({ 'RESEARCH_TASK_FAILED', 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID', 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID', + 'RETRIEVAL_DELETION_IN_PROGRESS', + 'RETRIEVAL_EXECUTION_LEASE_LOST', 'SOURCE_BULK_ACTION_FAILED', 'SOURCE_CREDENTIAL_CONFIG_INVALID', 'SOURCE_CREDENTIAL_MUTATION_FAILED', @@ -2703,6 +2714,7 @@ export const zKnowledgeFsSourceWorkflowResponse = z.object({ 'SOURCE_SECRET_INTEGRITY_FAILED', 'SOURCE_SECRET_REF_CONFLICT', 'SOURCE_SYNC_FAILED', + 'SOURCE_SYNC_SELECTION_MISMATCH', 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID', 'SOURCE_WEBSITE_CRAWL_FAILED', 'SOURCE_WORKFLOW_FAILED', diff --git a/web/features/new-rag/documents/__tests__/page.spec.tsx b/web/features/new-rag/documents/__tests__/page.spec.tsx index 7f19304db71..21b923f2f55 100644 --- a/web/features/new-rag/documents/__tests__/page.spec.tsx +++ b/web/features/new-rag/documents/__tests__/page.spec.tsx @@ -250,6 +250,7 @@ const taskApiResponse = vi.hoisted(() => (item: BackgroundTask) => ({ progress_percent: item.progressPercent, progress_total: item.progressTotal ?? 1, source_id: item.sourceId ?? null, + source_title: item.sourceTitle ?? null, state: item.state === 'succeeded' ? 'completed' @@ -4125,17 +4126,24 @@ describe('DocumentsPage', () => { it('shows document, bulk re-index, and source tasks returned by the task list', async () => { const user = userEvent.setup() documentsQuery.data = { pages: [{ items: [document({})] }] } + sourcesQuery.data = { pages: [{ items: [] }] } tasksQuery.data = { pages: [ { items: [ task({ id: 'document-task', state: 'succeeded' }), - backgroundTask({ id: 'reindex-task', progressCompleted: 12, progressTotal: 12 }), + backgroundTask({ + documentId: 'document-1', + id: 'reindex-task', + progressCompleted: 1, + progressTotal: 1, + }), backgroundTask({ errorMessage: 'Source sync failed', id: 'source-task', operation: 'source_sync', sourceId: 'source-1', + sourceTitle: 'Notion support SOP', state: 'failed', taskKind: 'source', }), @@ -4154,13 +4162,14 @@ describe('DocumentsPage', () => { const panel = screen.getByRole('dialog', { name: 'dataset.newKnowledge.backgroundTasks' }) expect(within(panel).getAllByRole('listitem')).toHaveLength(3) expect( - within(panel).getByText('dataset.newKnowledge.reindexDocuments · 12'), + within(panel).getByText('dataset.newKnowledge.reindexDocuments · sso-enterprise.pdf'), ).toBeInTheDocument() expect( within(panel).getByText( 'dataset.newKnowledge.overview.operation.source_sync · Notion support SOP', ), ).toBeInTheDocument() + expect(within(panel).queryByText(/ · 1$/)).not.toBeInTheDocument() expect(within(panel).getByText('dataset.newKnowledge.taskFailure.internal')).toBeInTheDocument() }) diff --git a/web/features/new-rag/documents/detail/__tests__/page.spec.tsx b/web/features/new-rag/documents/detail/__tests__/page.spec.tsx index fd3b785c7f0..a40125ea62c 100644 --- a/web/features/new-rag/documents/detail/__tests__/page.spec.tsx +++ b/web/features/new-rag/documents/detail/__tests__/page.spec.tsx @@ -238,6 +238,7 @@ const taskApiResponse = vi.hoisted(() => (item: BackgroundTask) => ({ completed_at: item.completedAt ?? null, created_at: item.createdAt, document_id: item.documentId ?? null, + document_title: item.documentTitle ?? null, document_revision: item.documentRevision ?? null, error_code: item.errorCode ?? null, error_message: item.errorMessage ?? null, @@ -250,6 +251,7 @@ const taskApiResponse = vi.hoisted(() => (item: BackgroundTask) => ({ progress_percent: item.progressPercent, progress_total: item.progressTotal ?? 1, source_id: item.sourceId ?? null, + source_title: item.sourceTitle ?? null, state: item.state === 'succeeded' ? 'completed' @@ -2034,7 +2036,7 @@ describe('DocumentDetailPage', () => { id: 'another-task', state: 'succeeded', }), - backgroundTask({ id: 'bulk-reindex-task' }), + backgroundTask({ documentId: 'document-1', id: 'bulk-reindex-task' }), ], }, ], @@ -2059,7 +2061,7 @@ describe('DocumentDetailPage', () => { }) expect(within(taskDrawer).getAllByText(/dataset\.newKnowledge\.addDocument/)).toHaveLength(2) expect( - within(taskDrawer).getByText('dataset.newKnowledge.reindexDocuments · 1'), + within(taskDrawer).getByText('dataset.newKnowledge.reindexDocuments · sso-enterprise.pdf'), ).toBeInTheDocument() }) diff --git a/web/features/new-rag/documents/detail/tasks/task-row.tsx b/web/features/new-rag/documents/detail/tasks/task-row.tsx index 00c075d178b..fd5abe5b312 100644 --- a/web/features/new-rag/documents/detail/tasks/task-row.tsx +++ b/web/features/new-rag/documents/detail/tasks/task-row.tsx @@ -117,7 +117,7 @@ export function DocumentTaskRow({ : task.operation === 'document_upload' ? `${t(($) => $['newKnowledge.addDocument'])}${progress ? ` · ${progress.total}` : ''}` : task.operation === 'document_reindex' - ? `${t(($) => $['newKnowledge.reindexDocuments'])}${progress ? ` · ${progress.total}` : resolvedDocumentTitle ? ` · ${resolvedDocumentTitle}` : ''}` + ? `${t(($) => $['newKnowledge.reindexDocuments'])}${resolvedDocumentTitle ? ` · ${resolvedDocumentTitle}` : progress && progress.total > 1 ? ` · ${progress.total}` : ''}` : task.operation === 'document_delete' && resolvedDocumentTitle ? `${operationTitle} · ${resolvedDocumentTitle}` : progress @@ -199,8 +199,12 @@ export function DocumentTaskRow({ } />
-

{title}

-

{status}

+

+ {title} +

+

+ {status} +

{taskError && (

{taskError} diff --git a/web/features/new-rag/documents/models.ts b/web/features/new-rag/documents/models.ts index 11fc37674ab..ce1038118f4 100644 --- a/web/features/new-rag/documents/models.ts +++ b/web/features/new-rag/documents/models.ts @@ -111,6 +111,7 @@ export type BackgroundTask = { | 'canceled' | 'superseded' sourceId?: string + sourceTitle?: string taskKind: KnowledgeFsBackgroundTaskResponse['task_kind'] updatedAt: string } @@ -268,6 +269,7 @@ export function backgroundTaskFromApi(task: KnowledgeFsBackgroundTaskResponse): ? 'canceled' : task.state, sourceId: task.source_id ?? undefined, + sourceTitle: task.source_title ?? undefined, taskKind: task.task_kind, updatedAt: task.updated_at, } diff --git a/web/features/new-rag/documents/tasks/documents-drawer.tsx b/web/features/new-rag/documents/tasks/documents-drawer.tsx index 083ffaa7a1e..3c0235c991d 100644 --- a/web/features/new-rag/documents/tasks/documents-drawer.tsx +++ b/web/features/new-rag/documents/tasks/documents-drawer.tsx @@ -204,14 +204,16 @@ function useDocumentsTaskRowTitle(task: BackgroundTask) { : task.operation === 'document_upload' ? `${t(($) => $['newKnowledge.addDocument'])}${progress ? ` · ${progress.total}` : ''}` : task.operation === 'document_reindex' - ? `${t(($) => $['newKnowledge.reindexDocuments'])}${progress ? ` · ${progress.total}` : documentTitle ? ` · ${documentTitle}` : ''}` + ? `${t(($) => $['newKnowledge.reindexDocuments'])}${documentTitle ? ` · ${documentTitle}` : progress && progress.total > 1 ? ` · ${progress.total}` : ''}` : task.operation === 'document_delete' && documentTitle ? `${operationTitle} · ${documentTitle}` : sourceTitle ? `${operationTitle} · ${sourceTitle}` - : progress - ? `${operationTitle} · ${progress.total}` - : operationTitle + : task.operation === 'source_sync' + ? operationTitle + : progress + ? `${operationTitle} · ${progress.total}` + : operationTitle } function DocumentsTaskDetails({ task }: { task: BackgroundTask }) { @@ -266,8 +268,12 @@ function DocumentsTaskDetails({ task }: { task: BackgroundTask }) { } />

-

{title}

-

{status}

+

+ {title} +

+

+ {status} +

{taskError && (

{taskError} diff --git a/web/features/new-rag/documents/tasks/drawer-state.ts b/web/features/new-rag/documents/tasks/drawer-state.ts index 4683fa8537b..d519add2f04 100644 --- a/web/features/new-rag/documents/tasks/drawer-state.ts +++ b/web/features/new-rag/documents/tasks/drawer-state.ts @@ -119,7 +119,8 @@ export const createTaskDrawerRowLabelsAtom = (task: BackgroundTask) => { documentTitlePending: Boolean( task.documentId && !task.documentTitle && get(taskDrawerDocumentsPendingAtom), ), - sourceTitle: task.sourceId ? get(sourceNamesAtom).get(task.sourceId) : undefined, + sourceTitle: + task.sourceTitle ?? (task.sourceId ? get(sourceNamesAtom).get(task.sourceId) : undefined), })) return selectAtom( labelsAtom, diff --git a/web/features/new-rag/knowledge-fs-task-error.ts b/web/features/new-rag/knowledge-fs-task-error.ts index 1762bcdaa1c..a213431fb8b 100644 --- a/web/features/new-rag/knowledge-fs-task-error.ts +++ b/web/features/new-rag/knowledge-fs-task-error.ts @@ -58,6 +58,8 @@ const failureMessageKeyByCode = { RESEARCH_TASK_FAILED: 'newKnowledge.taskFailure.research', RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID: 'newKnowledge.taskFailure.access', RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID: 'newKnowledge.taskFailure.research', + RETRIEVAL_DELETION_IN_PROGRESS: 'newKnowledge.taskFailure.conflict', + RETRIEVAL_EXECUTION_LEASE_LOST: 'newKnowledge.taskFailure.conflict', SOURCE_BULK_ACTION_FAILED: 'newKnowledge.taskFailure.source', SOURCE_CREDENTIAL_CONFIG_INVALID: 'newKnowledge.taskFailure.source', SOURCE_CREDENTIAL_MUTATION_FAILED: 'newKnowledge.taskFailure.source', @@ -78,6 +80,7 @@ const failureMessageKeyByCode = { SOURCE_SECRET_INTEGRITY_FAILED: 'newKnowledge.taskFailure.source', SOURCE_SECRET_REF_CONFLICT: 'newKnowledge.taskFailure.conflict', SOURCE_SYNC_FAILED: 'newKnowledge.taskFailure.source', + SOURCE_SYNC_SELECTION_MISMATCH: 'newKnowledge.taskFailure.conflict', SOURCE_WEBSITE_CRAWL_CONFIG_INVALID: 'newKnowledge.taskFailure.source', SOURCE_WEBSITE_CRAWL_FAILED: 'newKnowledge.taskFailure.source', SOURCE_WORKFLOW_FAILED: 'newKnowledge.taskFailure.source',