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:
parent
297780e3a8
commit
b093e87c89
7 changed files with 134 additions and 41 deletions
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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)}`
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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: (
|
||||
|
|
|
|||
Loading…
Reference in a new issue