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) {...@@ -1021,7 +1021,10 @@ export default function ChatView(props: ChatViewProps) {
1021 const lastVisitedAt = activeThreadLastVisitedAt ? Date.parse(activeThreadLastVisitedAt) : NaN;1021 const lastVisitedAt = activeThreadLastVisitedAt ? Date.parse(activeThreadLastVisitedAt) : NaN;
1022 if (!Number.isNaN(lastVisitedAt) && lastVisitedAt >= turnCompletedAt) return;1022 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 );
1025 }, [1028 }, [
1026 activeLatestTurn?.completedAt,1029 activeLatestTurn?.completedAt,
1027 activeThreadLastVisitedAt,1030 activeThreadLastVisitedAt,
apps/web/src/uiStateStore.test.ts+23
...@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";...@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4import {4import {
5 clearThreadUi,5 clearThreadUi,
6 hydratePersistedProjectState,6 hydratePersistedProjectState,
7 markThreadVisited,
7 markThreadUnread,8 markThreadUnread,
8 PERSISTED_STATE_KEY,9 PERSISTED_STATE_KEY,
9 type PersistedUiState,10 type PersistedUiState,
...@@ -27,6 +28,28 @@ function makeUiState(overrides: Partial<UiState> = {}): UiState {...@@ -27,6 +28,28 @@ function makeUiState(overrides: Partial<UiState> = {}): UiState {
27}28}
2829
29describe("uiStateStore pure functions", () => {30describe("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
30 it("markThreadUnread moves lastVisitedAt before completion for a completed thread", () => {53 it("markThreadUnread moves lastVisitedAt before completion for a completed thread", () => {
31 const threadId = ThreadId.make("thread-1");54 const threadId = ThreadId.make("thread-1");
32 const latestTurnCompletedAt = "2026-02-25T12:30:00.000Z";55 const latestTurnCompletedAt = "2026-02-25T12:30:00.000Z";