diff --git a/api/knowledge-fs-contract.lock.json b/api/knowledge-fs-contract.lock.json index ad89bc25797..7b582c1a27f 100644 --- a/api/knowledge-fs-contract.lock.json +++ b/api/knowledge-fs-contract.lock.json @@ -1,6 +1,6 @@ { "schemaVersion": 5, - "subtreeTree": "05e402ad162663945d482ebfa7c5221e73347364", + "subtreeTree": "b480632eddd1d3946a18d45472c0856bee471b58", "openapiSha256": "2cf348c68bbe65dd51bbde9a0a4f91398beeebd79e89e9288c9386b26ae09796", "capabilityV2AuthManifestSha256": "fc0a47e23cce12544882f0298522b4933002e892b84ce1815df7e81d36a7a0c7", "capabilityV2AuthTestVectorSha256": "ae0de37b1ff05c40f905cf17a7b410d8971acacf64db07d5ee3d6fecfa559ce3", diff --git a/knowledge-fs/.harness/changes/2026-08-19-query-activity-consolidation.md b/knowledge-fs/.harness/changes/2026-08-19-query-activity-consolidation.md new file mode 100644 index 00000000000..6e08d696ce7 --- /dev/null +++ b/knowledge-fs/.harness/changes/2026-08-19-query-activity-consolidation.md @@ -0,0 +1,28 @@ +# Query Activity Consolidation + +## Summary + +- Records one `query.requested` activity when a user starts a query, including the resolved + retrieval mode and a bounded copy of the question. +- Stops appending `query.completed`, `query.failed`, and `profile.published` activities. +- Excludes historical terminal-query and profile-publication rows from activity-feed reads while + retaining their storage decoding compatibility. +- Shows the question and retrieval mode directly in recent activity and the complete activity + drawer. + +## Safety and Compatibility + +- Query questions are limited to 4,000 characters in activity details; longer values are marked + as truncated. +- Credentials, tokens, object keys, and the legacy arbitrary `query` detail key remain excluded by + the activity-detail allow-list. +- Overview answer-rate and outcome calculations continue to use durable AnswerTrace and + FailedQuery facts rather than the removed terminal activity writes. + +## Verification + +- `pnpm --filter @knowledge/api test` +- `pnpm --filter @knowledge/api typecheck` +- `pnpm exec vitest run features/new-rag/__tests__/knowledge-overview-page.spec.tsx` (from `web/`) +- `pnpm type-check` (from `web/`) +- `git diff --check` diff --git a/knowledge-fs/packages/api/src/knowledge-space-overview-database-repository.test.ts b/knowledge-fs/packages/api/src/knowledge-space-overview-database-repository.test.ts index 67f429d8113..2b8a929a92f 100644 --- a/knowledge-fs/packages/api/src/knowledge-space-overview-database-repository.test.ts +++ b/knowledge-fs/packages/api/src/knowledge-space-overview-database-repository.test.ts @@ -123,16 +123,12 @@ describe.each(["postgres", "tidb"] as const)( JSON.stringify(["team:camera"]), ]); const sql = select?.sql ?? ""; - expect(sql).toContain("UNION ALL"); - expect(sql).toContain("answer_traces"); + expect(sql).not.toContain("UNION ALL"); + expect(sql).not.toContain("answer_traces"); + expect(sql).toContain("NOT IN"); expect(sql).toContain("query.completed"); expect(sql).toContain("query.failed"); - expect(sql).toMatch( - /stored_request\.[`"]actor_subject_id[`"] = stored_trace\.[`"]subject_id[`"]/u, - ); - expect(sql).toMatch( - /stored_trace\.[`"]created_at[`"] >= stored_request\.[`"]occurred_at[`"]/u, - ); + expect(sql).toContain("profile.published"); expect(sql.indexOf("tenant_id")).toBeLessThan(sql.indexOf("ORDER BY")); expect(sql.indexOf("knowledge_space_id")).toBeLessThan(sql.indexOf("ORDER BY")); expect(sql).toContain(dialect === "postgres" ? "::jsonb @>" : "JSON_CONTAINS"); diff --git a/knowledge-fs/packages/api/src/knowledge-space-overview-database-repository.ts b/knowledge-fs/packages/api/src/knowledge-space-overview-database-repository.ts index 9b2a20206c1..f6a005c12c0 100644 --- a/knowledge-fs/packages/api/src/knowledge-space-overview-database-repository.ts +++ b/knowledge-fs/packages/api/src/knowledge-space-overview-database-repository.ts @@ -23,6 +23,7 @@ import { import { lockKnowledgeSpaceForDeletionAdmission } from "./knowledge-space-deletion-admission"; import { type AppendKnowledgeSpaceActivityInput, + HiddenKnowledgeSpaceActivityActions, type KnowledgeSpaceActivityAction, KnowledgeSpaceActivityActions, type KnowledgeSpaceActivityEvent, @@ -1318,9 +1319,11 @@ function activityReadModelSql(database: DatabaseAdapter): string { const selectColumns = (alias: string) => columns.map((column) => `${alias}.${q(database, column)}`).join(", "); const activity = q(database, "knowledge_space_activity_events"); - const traces = q(database, "answer_traces"); + const hiddenActions = HiddenKnowledgeSpaceActivityActions.map((action) => `'${action}'`).join( + ", ", + ); - return `SELECT ${selectColumns("stored_event")} FROM ${activity} stored_event WHERE NOT (stored_event.${q(database, "action")} IN ('query.completed', 'query.failed') AND EXISTS (SELECT 1 FROM ${traces} stored_trace WHERE stored_trace.${q(database, "knowledge_space_id")} = stored_event.${q(database, "knowledge_space_id")} AND ${textIdSql(database, "stored_trace", "id")} = stored_event.${q(database, "resource_id")} AND EXISTS (SELECT 1 FROM ${activity} stored_request WHERE stored_request.${q(database, "tenant_id")} = stored_event.${q(database, "tenant_id")} AND stored_request.${q(database, "knowledge_space_id")} = stored_event.${q(database, "knowledge_space_id")} AND stored_request.${q(database, "resource_id")} = stored_event.${q(database, "resource_id")} AND stored_request.${q(database, "actor_subject_id")} = stored_trace.${q(database, "subject_id")} AND stored_request.${q(database, "action")} = 'query.requested' AND stored_trace.${q(database, "created_at")} >= stored_request.${q(database, "occurred_at")}))) UNION ALL SELECT terminal_trace.${q(database, "id")} AS ${q(database, "id")}, requested.${q(database, "tenant_id")}, requested.${q(database, "knowledge_space_id")}, requested.${q(database, "actor_type")}, requested.${q(database, "actor_subject_id")}, CASE WHEN terminal_trace.${q(database, "completed")} = TRUE THEN 'query.completed' ELSE 'query.failed' END AS ${q(database, "action")}, requested.${q(database, "resource_type")}, requested.${q(database, "resource_id")}, CASE WHEN terminal_trace.${q(database, "completed")} = TRUE THEN 'success' ELSE 'failure' END AS ${q(database, "result")}, requested.${q(database, "required_permission_scope")}, requested.${q(database, "details")}, terminal_trace.${q(database, "created_at")} AS ${q(database, "occurred_at")} FROM ${traces} terminal_trace INNER JOIN ${activity} requested ON requested.${q(database, "knowledge_space_id")} = terminal_trace.${q(database, "knowledge_space_id")} AND requested.${q(database, "resource_id")} = ${textIdSql(database, "terminal_trace", "id")} AND requested.${q(database, "actor_subject_id")} = terminal_trace.${q(database, "subject_id")} AND requested.${q(database, "action")} = 'query.requested' AND requested.${q(database, "resource_type")} = 'query' AND terminal_trace.${q(database, "created_at")} >= requested.${q(database, "occurred_at")}`; + return `SELECT ${selectColumns("stored_event")} FROM ${activity} stored_event WHERE stored_event.${q(database, "action")} NOT IN (${hiddenActions})`; } function textIdSql(database: DatabaseAdapter, alias: string, column: string): string { diff --git a/knowledge-fs/packages/api/src/knowledge-space-overview.test.ts b/knowledge-fs/packages/api/src/knowledge-space-overview.test.ts index d7a1b7af148..cf88460bd26 100644 --- a/knowledge-fs/packages/api/src/knowledge-space-overview.test.ts +++ b/knowledge-fs/packages/api/src/knowledge-space-overview.test.ts @@ -5,6 +5,7 @@ import { decodeKnowledgeSpaceActivityCursor, deterministicKnowledgeSpaceActivityId, encodeKnowledgeSpaceActivityCursor, + sanitizeKnowledgeSpaceActivityDetails, } from "./knowledge-space-overview"; const TENANT_ID = "tenant-overview"; @@ -23,7 +24,12 @@ describe("in-memory knowledge-space Overview repository", () => { const input = { action: "query.requested" as const, actor: { id: "member-1", type: "member" as const }, - details: { apiKey: "must-not-leak", mode: "fast", query: "must-not-leak" }, + details: { + apiKey: "must-not-leak", + mode: "fast", + query: "legacy-query-key-must-not-leak", + question: "What is the permission model?", + }, id, knowledgeSpaceId: SPACE_ID, occurredAt: NOW, @@ -40,7 +46,7 @@ describe("in-memory knowledge-space Overview repository", () => { }); expect(replay).toEqual(first); - expect(first.details).toEqual({ mode: "fast" }); + expect(first.details).toEqual({ mode: "fast", question: "What is the permission model?" }); await expect(repository.appendActivity({ ...input, result: "failure" })).rejects.toThrow( "idempotency key", ); @@ -49,6 +55,59 @@ describe("in-memory knowledge-space Overview repository", () => { ); }); + it("keeps legacy terminal and profile events out of the product activity feed", async () => { + const repository = createInMemoryKnowledgeSpaceOverviewRepository({ + maxEvents: 3, + maxListLimit: 10, + }); + const common = { + actor: { id: "member-1", type: "member" as const }, + knowledgeSpaceId: SPACE_ID, + occurredAt: NOW, + requiredPermissionScope: ["team:camera"], + tenantId: TENANT_ID, + }; + await repository.appendActivity({ + ...common, + action: "query.requested", + id: "00000000-0000-4000-8000-000000000021", + resource: { id: QUERY_ID, type: "query" }, + result: "success", + }); + await repository.appendActivity({ + ...common, + action: "query.completed", + id: "00000000-0000-4000-8000-000000000022", + resource: { id: QUERY_ID, type: "query" }, + result: "success", + }); + await repository.appendActivity({ + ...common, + action: "profile.published", + id: "00000000-0000-4000-8000-000000000023", + resource: { id: "profile-1", type: "profile" }, + result: "success", + }); + + const activity = await repository.listActivity({ + candidateGrants: ["team:camera"], + knowledgeSpaceId: SPACE_ID, + limit: 10, + tenantId: TENANT_ID, + }); + + expect(activity.items.map((event) => event.action)).toEqual(["query.requested"]); + }); + + it("bounds long questions without losing the truncation marker on database re-sanitization", () => { + const details = sanitizeKnowledgeSpaceActivityDetails({ question: "q".repeat(4_001) }); + + expect(details.question).toHaveLength(4_000); + expect(details.question).toMatch(/…$/u); + expect(details.questionTruncated).toBe(true); + expect(sanitizeKnowledgeSpaceActivityDetails(details)).toEqual(details); + }); + it("counts distinct requested query identities and only their later successful completion", async () => { const repository = createInMemoryKnowledgeSpaceOverviewRepository({ maxEvents: 20, diff --git a/knowledge-fs/packages/api/src/knowledge-space-overview.ts b/knowledge-fs/packages/api/src/knowledge-space-overview.ts index 111c82ddacc..1a945044d18 100644 --- a/knowledge-fs/packages/api/src/knowledge-space-overview.ts +++ b/knowledge-fs/packages/api/src/knowledge-space-overview.ts @@ -19,6 +19,16 @@ export const KnowledgeSpaceActivityActions = [ ] as const; export type KnowledgeSpaceActivityAction = (typeof KnowledgeSpaceActivityActions)[number]; +/** Retained for decoding historical rows, but excluded from the product activity feed. */ +export const HiddenKnowledgeSpaceActivityActions = [ + "query.completed", + "query.failed", + "profile.published", +] as const satisfies readonly KnowledgeSpaceActivityAction[]; +const HiddenKnowledgeSpaceActivityActionSet: ReadonlySet = new Set( + HiddenKnowledgeSpaceActivityActions, +); + export const KnowledgeSpaceActivityResourceTypes = [ "knowledge-space", "query", @@ -41,7 +51,7 @@ export interface KnowledgeSpaceActivityEvent { readonly id?: string | undefined; readonly type: "member" | "system"; }; - /** A deliberately small allow-list; query text, credentials, tokens and object keys are absent. */ + /** A deliberately small allow-list; credentials, tokens and object keys are absent. */ readonly details: Readonly>; readonly id: string; readonly knowledgeSpaceId: string; @@ -399,6 +409,7 @@ export function createInMemoryKnowledgeSpaceOverviewRepository(options: { event.knowledgeSpaceId === input.knowledgeSpaceId && candidatePermissionScopeAllows(event.requiredPermissionScope, input.candidateGrants), ) + .filter((event) => !HiddenKnowledgeSpaceActivityActionSet.has(event.action)) .filter((event) => !input.action || event.action === input.action) .filter((event) => !input.actorType || event.actor.type === input.actorType) .filter((event) => !input.actorId || event.actor.id === input.actorId) @@ -489,6 +500,8 @@ const SAFE_ACTIVITY_DETAIL_KEYS = new Set([ "durationMs", "mode", "providerId", + "question", + "questionTruncated", "reasonCode", "statusCode", ]); @@ -502,7 +515,13 @@ export function sanitizeKnowledgeSpaceActivityDetails( if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") { continue; } - if (typeof value === "string" && value.length > 160) continue; + if (typeof value === "string" && key === "question" && value.length > 4_000) { + safe.question = `${value.slice(0, 3_999)}…`; + safe.questionTruncated = true; + continue; + } + if (typeof value === "string" && value.length > 160 && key !== "question") continue; + if (key === "questionTruncated" && value !== true) continue; if (typeof value === "number" && !Number.isFinite(value)) continue; safe[key] = value; } diff --git a/knowledge-fs/packages/api/src/knowledge-space-profile-repository.test.ts b/knowledge-fs/packages/api/src/knowledge-space-profile-repository.test.ts index 40d9a72d690..ac143a602ab 100644 --- a/knowledge-fs/packages/api/src/knowledge-space-profile-repository.test.ts +++ b/knowledge-fs/packages/api/src/knowledge-space-profile-repository.test.ts @@ -324,7 +324,7 @@ describe("knowledge-space profile repository", () => { (call) => call.tableName === "knowledge_space_activity_events" && call.operation === "insert", ), - ).toBe(true); + ).toBe(false); }, ); diff --git a/knowledge-fs/packages/api/src/knowledge-space-profile-repository.ts b/knowledge-fs/packages/api/src/knowledge-space-profile-repository.ts index 0df0476f18f..4d51f049d68 100644 --- a/knowledge-fs/packages/api/src/knowledge-space-profile-repository.ts +++ b/knowledge-fs/packages/api/src/knowledge-space-profile-repository.ts @@ -34,8 +34,6 @@ import { assertDatabaseKnowledgeSpacePermissionFence, } from "./knowledge-space-access-control"; import { lockKnowledgeSpaceForDeletionAdmission } from "./knowledge-space-deletion-admission"; -import { deterministicKnowledgeSpaceActivityId } from "./knowledge-space-overview"; -import { appendKnowledgeSpaceActivityWithExecutor } from "./knowledge-space-overview-database-repository"; export const KnowledgeSpaceProfileKinds = ["embedding", "retrieval"] as const; export type KnowledgeSpaceProfileKind = (typeof KnowledgeSpaceProfileKinds)[number]; @@ -492,28 +490,6 @@ export function createDatabaseKnowledgeSpaceProfileRepository({ if (!head) { throw new Error("Activated knowledge-space profile head could not be reloaded"); } - await appendKnowledgeSpaceActivityWithExecutor({ - database, - executor: transaction, - input: { - action: "profile.published", - actor: { id: candidate.createdBySubjectId, type: "member" }, - details: { providerId: candidate.provider }, - id: deterministicKnowledgeSpaceActivityId( - "profile.published", - input.tenantId, - input.knowledgeSpaceId, - input.kind, - candidate.id, - ), - knowledgeSpaceId: input.knowledgeSpaceId, - occurredAt: input.now, - requiredPermissionScope: [], - resource: { id: candidate.id, type: "profile" }, - result: "success", - tenantId: input.tenantId, - }, - }); return head; }); }, @@ -1588,28 +1564,6 @@ async function activateUnpublishedProfileRevision({ const head = await getProfileHead(database, transaction, input, false); if (!head) throw new Error("Activated unpublished profile head could not be reloaded"); - await appendKnowledgeSpaceActivityWithExecutor({ - database, - executor: transaction, - input: { - action: "profile.published", - actor: { id: candidate.createdBySubjectId, type: "member" }, - details: { providerId: candidate.provider }, - id: deterministicKnowledgeSpaceActivityId( - "profile.published", - input.tenantId, - input.knowledgeSpaceId, - input.kind, - candidate.id, - ), - knowledgeSpaceId: input.knowledgeSpaceId, - occurredAt: input.now, - requiredPermissionScope: [], - resource: { id: candidate.id, type: "profile" }, - result: "success", - tenantId: input.tenantId, - }, - }); return { head, replayed: false }; } diff --git a/knowledge-fs/packages/api/src/query-handlers-branch-coverage.test.ts b/knowledge-fs/packages/api/src/query-handlers-branch-coverage.test.ts index 8e0c98ce504..01b907f6c47 100644 --- a/knowledge-fs/packages/api/src/query-handlers-branch-coverage.test.ts +++ b/knowledge-fs/packages/api/src/query-handlers-branch-coverage.test.ts @@ -221,7 +221,7 @@ describe("query handler branch coverage", () => { await response.body?.cancel(); }); - it("records requested and failed terminal activity and releases after session failure", async () => { + it("records only the query request and releases after session failure", async () => { const release = vi.fn(async () => undefined); const appendActivity = vi.fn(async () => ({})); const failure = new Error("session failed"); @@ -231,19 +231,26 @@ describe("query handler branch coverage", () => { sessionError: failure, }); await expect(fixture.invoke()).rejects.toBe(failure); - expect(appendActivity).toHaveBeenCalledTimes(2); + expect(appendActivity).toHaveBeenCalledOnce(); + expect(appendActivity).toHaveBeenCalledWith( + expect.objectContaining({ + action: "query.requested", + details: { mode: "fast", question: "question" }, + result: "success", + }), + ); expect(release).toHaveBeenCalledOnce(); }); - it("records successful terminal activity when the stream completes", async () => { + it("does not append a terminal activity when the stream completes", async () => { const appendActivity = vi.fn(async () => ({})); const fixture = queryFixture({ overview: { appendActivity } }); const response = await fixture.invoke(); expect(response.status).toBe(200); await response.text(); - expect(appendActivity).toHaveBeenCalledTimes(2); - expect(appendActivity).toHaveBeenLastCalledWith( - expect.objectContaining({ action: "query.completed", result: "success" }), + expect(appendActivity).toHaveBeenCalledOnce(); + expect(appendActivity).toHaveBeenCalledWith( + expect.objectContaining({ action: "query.requested", result: "success" }), ); }); }); diff --git a/knowledge-fs/packages/api/src/query-handlers.ts b/knowledge-fs/packages/api/src/query-handlers.ts index 3a92516e696..15def72551f 100644 --- a/knowledge-fs/packages/api/src/query-handlers.ts +++ b/knowledge-fs/packages/api/src/query-handlers.ts @@ -388,35 +388,13 @@ export function registerQueryHandlers({ throw error; } - const appendTerminalActivity = async (status: "canceled" | "failed" | "succeeded") => { - if (!overview) return; - const occurredAt = new Date(now()).toISOString(); - await overview.appendActivity({ - action: status === "succeeded" ? "query.completed" : "query.failed", - actor: { id: subject.subjectId, type: "member" }, - details: { mode: resolvedMode }, - id: deterministicKnowledgeSpaceActivityId( - `query.${status}`, - subject.tenantId, - space.id, - queryRunId, - ), - knowledgeSpaceId: space.id, - occurredAt, - requiredPermissionScope: [], - resource: { id: queryRunId, type: "query" }, - result: status === "succeeded" ? "success" : status === "canceled" ? "canceled" : "failure", - tenantId: subject.tenantId, - }); - }; - let requestedActivityPersisted = false; try { if (overview) { const occurredAt = new Date(now()).toISOString(); await overview.appendActivity({ action: "query.requested", actor: { id: subject.subjectId, type: "member" }, - details: { mode: resolvedMode }, + details: { mode: resolvedMode, ...(query ? { question: query } : {}) }, id: deterministicKnowledgeSpaceActivityId( "query.requested", subject.tenantId, @@ -427,10 +405,9 @@ export function registerQueryHandlers({ occurredAt, requiredPermissionScope: [], resource: { id: queryRunId, type: "query" }, - result: "pending", + result: "success", tenantId: subject.tenantId, }); - requestedActivityPersisted = true; } const session = await sessionRepository.recordQuery({ activeDocumentIds: body.activeDocumentIds, @@ -500,18 +477,10 @@ export function registerQueryHandlers({ subject, traceId: queryRunId, }, - ...(overview - ? { - onTerminal: appendTerminalActivity, - } - : {}), sessionId: session.context.sessionId, traceId: queryRunId, }); } catch (error) { - if (requestedActivityPersisted) { - await appendTerminalActivity("failed").catch(() => undefined); - } await executionLease?.release().catch(() => undefined); throw error; } diff --git a/knowledge-fs/packages/api/src/query-overview-durability.test.ts b/knowledge-fs/packages/api/src/query-overview-durability.test.ts index 10b26043b76..ce748ee0b4d 100644 --- a/knowledge-fs/packages/api/src/query-overview-durability.test.ts +++ b/knowledge-fs/packages/api/src/query-overview-durability.test.ts @@ -22,7 +22,7 @@ const SUBJECT = { }; describe("query Overview durability", () => { - it("persists query.requested before admitting generation and records terminal activity", async () => { + it("persists one query activity with its question and resolved mode before generation", async () => { const spaces = createInMemoryKnowledgeSpaceRepository({ generateId: () => "018f0d60-7a49-7cc2-9c1b-5b36f18f9a02", maxListLimit: 10, @@ -118,32 +118,20 @@ describe("query Overview durability", () => { limit: 10, tenantId: SUBJECT.tenantId, }); - expect(activity.items.map((event) => event.action).sort()).toEqual([ - "query.completed", - "query.requested", + expect(activity.items).toEqual([ + expect.objectContaining({ + action: "query.requested", + details: { mode: "fast", question: "Is the request durable?" }, + id: deterministicKnowledgeSpaceActivityId( + "query.requested", + SUBJECT.tenantId, + space.id, + QUERY_RUN_ID, + ), + resource: { id: QUERY_RUN_ID, type: "query" }, + result: "success", + }), ]); - expect(activity.items).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: deterministicKnowledgeSpaceActivityId( - "query.requested", - SUBJECT.tenantId, - space.id, - QUERY_RUN_ID, - ), - resource: { id: QUERY_RUN_ID, type: "query" }, - }), - expect.objectContaining({ - id: deterministicKnowledgeSpaceActivityId( - "query.succeeded", - SUBJECT.tenantId, - space.id, - QUERY_RUN_ID, - ), - resource: { id: QUERY_RUN_ID, type: "query" }, - }), - ]), - ); }); }); diff --git a/web/features/new-rag/__tests__/knowledge-overview-page.spec.tsx b/web/features/new-rag/__tests__/knowledge-overview-page.spec.tsx index c985556dc69..578f797644b 100644 --- a/web/features/new-rag/__tests__/knowledge-overview-page.spec.tsx +++ b/web/features/new-rag/__tests__/knowledge-overview-page.spec.tsx @@ -327,6 +327,7 @@ describe('KnowledgeOverviewPage', () => { queryData.attention.data = [] queryData.activity.data[0]!.action = 'source.synced' queryData.activity.data[0]!.actor = { id: 'dify-account:member-1', type: 'member' } + queryData.activity.data[0]!.details = { count: 1 } queryData.activity.data[0]!.occurred_at = '2026-07-29T08:05:00Z' queryData.activity.data[0]!.result = 'success' queryData.stats.source_count = 3 @@ -730,6 +731,29 @@ describe('KnowledgeOverviewPage', () => { expect(within(dialog).getByText(/2h ago|2 hr\. ago|2 hours ago/)).toBeInTheDocument() }) + it('shows one initiated query activity with its question and retrieval mode', async () => { + const user = userEvent.setup() + queryData.activity.data[0]!.action = 'query.requested' + queryData.activity.data[0]!.details = { + mode: 'research', + question: 'How do permissions work?', + } + queryData.activity.data[0]!.result = 'success' + + renderWithNuqs() + + const label = 'dataset.newKnowledge.qualityPage.question: How do permissions work? — research' + expect(within(screen.getByRole('table')).getByText(label)).toBeInTheDocument() + + await user.click( + screen.getByRole('button', { name: 'dataset.newKnowledge.overview.allActivity' }), + ) + expect(within(screen.getByRole('dialog')).getByText(label)).toBeInTheDocument() + expect( + screen.queryByText('dataset.newKnowledge.overview.activityQueued'), + ).not.toBeInTheDocument() + }) + it('refreshes overview snapshots until they catch up with a completed background task', () => { queryData.tasks[0]!.state = 'running' queryData.tasks[0]!.updated_at = '2026-07-29T10:00:00Z' diff --git a/web/features/new-rag/overview/knowledge-overview-page.tsx b/web/features/new-rag/overview/knowledge-overview-page.tsx index 4ab4c709051..96b6dfaeadb 100644 --- a/web/features/new-rag/overview/knowledge-overview-page.tsx +++ b/web/features/new-rag/overview/knowledge-overview-page.tsx @@ -689,8 +689,6 @@ function activityOperationLabel( if (activity.action.startsWith('query.')) return t(($) => $['newKnowledge.overview.queryOutcomes']) if (activity.action === 'permission.updated') return t(($) => $['newKnowledge.permission']) - if (activity.action === 'profile.published') - return t(($) => $['newKnowledge.retrievalTest.title']) if (activity.action === 'settings.updated') return t(($) => $['newKnowledge.overview.updateEvidence']) return t(($) => $['newKnowledge.backgroundTasks']) @@ -700,6 +698,16 @@ function activityLabel( activity: KnowledgeFsOverviewActivityResponse, t: ReturnType>['t'], ) { + if (activity.action === 'query.requested') { + const question = activity.details.question + const mode = activity.details.mode + const label = + typeof question === 'string' && question.trim() + ? `${t(($) => $['newKnowledge.qualityPage.question'])}: ${question}` + : activityOperationLabel(activity, t) + return typeof mode === 'string' && mode.trim() ? `${label} — ${mode}` : label + } + const operation = activityOperationLabel(activity, t) let label: string if (activity.result === 'success') @@ -708,11 +716,7 @@ function activityLabel( label = t(($) => $['newKnowledge.overview.activityFailed'], { operation }) else if (activity.result === 'canceled') label = t(($) => $['newKnowledge.overview.activityCanceled'], { operation }) - else - label = - activity.action === 'query.requested' - ? t(($) => $['newKnowledge.overview.activityQueued'], { operation }) - : t(($) => $['newKnowledge.overview.activityRunning'], { operation }) + else label = t(($) => $['newKnowledge.overview.activityRunning'], { operation }) const detail = [ activity.details.reasonCode,