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,