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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- `resume` accepts a session ID or ID prefix (hex/hyphen, 8+ characters) in addition to a title keyword.
- Optional herdr companion recommendations in both READMEs and parallel guides, including pane setup and the distinction between detaching and restarting an agent.
- Six core benefits at the top of both READMEs, plus bilingual parallel-workflow guides covering worktrees, startup identities, token snapshots, and safe timing for recovery.
- Separate official-account and official + relay workflow guides in both READMEs, with a comparison of authentication versus provider changes, shared history, bidirectional resume examples, and version-dependent provider writeback notes.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ After an external `codex login` or a manual provider change, run `save <name>` b
codex-switch list # saved profiles; * marks the recorded active one
codex-switch status # local paths, account ID, and provider
codex-switch sessions # local CLI / exec sessions
codex-switch resume "parser" # use a title keyword matching one CLI session
codex-switch resume "parser" # resume by title keyword or session ID
```

See [provider setup, recovery, and troubleshooting](docs/usage.md) for the next steps.
Expand All @@ -257,7 +257,7 @@ See [provider setup, recovery, and troubleshooting](docs/usage.md) for the next
| `list` | Show saved profiles and the recorded active profile. |
| `status` | Show local file locations and account/provider metadata. |
| `sessions` | List local sessions from the expected SQLite schema. |
| `resume <keyword>` | Resume one matching CLI session with current provider/model overrides. |
| `resume <keyword or id>` | Resume one matching CLI session (by title keyword or session ID) with current provider/model overrides. |
| `delete <name>` | Remove the saved profile; leave active Codex files untouched. |
| `help` | Show built-in help. |

Expand Down
8 changes: 4 additions & 4 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
```bash
codex-switch save work # 保存当前登录态和配置
codex-switch use personal # 切换到已经保存的个人配置
codex-switch resume "parser" # 按标题关键词继续本地会话
codex-switch resume "parser" # 按标题关键词或会话 ID 继续本地会话
```

## 六个核心优势
Expand Down Expand Up @@ -105,7 +105,7 @@ codex-switch use official-personal
codex resume # 选择同一条会话继续
```

要精确恢复,可把会话 ID 填入 `codex resume "your-session-id"`。也可以用 `codex-switch sessions` 查找,再用 `codex-switch resume "parser"` 按标题恢复,并显式采用当前 profile 的模型设置。
要精确恢复,可以直接把会话 ID 传给 `codex-switch resume "your-session-id"`,或填入原生 `codex resume`。也可以用 `codex-switch sessions` 查找,再用 `codex-switch resume "parser"` 按标题恢复,并显式采用当前 profile 的模型设置。

### 为什么通常直接 resume 就够了?

Expand Down Expand Up @@ -241,7 +241,7 @@ codex
codex-switch list # 查看已保存的 profile,* 表示记录的活跃项
codex-switch status # 查看本地路径、账号 ID 和 provider
codex-switch sessions # 列出本地 CLI / exec 会话
codex-switch resume "parser" # 关键词需要唯一匹配一个 CLI 会话
codex-switch resume "parser" # 标题关键词或会话 ID,需唯一匹配一个 CLI 会话
```

设备码重新登录的正确顺序是:
Expand All @@ -263,7 +263,7 @@ codex-switch relogin --device-auth work
| `list` | 列出 profile 和记录的活跃项。 |
| `status` | 查看本地文件路径和账号/provider 元信息。 |
| `sessions` | 从预期的 SQLite 表结构读取本地会话。 |
| `resume <关键词>` | 用当前 provider/model 参数恢复唯一匹配的 CLI 会话。 |
| `resume <关键词或ID>` | 用当前 provider/model 参数恢复唯一匹配的 CLI 会话(按标题关键词或会话 ID)。 |
| `delete <名字>` | 删除已保存的 profile,不删除当前 Codex 登录文件。 |
| `help` | 查看内置帮助。 |

Expand Down
35 changes: 23 additions & 12 deletions codex-switch
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# codex-switch relogin <名字> 重新登录后存成 profile(token 失效时用;加 --device-auth 用设备码)
# codex-switch list 列出所有 profile 和当前登录的账号
# codex-switch sessions 列出所有会话(id+provider+标题),方便 codex resume <id>
# codex-switch resume <关键词> 按标题关键词恢复会话,自动用当前 provider/model(跨 provider 恢复)
# codex-switch resume <关键词|ID> 按标题关键词或会话 ID 恢复,自动用当前 provider/model(跨 provider 恢复)
# codex-switch status 查看当前登录态与配置
# codex-switch delete <名字> 删除某个 profile
# codex-switch help 显示帮助
Expand Down Expand Up @@ -254,34 +254,45 @@ PY
}

cmd_resume() {
[ $# -ge 1 ] || die "用法:codex-switch resume <标题关键词>"
[ $# -ge 1 ] || die "用法:codex-switch resume <标题关键词|会话ID>"
local kw="$*"
local db="$CODEX_HOME/state_5.sqlite"
[ -f "$db" ] || die "找不到会话数据库 $db"
local out
out="$(python3 - "$db" "$kw" <<'PY'
import sqlite3, sys
import sqlite3, sys, re
db, kw = sys.argv[1], sys.argv[2].lower()
# 输入像会话 ID(十六进制/连字符,≥8 位)就按 id 前缀匹配,否则按标题关键词
by_id = re.fullmatch(r'[0-9a-f-]{8,}', kw) is not None
con = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
cur = con.cursor()
rows = cur.execute("""
SELECT id, COALESCE(title,''), model_provider,
datetime(updated_at,'unixepoch','localtime')
FROM threads
WHERE source='cli' AND lower(COALESCE(title,'')) LIKE ?
ORDER BY updated_at DESC
""", (f"%{kw}%",)).fetchall()
if by_id:
rows = cur.execute("""
SELECT id, COALESCE(title,''), model_provider,
datetime(updated_at,'unixepoch','localtime')
FROM threads
WHERE source='cli' AND lower(id) LIKE ?
ORDER BY updated_at DESC
""", (kw + '%',)).fetchall()
else:
rows = cur.execute("""
SELECT id, COALESCE(title,''), model_provider,
datetime(updated_at,'unixepoch','localtime')
FROM threads
WHERE source='cli' AND lower(COALESCE(title,'')) LIKE ?
ORDER BY updated_at DESC
""", (f"%{kw}%",)).fetchall()
for r in rows:
print("\t".join(r))
con.close()
PY
)"
if [ -z "$out" ]; then
die "没有标题匹配「${kw}」的会话(codex-switch sessions 可看全部)"
die "没有匹配「${kw}」的会话(按标题或 ID;codex-switch sessions 可看全部)"
fi
local n; n="$(printf '%s\n' "$out" | wc -l)"
if [ "$n" -gt 1 ]; then
echo "匹配到 $n 条,关键词再精确一点,或复制完整 ID 直接 codex resume <id>:"
echo "匹配到 $n 条,请输入更精确的关键词或更长的 ID 前缀:"
printf '%s\n' "$out" | awk -F'\t' '{printf " %s %-8s %s %s\n", $4, $3, $1, $2}'
exit 1
fi
Expand Down
2 changes: 1 addition & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ codex-switch sessions
codex-switch resume "parser regression"
```

