Skip to content
Open
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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ Use `codex-provider restore <backup-dir>` when:
- the user wants to roll back a previous sync
- the user synced to the wrong provider

Use `codex-provider delete <session-id> [...]` when:

- the user wants to permanently remove specific sessions by id
- the user expects rollout files and SQLite thread rows to be removed together

Use `codex-provider status` only when:

- the user asks for inspection only
Expand Down Expand Up @@ -126,6 +131,7 @@ codex-provider sync
codex-provider sync --keep 5
codex-provider sync --provider openai
codex-provider switch apigather
codex-provider delete 019d95ee-abd6-7de3-b1f8-63b13c725f17
codex-provider prune-backups --keep 5
codex-provider restore C:\Users\you\.codex\backups_state\provider-sync\20260319T042708906Z
```
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ codex-provider restore C:\Users\you\.codex\backups_state\provider-sync\<timestam
codex-provider prune-backups --keep 5
```

按 session id 删除会话(会先备份):

```bash
codex-provider delete <session-id>
codex-provider delete <session-id-1> <session-id-2>
```

## AI 一键处理

如果你想直接交给 AI 助手处理,把下面这段原样发给 AI:
Expand Down Expand Up @@ -144,6 +151,7 @@ codex-provider prune-backups --keep 5
- 只想查看状态:`codex-provider status`
- 当前 provider 不变,只修复历史会话可见性:`codex-provider sync`
- 切 provider 并同步历史:`codex-provider switch openai`
- 删除指定会话:`codex-provider delete <session-id>`
- 安装桌面双击启动器:`codex-provider install-windows-launcher`
- 回滚误操作:`codex-provider restore <backup-dir>`

Expand All @@ -159,6 +167,11 @@ codex-provider prune-backups --keep 5
- 修改 `config.toml` 根级 `model_provider`
- 然后立即执行一次同步
- `--keep <n>` 可覆盖这次执行后的备份保留数量
- `codex-provider delete <session-id> [more-session-ids...]`
- 按 session id 删除会话
- 会同时删除 rollout 文件中的目标会话文件,以及 SQLite `threads` 对应行
- 如果 rollout 文件被占用,会跳过对应会话并在结果里提示
- 删除前会自动创建备份,可用 `restore` 回滚
- `codex-provider prune-backups`
- 手动清理旧备份,只保留最近 `n` 份由本工具管理的备份
- `codex-provider restore <backup-dir>`
Expand All @@ -178,6 +191,8 @@ codex-provider sync
codex-provider sync --keep 5
codex-provider sync --provider openai
codex-provider switch apigather
codex-provider delete 019d95ee-abd6-7de3-b1f8-63b13c725f17
codex-provider delete 019d95ee-abd6-7de3-b1f8-63b13c725f17 019d95ef-c7fd-74b0-956c-6110fd8ff314
codex-provider prune-backups --keep 5
codex-provider install-windows-launcher
codex-provider install-windows-launcher --dir D:\Tools
Expand Down
15 changes: 15 additions & 0 deletions README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ Clean old managed backups manually:
codex-provider prune-backups --keep 5
```

Delete one or more sessions by id (with backup):

```bash
codex-provider delete <session-id>
codex-provider delete <session-id-1> <session-id-2>
```

## AI Quick Run

If you want an AI assistant to handle this in one shot, copy this prompt:
Expand Down Expand Up @@ -143,6 +150,7 @@ Quick mapping:
- inspect only: `codex-provider status`
- fix visibility under current provider: `codex-provider sync`
- switch provider and sync: `codex-provider switch openai`
- delete a specific session: `codex-provider delete <session-id>`
- install a desktop double-click launcher: `codex-provider install-windows-launcher`
- roll back a mistake: `codex-provider restore <backup-dir>`

Expand All @@ -158,6 +166,11 @@ Quick mapping:
- updates root `model_provider` in `config.toml`
- immediately runs a sync
- `--keep <n>` overrides how many managed backups are retained after the run
- `codex-provider delete <session-id> [more-session-ids...]`
- deletes sessions by id
- removes matching rollout session files and matching rows in SQLite `threads`
- skips sessions whose rollout files are currently locked, and reports them
- creates a backup before deletion so `restore` can roll back
- `codex-provider prune-backups`
- manually removes older managed backups and keeps the newest `n`
- `codex-provider restore <backup-dir>`
Expand All @@ -176,6 +189,8 @@ codex-provider sync --keep 5
codex-provider sync --provider openai
codex-provider switch openai
codex-provider switch apigather
codex-provider delete 019d95ee-abd6-7de3-b1f8-63b13c725f17
codex-provider delete 019d95ee-abd6-7de3-b1f8-63b13c725f17 019d95ef-c7fd-74b0-956c-6110fd8ff314
codex-provider prune-backups --keep 5
codex-provider install-windows-launcher
codex-provider install-windows-launcher --dir D:\Tools
Expand Down
15 changes: 15 additions & 0 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ codex-provider restore C:\Users\you\.codex\backups_state\provider-sync\<timestam
codex-provider prune-backups --keep 5
```

按 session id 删除会话(会先备份):

```bash
codex-provider delete <session-id>
codex-provider delete <session-id-1> <session-id-2>
```

## AI 一键处理

如果你想直接交给 AI 助手处理,把下面这段原样发给 AI:
Expand Down Expand Up @@ -144,6 +151,7 @@ codex-provider prune-backups --keep 5
- 只想查看状态:`codex-provider status`
- 当前 provider 不变,只修复历史会话可见性:`codex-provider sync`
- 切 provider 并同步历史:`codex-provider switch openai`
- 删除指定会话:`codex-provider delete <session-id>`
- 安装桌面双击启动器:`codex-provider install-windows-launcher`
- 回滚误操作:`codex-provider restore <backup-dir>`

