feat: add options for bypassing cache in session and subagent detail retrieval

- Updated handleGetSessionDetail and handleGetSubagentDetail functions to accept an optional options parameter for bypassing cache.
- Modified IPC and API methods to support the new options parameter, enhancing flexibility in data retrieval.
- Adjusted MemberLogsTab to utilize the bypassCache option when fetching log details, improving data freshness.
This commit is contained in:
iliya 2026-03-06 10:40:45 +02:00
parent 297780e3a8
commit b093e87c89
7 changed files with 134 additions and 41 deletions

View file

@ -198,7 +198,8 @@ async function handleGetSessionsByIds(
async function handleGetSessionDetail(
_event: IpcMainInvokeEvent,
projectId: string,
sessionId: string
sessionId: string,
options?: { bypassCache?: boolean }
): Promise<SessionDetail | null> {
try {
const validatedProject = validateProjectId(projectId);
@ -220,7 +221,7 @@ async function handleGetSessionDetail(
// Check cache first
let sessionDetail = dataCache.get(cacheKey);
if (sessionDetail) {
if (sessionDetail && !options?.bypassCache) {
return sessionDetail;
}

View file

@ -56,7 +56,8 @@ async function handleGetSubagentDetail(
_event: IpcMainInvokeEvent,
projectId: string,
sessionId: string,
subagentId: string
subagentId: string,
options?: { bypassCache?: boolean }
): Promise<SubagentDetail | null> {
try {
const validatedProject = validateProjectId(projectId);
@ -85,7 +86,7 @@ async function handleGetSubagentDetail(
// Check cache first
let subagentDetail = dataCache.getSubagent(cacheKey);
if (subagentDetail) {
if (subagentDetail && !options?.bypassCache) {
return subagentDetail;
}

View file

@ -300,30 +300,61 @@ export class TeamDataService {
});
}
// Enrich inbox messages without leadSessionId by propagating from neighboring
// messages that have it (lead_session, user_sent). Sort chronologically (asc),
// sweep forward, then sweep backward so orphans at the start also get a session.
// Enrich inbox messages without leadSessionId by assigning the nearest neighbor's
// session ID (by timestamp). This avoids the old forward-only propagation bug where
// messages between two sessions always inherited the *earlier* session, causing a
// spurious "New session" divider even when the message is chronologically closer to
// the later session.
if (config.leadSessionId || messages.some((m) => m.leadSessionId)) {
messages.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
// Forward pass: propagate leadSessionId from earlier messages to later ones
let currentSessionId: string | undefined;
for (const msg of messages) {
if (msg.leadSessionId) {
currentSessionId = msg.leadSessionId;
} else if (currentSessionId) {
msg.leadSessionId = currentSessionId;
// Collect indices of messages that already have a leadSessionId (anchors).
const anchors: { index: number; time: number; sessionId: string }[] = [];
for (let i = 0; i < messages.length; i++) {
if (messages[i].leadSessionId) {
anchors.push({
index: i,
time: Date.parse(messages[i].timestamp),
sessionId: messages[i].leadSessionId!,
});
}
}
// Backward pass: fill messages before the first known session.
// Seed with config.leadSessionId so that recent messages without an explicit
// session ID inherit the current (most recent) session — this ensures that
// session boundary separators work even when inbox entries lack the field.
currentSessionId = config.leadSessionId;
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].leadSessionId) {
currentSessionId = messages[i].leadSessionId;
} else if (currentSessionId) {
messages[i].leadSessionId = currentSessionId;
if (anchors.length > 0) {
// For each message without leadSessionId, find the closest anchor by timestamp
// and inherit its sessionId.
let anchorIdx = 0;
for (let i = 0; i < messages.length; i++) {
if (messages[i].leadSessionId) {
// Advance anchorIdx to track current position for efficient lookup
while (anchorIdx < anchors.length - 1 && anchors[anchorIdx].index < i) {
anchorIdx++;
}
continue;
}
const msgTime = Date.parse(messages[i].timestamp);
// Find closest anchor by timestamp (binary-search-like scan from current position)
let bestAnchor = anchors[0];
let bestDist = Math.abs(msgTime - bestAnchor.time);
for (let a = 0; a < anchors.length; a++) {
const dist = Math.abs(msgTime - anchors[a].time);
if (dist < bestDist) {
bestDist = dist;
bestAnchor = anchors[a];
} else if (dist > bestDist && anchors[a].time > msgTime) {
// Anchors are sorted by index (asc time) — once distance grows past the
// message time, further anchors will only be farther.
break;
}
}
messages[i].leadSessionId = bestAnchor.sessionId;
}
} else if (config.leadSessionId) {
// No anchors at all — fall back to config.leadSessionId for everything.
for (const msg of messages) {
msg.leadSessionId = config.leadSessionId;
}
}
}

View file

@ -347,14 +347,18 @@ const electronAPI: ElectronAPI = {
ipcRenderer.invoke('search-sessions', projectId, query, maxResults),
searchAllProjects: (query: string, maxResults?: number) =>
ipcRenderer.invoke('search-all-projects', query, maxResults),
getSessionDetail: (projectId: string, sessionId: string) =>
ipcRenderer.invoke('get-session-detail', projectId, sessionId),
getSessionDetail: (projectId: string, sessionId: string, options?: { bypassCache?: boolean }) =>
ipcRenderer.invoke('get-session-detail', projectId, sessionId, options),
getSessionMetrics: (projectId: string, sessionId: string) =>
ipcRenderer.invoke('get-session-metrics', projectId, sessionId),
getWaterfallData: (projectId: string, sessionId: string) =>
ipcRenderer.invoke('get-waterfall-data', projectId, sessionId),
getSubagentDetail: (projectId: string, sessionId: string, subagentId: string) =>
ipcRenderer.invoke('get-subagent-detail', projectId, sessionId, subagentId),
getSubagentDetail: (
projectId: string,
sessionId: string,
subagentId: string,
options?: { bypassCache?: boolean }
) => ipcRenderer.invoke('get-subagent-detail', projectId, sessionId, subagentId, options),
getSessionGroups: (projectId: string, sessionId: string) =>
ipcRenderer.invoke('get-session-groups', projectId, sessionId),
getSessionsByIds: (projectId: string, sessionIds: string[], options?: SessionsByIdsOptions) =>

View file

@ -246,7 +246,11 @@ export class HttpAPIClient implements ElectronAPI {
return this.get<SearchSessionsResult>(`/api/search?${params}`);
};
getSessionDetail = (projectId: string, sessionId: string): Promise<SessionDetail | null> =>
getSessionDetail = (
projectId: string,
sessionId: string,
_options?: { bypassCache?: boolean }
): Promise<SessionDetail | null> =>
this.get<SessionDetail | null>(
`/api/projects/${encodeURIComponent(projectId)}/sessions/${encodeURIComponent(sessionId)}`
);
@ -264,7 +268,8 @@ export class HttpAPIClient implements ElectronAPI {
getSubagentDetail = (
projectId: string,
sessionId: string,
subagentId: string
subagentId: string,
_options?: { bypassCache?: boolean }
): Promise<SubagentDetail | null> =>
this.get<SubagentDetail | null>(
`/api/projects/${encodeURIComponent(projectId)}/sessions/${encodeURIComponent(sessionId)}/subagents/${encodeURIComponent(subagentId)}`

View file

@ -57,6 +57,7 @@ export const MemberLogsTab = ({
showLeadPreview = false,
onPreviewOnlineChange,
}: MemberLogsTabProps): React.JSX.Element => {
const MIN_REFRESH_VISIBLE_MS = 250;
const intervalsKey = useMemo(
() => (taskWorkIntervals ? JSON.stringify(taskWorkIntervals) : ''),
[taskWorkIntervals]
@ -68,6 +69,8 @@ export const MemberLogsTab = ({
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const refreshCountRef = useRef(0);
const refreshBeganAtRef = useRef<number | null>(null);
const refreshHideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [error, setError] = useState<string | null>(null);
const [expandedId, setExpandedId] = useState<string | null>(null);
const expandedIdRef = useRef<string | null>(null);
@ -78,6 +81,10 @@ export const MemberLogsTab = ({
useEffect(() => {
return () => {
isMountedRef.current = false;
if (refreshHideTimeoutRef.current) {
clearTimeout(refreshHideTimeoutRef.current);
refreshHideTimeoutRef.current = null;
}
};
}, []);
@ -86,13 +93,40 @@ export const MemberLogsTab = ({
}, [expandedId]);
const beginRefreshing = useCallback((): void => {
if (refreshCountRef.current === 0) {
refreshBeganAtRef.current = Date.now();
if (refreshHideTimeoutRef.current) {
clearTimeout(refreshHideTimeoutRef.current);
refreshHideTimeoutRef.current = null;
}
}
refreshCountRef.current += 1;
if (isMountedRef.current) setRefreshing(true);
}, []);
const endRefreshing = useCallback((): void => {
refreshCountRef.current = Math.max(0, refreshCountRef.current - 1);
if (isMountedRef.current) setRefreshing(refreshCountRef.current > 0);
if (refreshCountRef.current > 0) {
if (isMountedRef.current) setRefreshing(true);
return;
}
const beganAt = refreshBeganAtRef.current;
refreshBeganAtRef.current = null;
const elapsed = beganAt ? Date.now() - beganAt : Number.POSITIVE_INFINITY;
if (!isMountedRef.current) return;
if (elapsed >= MIN_REFRESH_VISIBLE_MS) {
setRefreshing(false);
return;
}
const remaining = Math.max(0, MIN_REFRESH_VISIBLE_MS - elapsed);
refreshHideTimeoutRef.current = setTimeout(() => {
refreshHideTimeoutRef.current = null;
if (!isMountedRef.current) return;
if (refreshCountRef.current === 0) setRefreshing(false);
}, remaining);
}, []);
const getRowId = useCallback((log: MemberLogSummary): string => {
@ -258,12 +292,20 @@ export const MemberLogsTab = ({
}, [teamName, memberName, taskId, taskOwner, taskStatus, intervalsKey]);
const fetchDetailForLog = useCallback(
async (log: MemberLogSummary): Promise<EnhancedChunk[] | null> => {
async (
log: MemberLogSummary,
options?: { bypassCache?: boolean }
): Promise<EnhancedChunk[] | null> => {
if (log.kind === 'subagent') {
const d = await api.getSubagentDetail(log.projectId, log.sessionId, log.subagentId);
const d = await api.getSubagentDetail(
log.projectId,
log.sessionId,
log.subagentId,
options
);
return d?.chunks ?? null;
}
const d = await api.getSessionDetail(log.projectId, log.sessionId);
const d = await api.getSessionDetail(log.projectId, log.sessionId, options);
return (d?.chunks ?? null) as unknown as EnhancedChunk[] | null;
},
[]
@ -281,7 +323,7 @@ export const MemberLogsTab = ({
const shouldAutoRefreshSummary = taskId != null && taskStatus === 'in_progress';
if (!shouldAutoRefreshSummary && !nextExpanded.isOngoing) return;
const next = await fetchDetailForLog(nextExpanded);
const next = await fetchDetailForLog(nextExpanded, { bypassCache: true });
if (!isMountedRef.current) return;
// Ensure new reference so memoized transforms update.
setDetailChunks(next ? [...next] : null);
@ -327,7 +369,7 @@ export const MemberLogsTab = ({
const interval = setInterval(async () => {
beginRefreshing();
try {
const next = await fetchDetailForLog(previewLog);
const next = await fetchDetailForLog(previewLog, { bypassCache: true });
if (cancelled) return;
setPreviewChunks(next ? [...next] : null);
} catch {
@ -363,7 +405,7 @@ export const MemberLogsTab = ({
const refreshDetail = async (): Promise<void> => {
beginRefreshing();
try {
const next = await fetchDetailForLog(expandedLogSummary);
const next = await fetchDetailForLog(expandedLogSummary, { bypassCache: true });
if (cancelled) return;
// Ensure new reference so memoized transforms update.
setDetailChunks(next ? [...next] : null);
@ -395,7 +437,11 @@ export const MemberLogsTab = ({
setDetailChunks(null);
setDetailLoading(true);
try {
const chunks = await fetchDetailForLog(log);
const shouldBypassCache = log.isOngoing || taskStatus === 'in_progress';
const chunks = await fetchDetailForLog(
log,
shouldBypassCache ? { bypassCache: true } : undefined
);
setDetailChunks(chunks ? [...chunks] : null);
} catch {
setDetailChunks(null);
@ -403,7 +449,7 @@ export const MemberLogsTab = ({
setDetailLoading(false);
}
},
[expandedId, fetchDetailForLog, getRowId]
[expandedId, fetchDetailForLog, getRowId, taskStatus]
);
if (loading && logs.length === 0) {

View file

@ -614,13 +614,18 @@ export interface ElectronAPI {
maxResults?: number
) => Promise<SearchSessionsResult>;
searchAllProjects: (query: string, maxResults?: number) => Promise<SearchSessionsResult>;
getSessionDetail: (projectId: string, sessionId: string) => Promise<SessionDetail | null>;
getSessionDetail: (
projectId: string,
sessionId: string,
options?: { bypassCache?: boolean }
) => Promise<SessionDetail | null>;
getSessionMetrics: (projectId: string, sessionId: string) => Promise<SessionMetrics | null>;
getWaterfallData: (projectId: string, sessionId: string) => Promise<WaterfallData | null>;
getSubagentDetail: (
projectId: string,
sessionId: string,
subagentId: string
subagentId: string,
options?: { bypassCache?: boolean }
) => Promise<SubagentDetail | null>;
getSessionGroups: (projectId: string, sessionId: string) => Promise<ConversationGroup[]>;
getSessionsByIds: (