`sessions` reads CLI and exec sessions from the expected local database **without a provider or working-directory filter**. The `*` marker identifies the current provider; it does not exclude other providers. `resume` also searches across providers, but only searches CLI session titles, using SQLite `LIKE`; `%` and `_` act as wildcards. Zero or multiple matches stop with an error instead of choosing a session automatically. Use a more specific keyword if necessary.
`sessions` reads CLI and exec sessions from the expected local database **without a provider or working-directory filter**. The `*` marker identifies the current provider; it does not exclude other providers. `resume` also searches across providers: it matches CLI session titles with SQLite `LIKE` (`%` and `_` act as wildcards), or — when the argument is a session-ID-shaped prefix (hex/hyphen, 8+ characters) — it matches session IDs by prefix instead. Zero or multiple matches stop with an error instead of choosing a session automatically. Use a more specific keyword or a longer ID prefix if necessary.

With a single match, the script runs `codex resume <id>` with `-c` overrides from the active configuration. A provider override is always supplied; a model override is supplied if the script finds a model. The database lookup itself is read-only. Once launched, Codex may update session state.

Expand Down
25 changes: 25 additions & 0 deletions tests/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,31 @@ def test_session_selection_and_provider_model_forwarding(self):
"resume", "fixture-cli-1", "-c", 'model_provider="new-provider"', "-c", 'model="new-model"',
])

def test_resume_by_session_id(self):
self.seed(provider="new-provider", model="new-model")
with closing(sqlite3.connect(self.home / "state_5.sqlite")) as db:
db.execute(
"CREATE TABLE threads "
"(id TEXT, title TEXT, model_provider TEXT, updated_at INTEGER, source TEXT)"
)
db.executemany("INSERT INTO threads VALUES (?, ?, ?, ?, ?)", [
("01a07ed8-18ce-7a02-9f06-e00bf007b95d", "Main experiment", "openai", 1700000000, "cli"),
("01a0aaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "Other experiment", "openai", 1700000001, "cli"),
])
db.commit()
# 完整 ID 唯一命中
self.run_cli("resume", "01a07ed8-18ce-7a02-9f06-e00bf007b95d")
self.assertEqual(json.loads(self.calls.read_text()), [
"resume", "01a07ed8-18ce-7a02-9f06-e00bf007b95d",
"-c", 'model_provider="new-provider"', "-c", 'model="new-model"',
])
# ID 前缀唯一命中
self.run_cli("resume", "01a0aaaa")
self.assertEqual(json.loads(self.calls.read_text()), [
"resume", "01a0aaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"-c", 'model_provider="new-provider"', "-c", 'model="new-model"',
])

def test_installer_from_another_directory_and_destination_with_spaces(self):
destination = self.base / "custom bin"
result = subprocess.run(
Expand Down
Loading