authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-04-29 17:07:21-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-29 17:07:21-07:00
log22b7d8c524b3841cd50c1bb0927cdc472dd7902e
tree7ff74c6ebad60c07a9d01124da51ce1880d5da80
parentfe2fde5172a71887fe647c1a39b727a1c3d2e3ed
signature Signed by PGP key B5690EEEBB952194

[codex] Fix visited timestamp under clock skew (#2408)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 27 insertions(+), 1 deletions(-)

apps/web/src/components/ChatView.tsx+4-1
......@@ -1021,7 +1021,10 @@ export default function ChatView(props: ChatViewProps) {
10211021 const lastVisitedAt = activeThreadLastVisitedAt ? Date.parse(activeThreadLastVisitedAt) : NaN;
10221022 if (!Number.isNaN(lastVisitedAt) && lastVisitedAt >= turnCompletedAt) return;
10231023
1024 markThreadVisited(scopedThreadKey(scopeThreadRef(serverThread.environmentId, serverThread.id)));
1024 markThreadVisited(
1025 scopedThreadKey(scopeThreadRef(serverThread.environmentId, serverThread.id)),
1026 activeLatestTurn.completedAt,
1027 );
10251028 }, [
10261029 activeLatestTurn?.completedAt,
10271030 activeThreadLastVisitedAt,
apps/web/src/uiStateStore.test.ts+23
......@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
44import {
55 clearThreadUi,
66 hydratePersistedProjectState,
7 markThreadVisited,
78 markThreadUnread,
89 PERSISTED_STATE_KEY,
910 type PersistedUiState,
......@@ -27,6 +28,28 @@ function makeUiState(overrides: Partial<UiState> = {}): UiState {
2728}
2829
2930describe("uiStateStore pure functions", () => {
31 it("markThreadVisited stores the provided server timestamp", () => {
32 const threadId = ThreadId.make("thread-1");
33 const initialState = makeUiState();
34
35 const next = markThreadVisited(initialState, threadId, "2026-02-25T12:30:00.700Z");
36
37 expect(next.threadLastVisitedAtById[threadId]).toBe("2026-02-25T12:30:00.700Z");
38 });
39
40 it("markThreadVisited does not move visit state backwards under clock skew", () => {
41 const threadId = ThreadId.make("thread-1");
42 const initialState = makeUiState({
43 threadLastVisitedAtById: {
44 [threadId]: "2026-02-25T12:30:00.700Z",
45 },
46 });
47
48 const next = markThreadVisited(initialState, threadId, "2026-02-25T12:30:00.000Z");
49
50 expect(next).toBe(initialState);
51 });
52
3053 it("markThreadUnread moves lastVisitedAt before completion for a completed thread", () => {
3154 const threadId = ThreadId.make("thread-1");
3255 const latestTurnCompletedAt = "2026-02-25T12:30:00.000Z";