fix(sessions): canonicalize sidebar ids and timestamps

The sidebar could keep a provider-native id after backend remapping.

That left a duplicate non-working session visible until refresh.

Fresh sessions could also appear hours old.

SQLite CURRENT_TIMESTAMP is UTC without a timezone suffix.

Browser parsing then treated those values like local time.

Broadcast a canonical session_upserted event when the provider id is mapped.

Collapse provider-id aliases onto the stable app session id in the client.

Normalize session-row timestamps to ISO UTC when reading from the repository.
This commit is contained in:
Haileyesus
2026-06-12 20:52:18 +03:00
parent 123ae31020
commit 3bbb42c233
5 changed files with 205 additions and 24 deletions

View File

@@ -29,6 +29,7 @@ type UseProjectsStateArgs = {
*/
type SessionUpsertedEvent = ServerEvent & {
sessionId: string;
providerSessionId?: string | null;
provider: LLMProvider;
session: ProjectSession;
project: {
@@ -212,6 +213,26 @@ const mergeProjectSessionPage = (
return mergedProject;
};
const getSessionAliasIds = (event: SessionUpsertedEvent): Set<string> => {
const ids = new Set<string>();
const add = (value: unknown) => {
if (typeof value !== 'string') {
return;
}
const trimmed = value.trim();
if (trimmed) {
ids.add(trimmed);
}
};
add(event.sessionId);
add(event.providerSessionId);
add(event.session?.id);
return ids;
};
/**
* Resolves which provider bucket on a `Project` holds sessions for a provider.
* The legacy payload keeps Claude sessions in `sessions` and the other
@@ -237,23 +258,47 @@ const providerBucketKey = (
const upsertSessionIntoProject = (project: Project, event: SessionUpsertedEvent): Project => {
const bucketKey = providerBucketKey(event.provider);
const bucket = project[bucketKey] ?? [];
const existingIndex = bucket.findIndex((session) => session.id === event.sessionId);
const aliasIds = getSessionAliasIds(event);
const normalizedSession: ProjectSession = {
...event.session,
id: event.sessionId,
};
const existingIndex = bucket.findIndex((session) => aliasIds.has(String(session.id)));
let nextBucket: ProjectSession[];
let inserted = false;
if (existingIndex >= 0) {
const existing = bucket[existingIndex];
const updated = { ...existing, ...event.session };
if (serialize(existing) === serialize(updated)) {
let changed = false;
nextBucket = [];
for (const [index, session] of bucket.entries()) {
if (index === existingIndex) {
const updated = { ...session, ...normalizedSession };
if (serialize(session) !== serialize(updated)) {
changed = true;
}
nextBucket.push(updated);
continue;
}
if (aliasIds.has(String(session.id))) {
changed = true;
continue;
}
nextBucket.push(session);
}
if (!changed) {
return project;
}
nextBucket = [...bucket];
nextBucket[existingIndex] = updated;
} else {
nextBucket = [event.session, ...bucket];
nextBucket = [normalizedSession, ...bucket];
inserted = true;
}
const next: Project = { ...project, [bucketKey]: nextBucket };
if (existingIndex < 0) {
if (inserted) {
const total = Number(project.sessionMeta?.total ?? 0) + 1;
next.sessionMeta = {
...project.sessionMeta,
@@ -629,10 +674,40 @@ export function useProjectsState({
const updated = upsertSessionIntoProject(previousProject, upsert);
return updated === previousProject ? previousProject : updated;
});
const aliasedSelectedSessionId =
typeof upsert.providerSessionId === 'string' && upsert.providerSessionId !== upsert.sessionId
? upsert.providerSessionId
: null;
if (!aliasedSelectedSessionId) {
return;
}
const normalizedSelectedSession: ProjectSession = {
...upsert.session,
id: upsert.sessionId,
__provider: upsert.provider,
__projectId: upsert.project?.projectId ?? currentSelectedSession?.__projectId,
};
setSelectedSession((previousSession) => {
if (previousSession?.id !== aliasedSelectedSessionId) {
return previousSession;
}
return {
...previousSession,
...normalizedSelectedSession,
};
});
if (sessionId === aliasedSelectedSessionId) {
navigate(`/session/${upsert.sessionId}`);
}
};
return subscribe(handleEvent);
}, [subscribe]);
}, [navigate, sessionId, subscribe]);
useEffect(() => {
return () => {