From dcce27c3b055c9b6a04f4f9bf3013fa78ef74bf6 Mon Sep 17 00:00:00 2001 From: "BengalO6js-Qwen3.7-plus" Date: Thu, 2 Jul 2026 18:01:38 +0800 Subject: [PATCH] fix: filter stale artifacts from navigation truth source (#1067) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: The [导航] block injected stale file references (e.g. deploy-irb.py) into cat prompts, even after those files were deleted. This caused cats to be misdirected to read non-existent files, producing hallucinated cross-thread responses ('串台'). Root cause: The artifact ledger in threadMemory.recentArtifacts uses append+dedup strategy with no TTL and no file existence check. Once a file path entered the ledger, it persisted forever. Fix: Add filterStaleArtifacts() that checks file existence before ranking. Applied in two places: 1. Before rankArtifactSources() for navigation header [真相源] 2. In briefingContext for recentArtifacts passed to cats PR-type artifacts are always kept (they don't map to filesystem). Fail-open: if existence check throws, artifact is kept. Closes #1067 --- .../agents/routing/artifact-tracking.ts | 25 +++++++++++++++++++ .../services/agents/routing/route-helpers.ts | 16 ++++++++---- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/packages/api/src/domains/cats/services/agents/routing/artifact-tracking.ts b/packages/api/src/domains/cats/services/agents/routing/artifact-tracking.ts index 4b73547739..e30a87e82d 100644 --- a/packages/api/src/domains/cats/services/agents/routing/artifact-tracking.ts +++ b/packages/api/src/domains/cats/services/agents/routing/artifact-tracking.ts @@ -84,3 +84,28 @@ export function mergeLedger( } return [...byRef.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, MAX_LEDGER_ENTRIES); } + +/** + * Filter out artifacts whose file refs no longer exist on disk. + * PR-type artifacts (type === 'pr') are always kept — they don't map to filesystem paths. + * Fail-open: if existence check throws, the artifact is kept (avoid false removals). + * + * Issue: #1067 — stale artifacts in threadMemory.recentArtifacts pollute navigation [真相源] + */ +export async function filterStaleArtifacts(artifacts: RecentArtifact[]): Promise { + const fs = await import('node:fs/promises'); + const results = await Promise.all( + artifacts.map(async (a) => { + // PR artifacts don't have filesystem refs — always keep + if (a.type === 'pr') return a; + try { + await fs.access(a.ref); + return a; + } catch { + // File doesn't exist — drop this artifact + return null; + } + }), + ); + return results.filter((a): a is RecentArtifact => a !== null); +} diff --git a/packages/api/src/domains/cats/services/agents/routing/route-helpers.ts b/packages/api/src/domains/cats/services/agents/routing/route-helpers.ts index f43b8ed41d..bd24ad8d1f 100644 --- a/packages/api/src/domains/cats/services/agents/routing/route-helpers.ts +++ b/packages/api/src/domains/cats/services/agents/routing/route-helpers.ts @@ -20,7 +20,7 @@ import type { Thread } from '../../stores/ports/ThreadStore.js'; import { canViewMessage, resolveVisibleReplyParent } from '../../stores/visibility.js'; import type { AgentMessage, AgentService } from '../../types.js'; import type { InvocationDeps } from '../invocation/invoke-single-cat.js'; -import { extractRecentArtifacts, mergeLedger } from './artifact-tracking.js'; +import { extractRecentArtifacts, filterStaleArtifacts, mergeLedger } from './artifact-tracking.js'; import type { CoverageMap } from './context-transport.js'; import { buildCoverageMap, @@ -801,8 +801,12 @@ export async function assembleIncrementalContext( } const mergedLedger = mergeLedger(storedLedgerArtifacts, recentArtifacts); + // Issue #1067: filter out artifacts whose files no longer exist on disk + // Prevents stale artifacts (e.g. deleted /tmp files) from polluting navigation [真相源] + const liveArtifacts = await filterStaleArtifacts(mergedLedger); + const rankedSources = rankArtifactSources( - mergedLedger, + liveArtifacts, allThreadTasks.map((t) => ({ kind: t.kind, subjectKey: t.subjectKey ?? null, title: t.title, status: t.status })), { canonicalFeatureId: options?.canonicalFeatureId, threadTitle: options?.threadTitle }, ); @@ -1347,10 +1351,12 @@ async function assembleSmartWindowContext( ...(finalAnchorLines.length > 0 ? { anchorSummaries: finalAnchorLines } : {}), ...(baton ? { baton } : {}), ...(activeTasks.length > 0 ? { activeTasks } : {}), - ...(() => { + ...(await (async () => { const merged = mergeLedger(storedFileArtifacts, recentArtifacts); - return merged.length > 0 ? { recentArtifacts: merged } : {}; - })(), + // Issue #1067: also filter stale artifacts in briefing context + const live = await filterStaleArtifacts(merged); + return live.length > 0 ? { recentArtifacts: live } : {}; + })()), ...(rankedSources.length > 0 ? { rankedSources } : {}), }, navigationHeader,