From db4f6d72f498c803051bf18f227c25fdeb29f78d Mon Sep 17 00:00:00 2001 From: EchoTreee <2024140044@mails.szu.edu.cn> Date: Wed, 16 Sep 2026 13:39:30 +0800 Subject: [PATCH] feat: resume by session ID or ID prefix Let resume take a session ID (or ID prefix) in addition to a title keyword. ID-shaped input (hex/hyphen, 8+ chars) matches the session id by prefix; other input matches the title. Adds a smoke test and updates docs. --- CHANGELOG.md | 1 + README.md | 4 ++-- README.zh-CN.md | 8 ++++---- codex-switch | 35 +++++++++++++++++++++++------------ docs/usage.md | 2 +- tests/test_smoke.py | 25 +++++++++++++++++++++++++ 6 files changed, 56 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9799f..1a7df8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 37f2190..3033741 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,7 @@ After an external `codex login` or a manual provider change, run `save ` 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. @@ -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 ` | Resume one matching CLI session with current provider/model overrides. | +| `resume ` | Resume one matching CLI session (by title keyword or session ID) with current provider/model overrides. | | `delete ` | Remove the saved profile; leave active Codex files untouched. | | `help` | Show built-in help. | diff --git a/README.zh-CN.md b/README.zh-CN.md index e1c8d53..52fb9da 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -21,7 +21,7 @@ ```bash codex-switch save work # 保存当前登录态和配置 codex-switch use personal # 切换到已经保存的个人配置 -codex-switch resume "parser" # 按标题关键词继续本地会话 +codex-switch resume "parser" # 按标题关键词或会话 ID 继续本地会话 ``` ## 六个核心优势 @@ -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 就够了? @@ -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 会话 ``` 设备码重新登录的正确顺序是: @@ -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` | 查看内置帮助。 | diff --git a/codex-switch b/codex-switch index 68f514f..5175f10 100755 --- a/codex-switch +++ b/codex-switch @@ -12,7 +12,7 @@ # codex-switch relogin <名字> 重新登录后存成 profile(token 失效时用;加 --device-auth 用设备码) # codex-switch list 列出所有 profile 和当前登录的账号 # codex-switch sessions 列出所有会话(id+provider+标题),方便 codex resume -# codex-switch resume <关键词> 按标题关键词恢复会话,自动用当前 provider/model(跨 provider 恢复) +# codex-switch resume <关键词|ID> 按标题关键词或会话 ID 恢复,自动用当前 provider/model(跨 provider 恢复) # codex-switch status 查看当前登录态与配置 # codex-switch delete <名字> 删除某个 profile # codex-switch help 显示帮助 @@ -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 :" + echo "匹配到 $n 条,请输入更精确的关键词或更长的 ID 前缀:" printf '%s\n' "$out" | awk -F'\t' '{printf " %s %-8s %s %s\n", $4, $3, $1, $2}' exit 1 fi diff --git a/docs/usage.md b/docs/usage.md index 2d1e8ef..fa5c592 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -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 ` 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. diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 9f4379c..7f803ce 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -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(