Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

本文件记录面向用户和集成方的重要变化。完整的发布叙事、升级说明和下载入口见对应版本的中文发布说明;实现证据和测试门禁见技术发布说明。

## [1.0.4] - 2026-09-28

Windows x64 Electron 修复版本。发布状态和下载以 [GitHub Release](https://github.com/Dailin521/codex-provider-sync/releases/tag/v1.0.4) 为准;[升级说明](docs/release-notes/v1.0.4-zh.md)。

- 修复同一线程有多个分页 rollout 时,文件已同步但 SQLite 因关联冲突无法更新的问题(#107)。归属明确的分页文件组可一次同步;已有半同步状态可重试补齐 SQLite,随后无需再次创建备份。
- 修复 Windows 普通路径与 `\\?\` 路径别名的关联比较。缺少归属证据、锁定或执行期间发生变化的文件仍跳过并保留关联索引,正常数据继续处理。
- 感谢 [@Lorikein12138](https://github.com/Lorikein12138) 提供复现步骤与分页文件、Windows 长路径的根因分析。

## [1.0.3] - 2026-09-15

- 已下载更新由用户决定安装:存储配置、写操作、Watch 和恢复状态不再阻止“重启并安装”。
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Thank you to everyone who has contributed code, documentation, testing, investig
| [@Hccake](https://github.com/Hccake) | 独立 SQLite Home、恢复迁移保护和 WSL 路径安全 / Independent SQLite Home, restore relocation safeguards, and WSL path safety ([#55](https://github.com/Dailin521/codex-provider-sync/pull/55)) |
| [@JoJoJoinme](https://github.com/JoJoJoinme) | Codex 状态数据库位置检测 / Codex state-database location detection ([#36](https://github.com/Dailin521/codex-provider-sync/pull/36)) |
| [@Liuwei1125](https://github.com/Liuwei1125) | 嵌套 SQLite 状态数据库问题调查 / Nested SQLite state-database investigation |
| [@Lorikein12138](https://github.com/Lorikein12138) | 分页会话同步冲突与 Windows 长路径关联问题的复现和根因分析 / Reproduction and root-cause analysis of paginated session sync conflicts and Windows long-path associations ([#107](https://github.com/Dailin521/codex-provider-sync/issues/107)) |
| [@panjinhe](https://github.com/panjinhe) | Windows 同步性能、CLI 进度和瞬时锁重试 / Windows sync performance, CLI progress, and transient lock retries |
| [@PaulDing98](https://github.com/PaulDing98) | Provider/model 同步、watch 和桌面端单实例流程 / Provider/model synchronization, watch mode, and desktop single-instance flow ([#41](https://github.com/Dailin521/codex-provider-sync/pull/41), [#42](https://github.com/Dailin521/codex-provider-sync/pull/42)) |
| [@Plintun-Max](https://github.com/Plintun-Max) | 旧版 Node.js 的 SQLite fallback / SQLite fallback for older Node.js ([#40](https://github.com/Dailin521/codex-provider-sync/pull/40)) |
Expand Down
87 changes: 87 additions & 0 deletions apps/desktop/e2e/desktop-production-boundary.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,93 @@ test("production or unpacked desktop completes real Sync and Restore through Uti
}
});

test("production desktop syncs one paginated thread group, noops, and restores through Utility Core", async () => {
test.setTimeout(PRODUCTION_SMOKE_TIMEOUT_MS);
const fixture = await createDesktopSyncSwitchFixture({ paginatedGroup: true });
await claimDailyUpdateCheck(fixture.userData);
const baseline = await fixture.inspectPaginatedGroup();
expect(baseline.rollouts.map((rollout) => rollout.ordinal)).toEqual([7, 7]);
expect(baseline.rollouts.every((rollout) => rollout.metadata.history_mode === "paginated")).toBe(true);
expect(baseline.sqlite.rolloutPath).toBe(fixture.rolloutPath);
let electronApp;
try {
electronApp = await launchProductionDesktop({
args: [
...(packagedExecutable ? [] : [path.join(desktopRoot, "out", "main", "index.js")]),
`--user-data-dir=${fixture.userData}`,
"--lang=en-US"
],
env: {
...process.env,
CODEX_HOME: fixture.codexHome,
CPS_DESKTOP_E2E: "1",
CPS_DESKTOP_WINDOW_DISPLAY: "hidden",
ELECTRON_ENABLE_SECURITY_WARNINGS: "true"
}
});
const page = await electronApp.firstWindow();
await waitForProductionReady(page);

const result = page.getByRole("dialog", { name: "Operation result" });
const directSync = page.getByRole("button", { name: "Sync now", exact: true });
await expect(directSync).toBeEnabled();
await directSync.click();
await expect(result.getByRole("heading", { name: "Completed", exact: true })).toBeVisible({
timeout: PRODUCTION_OPERATION_TIMEOUT_MS
});
const synced = await fixture.inspectPaginatedGroup();
expect(synced.rollouts).toHaveLength(2);
for (const [index, rollout] of synced.rollouts.entries()) {
const { model_provider: provider, ...metadata } = rollout.metadata;
const { model_provider: originalProvider, ...originalMetadata } = baseline.rollouts[index].metadata;
expect(provider).toBe("prov_a");
expect(originalProvider).toBe("openai");
expect(metadata).toEqual(originalMetadata);
expect(rollout.headerWithoutProvider).toEqual(baseline.rollouts[index].headerWithoutProvider);
expect(rollout.bodyHash).toBe(baseline.rollouts[index].bodyHash);
expect(rollout.ordinal).toBe(baseline.rollouts[index].ordinal);
expect(rollout.turnContext).toEqual(baseline.rollouts[index].turnContext);
expect(rollout.body).toBe(baseline.rollouts[index].body);
}
expect(synced.sqlite).toEqual({ ...baseline.sqlite, provider: "prov_a" });
expect(synced.sqlite.rolloutPath).toBe(fixture.rolloutPath);
expect(synced.backupIds).toHaveLength(1);
const syncBackupId = synced.backupIds[0];
const syncLogs = await page.evaluate(() => window.codexProvider.operationLogs.list({ schemaVersion: 1, page: 1, pageSize: 50, operation: "sync" }));
const completedSync = syncLogs.entries.find((entry) => entry.status === "completed");
expect(completedSync.counts.changedSessionFiles).toBe(2);
expect(completedSync.counts.sqliteRowsUpdated).toBe(1);
await result.getByRole("button", { name: "Close", exact: true }).last().click();

await expect(directSync).toBeEnabled({ timeout: PRODUCTION_OPERATION_TIMEOUT_MS });
await directSync.click();
await expect(result.getByRole("heading", { name: "Completed", exact: true })).toBeVisible({
timeout: PRODUCTION_OPERATION_TIMEOUT_MS
});
await result.getByRole("button", { name: "Close", exact: true }).last().click();
const afterNoop = await fixture.inspectPaginatedGroup();
expect(afterNoop.rollouts).toEqual(synced.rollouts);
expect(afterNoop.sqlite).toEqual(synced.sqlite);
expect(afterNoop.backupIds).toEqual([syncBackupId]);

await page.getByRole("button", { name: "Backups / Restore", exact: true }).click();
await page.getByRole("button", { name: new RegExp(syncBackupId) }).click();
await page.getByRole("button", { name: "Preview restore", exact: true }).click();
const restoreDialog = page.getByRole("dialog", { name: "Confirm restore" });
await expect(restoreDialog).toBeVisible();
await restoreDialog.getByRole("button", { name: "Confirm restore", exact: true }).click();
await expect(restoreDialog).toBeHidden({ timeout: PRODUCTION_OPERATION_TIMEOUT_MS });
await expect(result.getByRole("heading", { name: "Completed", exact: true })).toBeVisible({
timeout: PRODUCTION_OPERATION_TIMEOUT_MS
});
const restored = await fixture.inspectPaginatedGroup();
expect(restored.rollouts).toEqual(baseline.rollouts);
expect(restored.sqlite).toEqual(baseline.sqlite);
} finally {
try { await electronApp?.close(); } finally { await fixture.close(); }
}
});


for (const mixed of [false, true]) test(mixed ? "production desktop syncs healthy mixed data and restores" : "production desktop syncs and restores large session metadata", async () => {
test.setTimeout(PRODUCTION_SMOKE_TIMEOUT_MS);
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codex-provider-sync/desktop",
"version": "1.0.3",
"version": "1.0.4",
"description": "Desktop app for synchronizing Codex Provider metadata and local session history.",
"homepage": "https://github.com/Dailin521/codex-provider-sync#readme",
"author": "Dailin521",
Expand Down
6 changes: 3 additions & 3 deletions desktop/CodexProviderSync.App/CodexProviderSync.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
<AssemblyName>CodexProviderSync</AssemblyName>
<Product>Codex Provider Sync</Product>
<Company>Dailin521</Company>
<Version>1.0.3</Version>
<AssemblyVersion>1.0.3.0</AssemblyVersion>
<FileVersion>1.0.3.0</FileVersion>
<Version>1.0.4</Version>
<AssemblyVersion>1.0.4.0</AssemblyVersion>
<FileVersion>1.0.4.0</FileVersion>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<Version>1.0.3</Version>
<AssemblyVersion>1.0.3.0</AssemblyVersion>
<FileVersion>1.0.3.0</FileVersion>
<Version>1.0.4</Version>
<AssemblyVersion>1.0.4.0</AssemblyVersion>
<FileVersion>1.0.4.0</FileVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
<Nullable>enable</Nullable>
<AssemblyName>CodexProviderSync.Automation</AssemblyName>
<RootNamespace>CodexProviderSync.Automation</RootNamespace>
<Version>1.0.3</Version>
<AssemblyVersion>1.0.3.0</AssemblyVersion>
<FileVersion>1.0.3.0</FileVersion>
<Version>1.0.4</Version>
<AssemblyVersion>1.0.4.0</AssemblyVersion>
<FileVersion>1.0.4.0</FileVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
6 changes: 3 additions & 3 deletions desktop/CodexProviderSync.Core/CodexProviderSync.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>1.0.3</Version>
<AssemblyVersion>1.0.3.0</AssemblyVersion>
<FileVersion>1.0.3.0</FileVersion>
<Version>1.0.4</Version>
<AssemblyVersion>1.0.4.0</AssemblyVersion>
<FileVersion>1.0.4.0</FileVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<RootNamespace>CodexProviderSync.GuiE2E</RootNamespace>
<Version>1.0.3</Version>
<AssemblyVersion>1.0.3.0</AssemblyVersion>
<FileVersion>1.0.3.0</FileVersion>
<Version>1.0.4</Version>
<AssemblyVersion>1.0.4.0</AssemblyVersion>
<FileVersion>1.0.4.0</FileVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
6 changes: 3 additions & 3 deletions desktop/CodexProviderSync.Mac/CodexProviderSync.Mac.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
<AssemblyName>CodexProviderSync</AssemblyName>
<Product>Codex Provider Sync</Product>
<Company>Dailin521</Company>
<Version>1.0.3</Version>
<AssemblyVersion>1.0.3.0</AssemblyVersion>
<FileVersion>1.0.3.0</FileVersion>
<Version>1.0.4</Version>
<AssemblyVersion>1.0.4.0</AssemblyVersion>
<FileVersion>1.0.4.0</FileVersion>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>

Expand Down
2 changes: 1 addition & 1 deletion docs/README_DESKTOP_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ Watch 需在“设置”主动开启;当前状态按存储配置显示。指
- 便携版/本地打包版打开官方下载页。退出后解压完整新版到新目录,不要只替换一个 EXE。
- 已启用正式更新通道的安装版可按需下载,再确认重启安装;安装由用户决定,存储状态、写操作、自动同步或未完成恢复不会禁用安装按钮。
- 设置更新区和新版弹窗可选“不再提醒此版本”;重启后保留,下个新版仍提醒。仍可手动检查/更新,也可在设置中恢复该版本提醒。
- 1.0.3 支持大首行及问题会话跳过。受影响的 1.0.1 安装版无法正常检查更新,请从官方下载页手动安装新版一次,详见[升级说明](release-notes/v1.0.3-zh.md)。线上跨版本下载安装仍待独立验收。
- 1.0.4 修复分页会话的多文件同步冲突,可再次同步补齐此前未对齐的索引,并保留 1.0.3 的大首行和问题会话跳过能力。受影响的 1.0.1 安装版无法正常检查更新,请从官方下载页手动安装新版一次,详见[升级说明](release-notes/v1.0.4-zh.md)。线上跨版本下载安装仍待独立验收。
- **旧 Windows .NET 的单 EXE 自更新不能直接升级到 Electron。** 首次迁移需安装/解压完整新版;不把 NSIS 安装器冒充旧 EXE,不承诺已经完成线上跨版本升级验证。
- 更新程序不替换 Codex 数据。下载安装方式、签名与更新通道以实际发布说明为准。

Expand Down
8 changes: 7 additions & 1 deletion docs/WINDOWS_ELECTRON_RELEASE_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,13 @@ main push 始终完整执行全部原有任务和 C10,最终 SHA 的发布门

正式版公告先通过 `node scripts/read-release-metadata.js --tag v1.0.x`,然后按该版本的明确授权执行 `gh release edit v1.0.x --draft=false --prerelease=false --latest=true --notes-file docs/release-notes/v1.0.x-zh.md`。公开后再次检查 `/releases/latest`、tag SHA、对应渠道的全部下载资产与哈希。RC 只公开 prerelease,不使用正式版命令,也不设 latest。

## 6. 当前 1.0.3 发布与历史例外
## 6. 当前 1.0.4 发布与历史例外

维护者已于 2026-09-28 确认本地 1.0.4 测试包成功修复 #107,并明确授权发布新版及感谢报告者。本轮为 **Windows x64 Electron 1.0.4**,沿用 `stable-updater`:安装版由用户显式下载并确认重启安装,便携版手动更新。先处理 PR #108 的全部有效审查并通过当前 head CI,采用 merge commit 合并;最终 main SHA 再次通过完整 CI 后,创建新的 `v1.0.4` 标签,准备并验收同 SHA 的 10 项附件,再公开对应 Release。授权不替代门禁,不得移动旧标签或覆盖既有 Release。

发布说明与贡献者名单感谢 [@Lorikein12138](https://github.com/Lorikein12138) 在 [#107](https://github.com/Dailin521/codex-provider-sync/issues/107) 的复现和根因分析。本地手测仅确认 #107 修复,不能扩展为线上升级或真实安装验收;安装/退出/卸载和 ZIP 解压验收由隔离容器完成。仍需公开说明未签名、线上跨版本下载安装尚未独立验收;不发布 npm、Legacy 或其他平台包。

### 1.0.3 发布授权记录

维护者已于 2026-09-15 授权提交、合并并发布新的 Windows 包。本次版本为 **1.0.3**,交付 128 MiB 首行、问题会话逐文件跳过和首行校验修复。遵循第 1~5 节:最终 main SHA 的 CI 通过后创建新的 `v1.0.3` 标签,以 `stable-updater` 准备并验收 10 项附件,再公开对应的新 Release。不得移动旧标签或覆盖既有 Release。

Expand Down
4 changes: 4 additions & 0 deletions docs/WORKING_PRINCIPLE_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

问题会话会被跳过,其关联索引保留原样;正常会话继续同步/切换。有跳过会显示“部分完成”,在预览、结果和操作日志可查看原因及本机完整路径(最多 200 项)。处理问题后重新预览即可纳入;全部历史跳过时,切换仍会备份并更新配置。数据库/目录/备份等全局故障仍停止,已写入时保留备份。诊断导出移除路径和索引标识。

同一线程有多个 rollout 文件时,只有每个文件首行都明确标为分页历史、线程 ID 与 SQLite 一致,而且 SQLite 记录的 rollout 路径能唯一指向组内一个有效文件,才会把它们作为同一线程同步。文件的 ordinal 只是位置,重复并不表示冲突或先后关系。缺少分页标记、路径无法唯一确认、跨线程或多个记录争用时,工具会保留索引并提示部分完成。组内任一文件被锁定、变化或写入失败,该线程的 SQLite Provider 也会保留,下一次重新同步再收敛;如果文件已同步而 SQLite 未同步,重试只会补齐 SQLite,之后的同步是 noop,不会新建备份。

Windows 会识别普通路径与 `\\?\`、UNC 与 `\\?\UNC\` 的等价写法后再检查是否仍在 Codex Home 内;不会改写数据库保存的路径,也不会因此接受设备、越界或 WSL 路径。同步期间新出现的文件本轮只做有限首行确认并延后处理;未知或损坏的新文件会保守保留相关 SQLite 行。

> 适用于当前 V1 Node Core(CLI / Web / Electron)。Legacy .NET 的历史行为不作为新实现规范。
> 用户安装见 [README](../README.md);开发约束、模块映射及测试入口以 [Node Core 当前架构](architecture/NODE_CORE_ARCHITECTURE_ZH.md) 为准,本文只解释原理。

Expand Down
Loading
Loading