Expand All @@ -159,6 +167,11 @@ codex-provider prune-backups --keep 5
- 修改 `config.toml` 根级 `model_provider`
- 然后立即执行一次同步
- `--keep <n>` 可覆盖这次执行后的备份保留数量
- `codex-provider delete <session-id> [more-session-ids...]`
- 按 session id 删除会话
- 会同时删除 rollout 文件中的目标会话文件,以及 SQLite `threads` 对应行
- 如果 rollout 文件被占用,会跳过对应会话并在结果里提示
- 删除前会自动创建备份,可用 `restore` 回滚
- `codex-provider prune-backups`
- 手动清理旧备份,只保留最近 `n` 份由本工具管理的备份
- `codex-provider restore <backup-dir>`
Expand All @@ -178,6 +191,8 @@ codex-provider sync
codex-provider sync --keep 5
codex-provider sync --provider openai
codex-provider switch apigather
codex-provider delete 019d95ee-abd6-7de3-b1f8-63b13c725f17
codex-provider delete 019d95ee-abd6-7de3-b1f8-63b13c725f17 019d95ef-c7fd-74b0-956c-6110fd8ff314
codex-provider prune-backups --keep 5
codex-provider install-windows-launcher
codex-provider install-windows-launcher --dir D:\Tools
Expand Down
61 changes: 57 additions & 4 deletions src/backup.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export async function createBackup({
codexHome,
targetProvider,
sessionChanges,
deletedSessionFiles = [],
configPath,
configBackupText
}) {
Expand All @@ -55,6 +56,32 @@ export async function createBackup({
await copyIfPresent(configPath, path.join(backupDir, "config.toml"));
}

const deletedFilesDir = path.join(backupDir, "deleted-session-files");
await fs.mkdir(deletedFilesDir, { recursive: true });
const deletedFilesManifest = [];
let deletedFileCounter = 0;
for (const rawFilePath of deletedSessionFiles ?? []) {
const absolutePath = path.resolve(rawFilePath);
const relativePath = path.relative(codexHome, absolutePath);
if (!relativePath || relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
continue;
}

const backupFileName = `${String(deletedFileCounter).padStart(4, "0")}${path.extname(absolutePath) || ".jsonl"}`;
const backupRelativePath = path.join("deleted-session-files", backupFileName);
const copied = await copyIfPresent(absolutePath, path.join(backupDir, backupRelativePath));
if (!copied) {
continue;
}

deletedFilesManifest.push({
path: absolutePath,
relativePath,
backupRelativePath
});
deletedFileCounter += 1;
}

const sessionManifest = {
version: 1,
namespace: BACKUP_NAMESPACE,
Expand All @@ -65,7 +92,8 @@ export async function createBackup({
path: change.path,
originalFirstLine: change.originalFirstLine,
originalSeparator: change.originalSeparator
}))
})),
deletedFiles: deletedFilesManifest
};
await fs.writeFile(
path.join(backupDir, "session-meta-backup.json"),
Expand All @@ -83,7 +111,8 @@ export async function createBackup({
targetProvider,
createdAt: sessionManifest.createdAt,
dbFiles: copiedDbFiles,
changedSessionFiles: sessionChanges.length
changedSessionFiles: sessionChanges.length,
deletedSessionFiles: deletedFilesManifest.length
},
null,
2
Expand Down Expand Up @@ -151,18 +180,23 @@ export async function restoreBackup(backupDir, codexHome, options = {}) {
const {
restoreConfig = true,
restoreDatabase = true,
restoreSessions = true
restoreSessions = true,
restoreDeletedSessionFiles = true
} = options;
const metadataPath = path.join(backupDir, "metadata.json");
const metadata = JSON.parse(await fs.readFile(metadataPath, "utf8"));
if (metadata.codexHome !== codexHome) {
throw new Error(`Backup was created for ${metadata.codexHome}, not ${codexHome}.`);
}

const needsSessionManifest = restoreSessions || restoreDeletedSessionFiles;
let sessionManifest = null;
if (restoreSessions) {
if (needsSessionManifest) {
const sessionManifestPath = path.join(backupDir, "session-meta-backup.json");
sessionManifest = JSON.parse(await fs.readFile(sessionManifestPath, "utf8"));
}

if (restoreSessions) {
await assertSessionFilesWritable(sessionManifest.files ?? []);
}

Expand Down Expand Up @@ -191,9 +225,28 @@ export async function restoreBackup(backupDir, codexHome, options = {}) {
await restoreSessionChanges(sessionManifest.files ?? []);
}

if (restoreDeletedSessionFiles) {
await restoreDeletedSessionFilesFromBackup(backupDir, codexHome, sessionManifest?.deletedFiles ?? []);
}

return metadata;
}

async function restoreDeletedSessionFilesFromBackup(backupDir, codexHome, deletedFiles) {
for (const entry of deletedFiles ?? []) {
const relativePath = String(entry?.relativePath ?? "").trim();
const backupRelativePath = String(entry?.backupRelativePath ?? "").trim();
if (!relativePath || !backupRelativePath) {
continue;
}

const sourcePath = path.join(backupDir, backupRelativePath);
const destinationPath = path.join(codexHome, relativePath);
await fs.mkdir(path.dirname(destinationPath), { recursive: true });
await copyIfPresent(sourcePath, destinationPath);
}
}

async function listManagedBackupDirectories(backupRoot) {
let entries;
try {
Expand Down
Loading