Skip to content

Commit fdd76cb

Browse files
committed
fix(sync): preserve newer local selection on mtime fallback
1 parent 0d8dcfa commit fdd76cb

2 files changed

Lines changed: 87 additions & 5 deletions

File tree

‎lib/codex-cli/sync.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -511,9 +511,9 @@ function shouldApplyCodexCliSelection(
511511
if (localVersion <= 0) {
512512
return persistedLocalTimestamp !== null;
513513
}
514-
// Keep local selection when plugin wrote more recently than Codex state.
515-
const toleranceMs = hasSyncVersion ? 0 : 1_000;
516-
return codexVersion >= localVersion - toleranceMs;
514+
// When only source mtime is available, require Codex to be at least as new as the
515+
// local selection. A grace window here can overwrite a newer persisted local choice.
516+
return codexVersion >= localVersion;
517517
}
518518

519519
function reconcileCodexCliState(

‎test/codex-cli-sync.test.ts‎

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@ describe("codex-cli sync", () => {
558558
expect(preview.summary.selectionChanged).toBe(false);
559559
});
560560

561-
it("preserves a newer persisted local selection after restart", async () => {
561+
it("preserves a newer persisted local selection after restart when the target mtime is only 500ms newer", async () => {
562562
await writeFile(
563563
accountsPath,
564564
JSON.stringify(
@@ -594,7 +594,7 @@ describe("codex-cli sync", () => {
594594
);
595595

596596
const sourceTime = new Date("2026-03-13T00:00:00.000Z");
597-
const targetTime = new Date("2026-03-13T00:00:05.000Z");
597+
const targetTime = new Date("2026-03-13T00:00:00.500Z");
598598
await utimes(accountsPath, sourceTime, sourceTime);
599599
await writeFile(targetStoragePath, "{\"version\":3}", "utf-8");
600600
await utimes(targetStoragePath, targetTime, targetTime);
@@ -638,6 +638,88 @@ describe("codex-cli sync", () => {
638638
expect(preview.summary.selectionChanged).toBe(false);
639639
});
640640

641+
it("preserves a newer persisted local selection on apply when the target mtime is only 500ms newer", async () => {
642+
await writeFile(
643+
accountsPath,
644+
JSON.stringify(
645+
{
646+
activeAccountId: "acc_a",
647+
accounts: [
648+
{
649+
accountId: "acc_a",
650+
email: "a@example.com",
651+
auth: {
652+
tokens: {
653+
access_token: "access-a",
654+
refresh_token: "refresh-a",
655+
},
656+
},
657+
},
658+
{
659+
accountId: "acc_b",
660+
email: "b@example.com",
661+
auth: {
662+
tokens: {
663+
access_token: "access-b",
664+
refresh_token: "refresh-b",
665+
},
666+
},
667+
},
668+
],
669+
},
670+
null,
671+
2,
672+
),
673+
"utf-8",
674+
);
675+
676+
const sourceTime = new Date("2026-03-13T00:00:00.000Z");
677+
const targetTime = new Date("2026-03-13T00:00:00.500Z");
678+
await utimes(accountsPath, sourceTime, sourceTime);
679+
await writeFile(targetStoragePath, "{\"version\":3}", "utf-8");
680+
await utimes(targetStoragePath, targetTime, targetTime);
681+
682+
vi.spyOn(storageModule, "getLastAccountsSaveTimestamp").mockReturnValue(0);
683+
vi.spyOn(writerModule, "getLastCodexCliSelectionWriteTimestamp").mockReturnValue(
684+
0,
685+
);
686+
687+
const current: AccountStorageV3 = {
688+
version: 3,
689+
accounts: [
690+
{
691+
accountId: "acc_a",
692+
accountIdSource: "token",
693+
email: "a@example.com",
694+
refreshToken: "refresh-a",
695+
accessToken: "access-a",
696+
addedAt: 1,
697+
lastUsed: 1,
698+
},
699+
{
700+
accountId: "acc_b",
701+
accountIdSource: "token",
702+
email: "b@example.com",
703+
refreshToken: "refresh-b",
704+
accessToken: "access-b",
705+
addedAt: 1,
706+
lastUsed: 1,
707+
},
708+
],
709+
activeIndex: 1,
710+
activeIndexByFamily: { codex: 1 },
711+
};
712+
713+
const result = await applyCodexCliSyncToStorage(current, {
714+
forceRefresh: true,
715+
});
716+
717+
expect(result.changed).toBe(false);
718+
expect(result.pendingRun).toBeNull();
719+
expect(result.storage?.activeIndex).toBe(1);
720+
expect(result.storage?.activeIndexByFamily?.codex).toBe(1);
721+
});
722+
641723
it("preserves a newer local selection when Codex state has no timestamp metadata", async () => {
642724
const state = {
643725
path: accountsPath,

0 commit comments

Comments
 (0)