Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<RecentArtifact[]> {
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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 },
);
Expand Down Expand Up @@ -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,
Expand Down