cxs is a local-first CLI for searching Codex session logs. It is designed for progressive retrieval: find the right session first, then read
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(cli): 支持 coverage 检查和最近排序

catoncat 8472118a 052d897f

+547 -70
+4 -2
AGENTS.md
··· 7 7 当前接受的产品边界: 8 8 9 9 - 命令面固定为:`status`、`sync`、`find`、`read-range`、`read-page`、`list`、`stats` 10 - - 主工作流固定为:`status -> sync --selector -> find/list -> read-range/read-page` 10 + - 主工作流固定为:`status -> ensure selector coverage -> find/list -> read-range/read-page` 11 11 - `sync` 是唯一会修改索引的命令;其余命令只读 SQLite 12 12 - 默认接受手动增量同步,不做 watcher / daemon / realtime sync 13 13 - 这个仓库可以作为其他 sidecar / GUI 的 retrieval engine,但本仓库自身不以 GUI 为目标 ··· 15 15 ## 当前实现真相 16 16 17 17 - 检索主链是 `message/session recall -> session heuristic rerank -> progressive read` 18 - - `status` 只返回执行上下文、source inventory、index 状态与 coverage 状态;它可以扫描 raw session metadata,但不写 index、不回答内容问题 18 + - `status` 只返回执行上下文、source inventory、index 状态与 coverage 状态;`status --selector` 只读地报告目标 selector 的 coverage/freshness 和 recommendedAction;它可以扫描 raw session metadata,但不写 index、不回答内容问题 19 19 - 内容回答只能来自 cxs index;source inventory 只能用于构造 selector 和判断可能的同步范围,不能作为内容真相源 20 20 - `sync --selector` 是建立 coverage 的唯一入口;只读命令不得隐式触发 sync 21 + - 查找前不要求无条件 sync;只有目标 selector coverage 缺失或 stale 时才同步。fresh `all(root)` coverage 可以覆盖同 root 下更窄 selector 22 + - `find` 默认按 relevance 排序;“最新/最近 + 关键词”应使用 `find <query> --sort ended`,必要时 `--exclude-session` 排除当前会话/self-hit 21 23 - 候选召回来自 `messages_fts` 与 `sessions_fts(title + summary_text + compact_text + reasoning_summary_text)`;极少数零 token CJK query 在 message 侧回退到 LIKE 22 24 - `summary_text`、`compact_text`、`reasoning_summary_text` 已持久化,也会通过 `sessions_fts` 参与 session-level recall 23 25 - session-level FTS 使用显式字段权重:title 8.0、compact 4.0、summary 3.0、reasoning summary 1.2
+24 -4
README.md
··· 7 7 Core workflow: 8 8 9 9 ```text 10 - status -> sync --selector -> find/list -> read-range/read-page 10 + status -> ensure selector coverage -> find/list -> read-range/read-page 11 11 ``` 12 12 13 13 ## What It Is ··· 57 57 cxs status --json 58 58 ``` 59 59 60 - Build coverage for a project: 60 + Check coverage for a project: 61 + 62 + ```bash 63 + cxs status --selector '{"kind":"cwd","root":"/Users/you/.codex/sessions","cwd":"/Users/you/work/project"}' --json 64 + ``` 65 + 66 + If `requestedCoverage.recommendedAction` is `"sync"`, build or refresh coverage: 61 67 62 68 ```bash 63 69 cxs sync --selector '{"kind":"cwd","root":"/Users/you/.codex/sessions","cwd":"/Users/you/work/project"}' ··· 84 90 85 91 | Command | Purpose | 86 92 | --- | --- | 87 - | `cxs status` | Show execution context, source inventory, index state, and coverage. Does not write the index. | 93 + | `cxs status` | Show execution context, source inventory, index state, and coverage. `--selector` checks whether a target range is fresh. Does not write the index. | 88 94 | `cxs sync --selector <json>` | Scan selected Codex sessions and update the SQLite index. This is the only write command. | 89 - | `cxs find <query>` | Search indexed sessions and return ranked session candidates with minimal snippets. | 95 + | `cxs find <query>` | Search indexed sessions and return ranked session candidates with minimal snippets. Use `--sort ended` for "latest + keyword" queries. | 90 96 | `cxs read-range <sessionUuid>` | Read a small message window around a matched sequence or in-session query. | 91 97 | `cxs read-page <sessionUuid>` | Read a session page by offset and limit. | 92 98 | `cxs list` | List indexed sessions without full-text search. | ··· 113 119 cxs list --selector '{"kind":"cwd","root":"/Users/you/.codex/sessions","cwd":"/Users/you/work/project"}' --sort ended -n 10 114 120 ``` 115 121 122 + Example latest keyword query, excluding the current self-hit: 123 + 124 + ```bash 125 + cxs find "xsearch" --selector '{"kind":"cwd","root":"/Users/you/.codex/sessions","cwd":"/Users/you/work/project"}' --sort ended --exclude-session <current_session_uuid> -n 5 --json 126 + ``` 127 + 128 + `find` defaults to relevance sorting. Do not treat default `find` order as time 129 + order. 130 + 116 131 ## Sync And Storage 117 132 118 133 By default, `cxs` reads Codex sessions from `~/.codex/sessions` and stores its ··· 135 150 before complete coverage is written. 136 151 Pass `--best-effort` only when you explicitly want successful files written 137 152 despite failures; best-effort sync does not record complete coverage. 153 + 154 + `sync` is not required before every query. Use `status --selector` to check 155 + coverage first. A fresh `{"kind":"all", ...}` coverage record covers narrower 156 + selectors under the same root; a high `stats.sessionCount` only means rows exist 157 + and is not itself a freshness proof. 138 158 139 159 Indexes created before `cxs-v6-selector-provenance` should be refreshed with 140 160 `sync --selector` so date selectors and read coverage use the current
+3
docs/INDEX_COVERAGE_DESIGN.md
··· 162 162 - 返回 source inventory 163 163 - 返回 index 状态 164 164 - 返回 coverage 状态 165 + - 当传入 `--selector` 时,返回该 selector 的 `requestedCoverage`、freshness 与 recommendedAction 165 166 166 167 约束: 167 168 ··· 171 172 - 不执行 sync 172 173 - 可扫描 raw sessions 的 metadata 173 174 - 不读取 raw message content 175 + - `status --selector` 是 coverage check,不是隐式 sync 174 176 175 177 ### sync 176 178 ··· 200 202 - 从 index 中召回相关 session 201 203 - 返回可继续读取的锚点或 session-level 命中 202 204 - 返回当前 index coverage 摘要 205 + - 默认按 relevance 排序;显式 `--sort ended|started` 才表示时间排序 203 206 204 207 约束: 205 208
+2 -2
package-lock.json
··· 1 1 { 2 2 "name": "@act0r/cxs", 3 - "version": "0.3.0", 3 + "version": "0.3.1", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 8 "name": "@act0r/cxs", 9 - "version": "0.3.0", 9 + "version": "0.3.1", 10 10 "license": "MIT", 11 11 "os": [ 12 12 "darwin",
+1 -1
package.json
··· 1 1 { 2 2 "name": "@act0r/cxs", 3 - "version": "0.3.0", 3 + "version": "0.3.1", 4 4 "type": "module", 5 5 "description": "Progressive search CLI for local Codex session logs", 6 6 "license": "MIT",
+11 -5
skill-packages/cxs/SKILL.md
··· 36 36 | 场景 | 起手 | 原因 | 37 37 | --- | --- | --- | 38 38 | 用户问"之前 / 上次 / 我记得 / 我们讨论过" | `cxs status --json` | 先拿 source inventory 和 coverage | 39 - | 用户问"本项目最近的对话" | 构造 `{"kind":"cwd",...}` selector 后 `sync` / `list` | 内容只从 cxs index 出来 | 39 + | 用户问"本项目最近的对话" | 构造 `{"kind":"cwd",...}` selector 后先查 coverage,再 `list --sort ended` | 内容只从 cxs index 出来 | 40 + | 用户问"最新/最近 + 关键词" | 先确保 selector coverage,再 `find <query> --sort ended` | `find` 默认是相关性排序,不是时间排序 | 40 41 | 用户给项目名 / cwd / 时间窗 | 显式构造 selector | cwd/date selector 比全文搜更稳 | 41 42 | 已锁定某 session,需要局部上下文 | `cxs read-range --seq` 或 `--query` | 局部扩窗,不冷启 `read-page` | 42 43 ··· 50 51 51 52 ## 工作流心法 52 53 53 - - **status → sync selector → find/list → read-range → read-page**:先确定覆盖边界,再回答内容问题 54 + - **status → ensure coverage → find/list → read-range → read-page**:先确定覆盖边界,再回答内容问题 55 + - `sync` 只是写入/更新 SQLite index 和 coverage;查找本身不需要 sync。只有目标 selector 的 coverage 缺失或 stale 时才 `sync --selector` 56 + - 用 `status --selector '<json>' --json` 检查目标范围;`requestedCoverage.recommendedAction === "query"` 时直接查,`"sync"` 时才同步 57 + - `stats.sessionCount` 很多不等于目标范围有 v6 complete coverage;fresh `{"kind":"all",...}` coverage 可以覆盖 cwd/date 子 selector 58 + - "最新/最近 + 关键词"不要直接把默认 `find` 结果当最新;用 `find <query> --selector ... --sort ended`,必要时 `--exclude-session <current_uuid>` 排除当前会话/self-hit 54 59 - `matchSource = "session"` 时 `matchSeq = null`;这种命中先 `read-page` 抽样,**不要伪造 `read-range --seq`** 55 - - 用户给 cwd 但不确定 sync 状态 → `status --json`;根据 source inventory 构造 cwd selector;再 `sync --selector` 60 + - 用户给 cwd 但不确定 sync 状态 → `status --json`;根据 source inventory 构造 cwd selector;再 `status --selector`;缺失/stale 才 `sync --selector` 56 61 - `cwd` 只是候选过滤,不是主题真相;还要再看 `title`、`summaryText`、开头几条 message 57 62 - 同主题可能多个 uuid;按 `cwd / startedAt / matchCount` 选,不要按 title 脑补去重 58 63 59 64 ## 前置 60 65 61 66 - 先 `status --json` 看 `context / sourceInventory / coverage` 62 - - 索引不存在、读命令返回 `index_unavailable`、或 coverage 不覆盖目标范围 → `sync --selector '<json>'` 67 + - 先用 `status --selector '<json>' --json` 看目标 selector 的 `requestedCoverage` 68 + - 索引不存在、读命令返回 `index_unavailable`、或 `requestedCoverage.recommendedAction === "sync"` → `sync --selector '<json>'` 63 69 - `sync` 默认严格模式;只有用户接受部分成功才加 `--best-effort`;best-effort 不写 complete coverage 64 70 - 从别的 cwd 调用时,若默认 db 不对,显式传 `--db` 65 71 ··· 73 79 - [`references/failure-cookbook.md`](references/failure-cookbook.md) — 错误症状速查 / `--json` error shape 速查 74 80 - [`references/advanced-queries.md`](references/advanced-queries.md) — query 语义 / CJK 行为 / snippet 高亮 75 81 76 - # skill-sync: distributable cxs skill package, selector-status workflow, 2026-04-28 82 + # skill-sync: distributable cxs skill package, coverage-first recent-query workflow, 2026-04-30
+18 -1
skill-packages/cxs/references/cli-surface.md
··· 12 12 export CXS_BIN=/absolute/path/to/bin/cxs 13 13 ``` 14 14 15 - 没有单独的 `init` 命令。首次安装后先跑 `status --json`,根据返回的 `context.root`、`sourceInventory.cwdGroups` 和问题范围构造 selector,再跑 `sync --selector '<json>'`。 15 + 没有单独的 `init` 命令。首次安装后先跑 `status --json`,根据返回的 `context.root`、`sourceInventory.cwdGroups` 和问题范围构造 selector;再用 `status --selector '<json>' --json` 检查 coverage。只有 `requestedCoverage.recommendedAction === "sync"` 时才跑 `sync --selector '<json>'`。 16 16 17 17 缺少 cxs 索引时,`find` / `read-range` / `read-page` / `list` / `stats --json` 返回: 18 18 ··· 28 28 29 29 ```bash 30 30 "${CXS_BIN:-cxs}" status --json 31 + "${CXS_BIN:-cxs}" status --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/Users/me/work/foo"}' --json 31 32 ``` 33 + 34 + `status --selector` 是只读 coverage check。看 `requestedCoverage`: 35 + 36 + - `recommendedAction: "query"`: 目标范围已有 fresh complete coverage,可直接 `find/list` 37 + - `recommendedAction: "sync"`: coverage 缺失或 stale,先 `sync --selector` 38 + - fresh `{"kind":"all",...}` coverage 可以覆盖 cwd/date 子 selector;`stats.sessionCount` 只是 rows 数,不等于 coverage 完整证明 32 39 33 40 Selector shapes: 34 41 ··· 70 77 ```bash 71 78 "${CXS_BIN:-cxs}" find "cf tunnel" --json -n 5 72 79 "${CXS_BIN:-cxs}" find "cf tunnel" --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/Users/me/work/foo"}' --json -n 5 80 + "${CXS_BIN:-cxs}" find "xsearch" --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/Users/me/work/foo"}' --sort ended --exclude-session <current_uuid> --json -n 5 73 81 ``` 82 + 83 + Options: 84 + 85 + | option | 说明 | 86 + | --- | --- | 87 + | `--selector <json>` | 结构化查询范围 | 88 + | `--sort relevance|ended|started` | 默认 `relevance`;问"最新/最近 + 关键词"时用 `ended` | 89 + | `--exclude-session <uuid>` | 排除指定 session;可重复。用于排除当前会话/self-hit | 90 + | `-n, --limit <n>` | 返回条数 | 74 91 75 92 ## read-range 76 93
+23 -3
skill-packages/cxs/references/failure-cookbook.md
··· 4 4 5 5 | 症状 | 先跑 | 处理 | 6 6 | --- | --- | --- | 7 - | `find` 零结果但用户坚持存在 | `status --json` | 看目标 selector 是否有 coverage;必要时 `sync --selector`;再带 selector 查询 | 7 + | `find` 零结果但用户坚持存在 | `status --selector '<json>' --json` | 看目标 selector 的 `requestedCoverage`;必要时 `sync --selector`;再带 selector 查询 | 8 8 | `sync` 非零退出带 per-file errors | `sync --selector '<json>' --json 2>&1` | 看 `errorDetails[]`;默认严格模式;只在允许部分成功时加 `--best-effort` | 9 9 | `sync` 返回 `selector_required` | 原命令补 `--selector` | selector 必须显式,不存在默认范围 | 10 10 | `find/list/stats/read-*` 输出 `index_unavailable` | `status --json` | 索引还没建立;选择 selector 后 `sync --selector` | 11 11 | `stats/list/find` 报 `database is locked` | 原命令重试一次 | 多半是 SQLite 忙;仍失败就先跳过 `stats` 直接读 | 12 12 | 同一主题多条 uuid | `find -n 10 --json` | 按 `startedAt`、`cwd`、`matchCount` 选 | 13 + | 最新/最近 + 关键词被当前会话抢结果 | `find <query> --sort ended --exclude-session <uuid>` | 默认 `find` 是 relevance 排序;时间问题显式用 `--sort ended` 并排除 self-hit | 13 14 | 中文/CJK 零结果 | 无 | 换至少两字中文、英文关键词,或先用 selector 缩范围 | 14 - | 用户问“最近本项目讨论了什么” | `status --json` | 用当前 repo cwd 构造 selector,同步后 `list --selector` | 15 + | 用户问“最近本项目讨论了什么” | `status --selector '<cwd selector>' --json` | coverage fresh 直接 `list --selector`;缺失/stale 才同步 | 15 16 | 用户说“在 X 项目里” | `status --json` | 从 `sourceInventory.cwdGroups` 选择 cwd selector | 16 17 | 从其他 cwd 调用找不到 db | `stats --json` | 看 `dbPath`;必要时显式传 `--db` | 17 18 ··· 21 22 "${CXS_BIN:-cxs}" status --json 22 23 ``` 23 24 24 - 如果目标范围没有 coverage,先同步明确 selector: 25 + 如果目标范围没有 fresh coverage,先同步明确 selector: 25 26 26 27 ```bash 28 + "${CXS_BIN:-cxs}" status --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/Users/me/work/foo"}' --json 27 29 "${CXS_BIN:-cxs}" sync --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/Users/me/work/foo"}' 28 30 ``` 31 + 32 + 如果 `status --selector` 返回 `recommendedAction: "query"`,跳过 `sync`。 29 33 30 34 然后查询时继续带同一个 selector。 31 35 ··· 77 81 78 82 ```bash 79 83 "${CXS_BIN:-cxs}" status --json 84 + "${CXS_BIN:-cxs}" status --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --json 80 85 "${CXS_BIN:-cxs}" sync --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --json 81 86 "${CXS_BIN:-cxs}" list --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --sort ended -n 8 --json 82 87 ``` 88 + 89 + 如果 `status --selector` 返回 `recommendedAction: "query"`,跳过 `sync`。 83 90 84 91 然后至少再看: 85 92 ··· 88 95 - `read-page` 开头 6 到 8 条 89 96 - `read-page` 结尾 6 到 8 条 90 97 98 + ## Recent keyword query 99 + 100 + 用户问“最新一次 X / 最近哪个 session 提到 X”时: 101 + 102 + ```bash 103 + "${CXS_BIN:-cxs}" status --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --json 104 + # recommendedAction 为 "sync" 时才同步 105 + "${CXS_BIN:-cxs}" find "X" --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --sort ended --exclude-session <current_session_uuid> --json -n 5 106 + ``` 107 + 108 + 不要直接用默认 `find "X"` 下“最新”结论;默认排序是 relevance。 109 + 91 110 ## --json error shape 速查 92 111 93 112 不同子命令在 `--json` 下的 error 形状不一致,解析时按命令分流: ··· 98 117 | `sync` invalid selector | stdout | `{ "error": { "code": "invalid_selector", "message": "..." } }` | 99 118 | `sync` per-file 错 | stderr | `SyncSummary`,看 `errors / errorDetails[]` | 100 119 | `sync` 锁超时 | stderr | `{ "error": <message string> }` | 120 + | `status` invalid selector | stdout | `{ "error": { "code": "invalid_selector", "message": "..." } }` | 101 121 | `find / read-range / read-page / list / stats` 索引不存在 | stdout | `{ "error": { "code": "index_unavailable", "message": "...", "dbPath": "...", "hint": "..." } }` | 102 122 | `find / read-range / read-page / list / stats` 其他异常 | 进程异常退出 | 直接非零退出 | 103 123
+18 -1
skill-packages/cxs/references/json-schema.md
··· 7 7 ```ts 8 8 { 9 9 query: string; 10 + sort: "relevance" | "ended" | "started"; 11 + excludedSessions: string[]; 10 12 results: FindResult[]; 11 13 coverage: CoverageStatus; 12 14 } ··· 115 117 dbSizeBytes: number; 116 118 lastSyncAt: string | null; 117 119 }; 118 - coverage: CoverageRecord[]; 120 + coverage: CoverageInventoryStatus[]; 121 + requestedCoverage?: RequestedCoverageStatus; 119 122 } 120 123 ``` 121 124 ··· 197 200 complete: boolean; 198 201 freshness: "not_checked"; 199 202 coveringSelectors: CoverageRecord[]; 203 + } 204 + ``` 205 + 206 + `RequestedCoverageStatus`: 207 + 208 + ```ts 209 + { 210 + requested: Selector; 211 + complete: boolean; 212 + freshness: "fresh" | "stale" | "missing"; 213 + sourceFingerprint: string; 214 + sourceFileCount: number; 215 + coveringSelectors: CoverageInventoryStatus[]; 216 + recommendedAction: "query" | "sync"; 200 217 } 201 218 ``` 202 219
+37 -5
skill-packages/cxs/references/progressive-workflow.md
··· 3 3 ## 默认流程 4 4 5 5 1. `status --json` 拿 source inventory 和 coverage 6 - 2. 选择明确 selector 并 `sync --selector` 7 - 3. `find` 或 `list` 拿候选 session 和命中锚点 8 - 4. `read-range` 在最佳候选周围扩局部上下文 9 - 5. `read-page` 只在局部窗口仍不够时翻整页 6 + 2. 选择明确 selector,用 `status --selector '<json>' --json` 检查 `requestedCoverage` 7 + 3. 如果 `recommendedAction` 是 `"sync"` 才 `sync --selector`;如果是 `"query"` 直接查 8 + 4. `find` 或 `list` 拿候选 session 和命中锚点 9 + 5. `read-range` 在最佳候选周围扩局部上下文 10 + 6. `read-page` 只在局部窗口仍不够时翻整页 10 11 11 12 硬规则: 12 13 13 14 - 没有 `sessionUuid` 时,不要冷启动 `read-page` 14 - - 用户给了 `cwd` 或时间窗口:构造同范围 selector;先同步这个 selector;查询时继续带同一个 selector 15 + - `sync` 只负责更新 index/coverage;查找不需要每次 sync 16 + - 用户给了 `cwd` 或时间窗口:构造同范围 selector;先确认 coverage;缺失/stale 才同步;查询时继续带同一个 selector 17 + - 用户问"最新/最近 + 关键词":不要用默认 `find` 当时间结论;用 `find <query> --sort ended`,并用 `--exclude-session <current_uuid>` 排除当前会话/self-hit 15 18 - 已锁定 session 但锚点不对时,用 `read-range --query` 16 19 - `cwd` 只是候选过滤,不是主题真相;还要再看 `title`、`summaryText` 和开头几条 message 17 20 ··· 21 24 22 25 ```bash 23 26 "${CXS_BIN:-cxs}" status --json 27 + "${CXS_BIN:-cxs}" status --selector '{"kind":"all","root":"/Users/me/.codex/sessions"}' --json 24 28 "${CXS_BIN:-cxs}" sync --selector '{"kind":"all","root":"/Users/me/.codex/sessions"}' --json 25 29 "${CXS_BIN:-cxs}" find "cf tunnel" --json -n 5 26 30 ``` 31 + 32 + 如果 `status --selector` 返回 `recommendedAction: "query"`,跳过 `sync`。 27 33 28 34 然后: 29 35 ··· 45 51 46 52 ```bash 47 53 "${CXS_BIN:-cxs}" status --json 54 + "${CXS_BIN:-cxs}" status --selector '{"kind":"cwd_date_range","root":"/Users/me/.codex/sessions","cwd":"/Users/me/work/hammerspoon","fromDate":"2026-04-15","toDate":"2026-04-30"}' --json 48 55 "${CXS_BIN:-cxs}" sync --selector '{"kind":"cwd_date_range","root":"/Users/me/.codex/sessions","cwd":"/Users/me/work/hammerspoon","fromDate":"2026-04-15","toDate":"2026-04-30"}' --json 49 56 "${CXS_BIN:-cxs}" list --selector '{"kind":"cwd_date_range","root":"/Users/me/.codex/sessions","cwd":"/Users/me/work/hammerspoon","fromDate":"2026-04-15","toDate":"2026-04-30"}' --json 50 57 ``` 58 + 59 + 如果 `status --selector` 返回 `recommendedAction: "query"`,跳过 `sync`。 51 60 52 61 再在候选 session 内局部重定位: 53 62 ··· 63 72 64 73 ```bash 65 74 "${CXS_BIN:-cxs}" status --json 75 + "${CXS_BIN:-cxs}" status --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --json 66 76 "${CXS_BIN:-cxs}" sync --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --json 67 77 "${CXS_BIN:-cxs}" list --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --sort ended -n 8 --json 68 78 ``` 69 79 80 + 如果 `status --selector` 返回 `recommendedAction: "query"`,跳过 `sync`。 81 + 70 82 不要把 `cwd` 直接当主题真相。至少再看: 71 83 72 84 - `title` 73 85 - `summaryText` 74 86 - 开头几条 message 75 87 - 结尾几条 message 88 + 89 + ## Worked Scenario 4 90 + 91 + 用户说:`最新的一次 xsearch 是哪个 session` 92 + 93 + 这类是"最近 + 关键词",先按时间语义查,不要把默认相关性排序当最新: 94 + 95 + ```bash 96 + "${CXS_BIN:-cxs}" status --json 97 + "${CXS_BIN:-cxs}" status --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --json 98 + # 如果 recommendedAction 是 "sync": 99 + "${CXS_BIN:-cxs}" sync --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --json 100 + "${CXS_BIN:-cxs}" find "xsearch" --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --sort ended --exclude-session <current_session_uuid> --json -n 5 101 + ``` 102 + 103 + 如果不知道当前 session uuid,先从最近列表识别当前 self-hit,再把它传给 `--exclude-session`: 104 + 105 + ```bash 106 + "${CXS_BIN:-cxs}" list --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --sort ended -n 5 --json 107 + ``` 76 108 77 109 ## 来源 78 110
+11 -5
skill-packages/cxsd/SKILL.md
··· 28 28 | --- | --- | --- | 29 29 | 验证当前 checkout 的 cxs 行为 | `"${CXSD_BIN:-cxsd}" status --json` | 用本地源码,不碰发布版 | 30 30 | 用户问"之前 / 上次 / 我记得 / 我们讨论过"且要试 dev 版 | `"${CXSD_BIN:-cxsd}" status --json` | 先拿 source inventory 和 coverage | 31 - | 用户问"本项目最近的对话" | 构造 `{"kind":"cwd",...}` selector 后用 `"${CXSD_BIN:-cxsd}" sync` / `"${CXSD_BIN:-cxsd}" list` | 内容只从 cxs index 出来 | 31 + | 用户问"本项目最近的对话" | 构造 `{"kind":"cwd",...}` selector 后先查 coverage,再 `"${CXSD_BIN:-cxsd}" list --sort ended` | 内容只从 cxs index 出来 | 32 + | 用户问"最新/最近 + 关键词" | 先确保 selector coverage,再 `"${CXSD_BIN:-cxsd}" find <query> --sort ended` | `find` 默认是相关性排序,不是时间排序 | 32 33 | 用户给项目名 / cwd / 时间窗 | 显式构造 selector | cwd/date selector 比全文搜更稳 | 33 34 | 已锁定某 session,需要局部上下文 | `"${CXSD_BIN:-cxsd}" read-range --seq` 或 `--query` | 局部扩窗,不冷启 `read-page` | 34 35 ··· 43 44 44 45 ## 工作流心法 45 46 46 - - **status → sync selector → find/list → read-range → read-page**:先确定覆盖边界,再回答内容问题 47 + - **status → ensure coverage → find/list → read-range → read-page**:先确定覆盖边界,再回答内容问题 48 + - `sync` 只是写入/更新 SQLite index 和 coverage;查找本身不需要 sync。只有目标 selector 的 coverage 缺失或 stale 时才 `"${CXSD_BIN:-cxsd}" sync --selector` 49 + - 用 `"${CXSD_BIN:-cxsd}" status --selector '<json>' --json` 检查目标范围;`requestedCoverage.recommendedAction === "query"` 时直接查,`"sync"` 时才同步 50 + - `stats.sessionCount` 很多不等于目标范围有 v6 complete coverage;fresh `{"kind":"all",...}` coverage 可以覆盖 cwd/date 子 selector 51 + - "最新/最近 + 关键词"不要直接把默认 `find` 结果当最新;用 `"${CXSD_BIN:-cxsd}" find <query> --selector ... --sort ended`,必要时 `--exclude-session <current_uuid>` 排除当前会话/self-hit 47 52 - `matchSource = "session"` 时 `matchSeq = null`;这种命中先 `read-page` 抽样,**不要伪造 `read-range --seq`** 48 - - 用户给 cwd 但不确定 sync 状态 → `"${CXSD_BIN:-cxsd}" status --json`;根据 source inventory 构造 cwd selector;再 `"${CXSD_BIN:-cxsd}" sync --selector` 53 + - 用户给 cwd 但不确定 sync 状态 → `"${CXSD_BIN:-cxsd}" status --json`;根据 source inventory 构造 cwd selector;再 `"${CXSD_BIN:-cxsd}" status --selector`;缺失/stale 才 `"${CXSD_BIN:-cxsd}" sync --selector` 49 54 - `cwd` 只是候选过滤,不是主题真相;还要再看 `title`、`summaryText`、开头几条 message 50 55 - 同主题可能多个 uuid;按 `cwd / startedAt / matchCount` 选,不要按 title 脑补去重 51 56 52 57 ## 前置 53 58 54 59 - 先 `status --json` 看 `context / sourceInventory / coverage` 55 - - 索引不存在、读命令返回 `index_unavailable`、或 coverage 不覆盖目标范围 → `"${CXSD_BIN:-cxsd}" sync --selector '<json>'` 60 + - 先用 `"${CXSD_BIN:-cxsd}" status --selector '<json>' --json` 看目标 selector 的 `requestedCoverage` 61 + - 索引不存在、读命令返回 `index_unavailable`、或 `requestedCoverage.recommendedAction === "sync"` → `"${CXSD_BIN:-cxsd}" sync --selector '<json>'` 56 62 - `sync` 默认严格模式;只有用户接受部分成功才加 `--best-effort`;best-effort 不写 complete coverage 57 63 - 从别的 cwd 调用时,若默认 db 不对,显式传 `--db` 58 64 ··· 66 72 - [`references/failure-cookbook.md`](references/failure-cookbook.md) — 错误症状速查 / `--json` error shape 速查 67 73 - [`references/advanced-queries.md`](references/advanced-queries.md) — query 语义 / CJK 行为 / snippet 高亮 68 74 69 - # skill-sync: local cxsd dev skill, selector-status workflow, 2026-04-28 75 + # skill-sync: local cxsd dev skill, coverage-first recent-query workflow, 2026-04-30
+18 -1
skill-packages/cxsd/references/cli-surface.md
··· 12 12 export CXSD_BIN=/absolute/path/to/bin/cxsd 13 13 ``` 14 14 15 - 没有单独的 `init` 命令。首次安装后先跑 `status --json`,根据返回的 `context.root`、`sourceInventory.cwdGroups` 和问题范围构造 selector,再跑 `sync --selector '<json>'`。 15 + 没有单独的 `init` 命令。首次安装后先跑 `status --json`,根据返回的 `context.root`、`sourceInventory.cwdGroups` 和问题范围构造 selector;再用 `status --selector '<json>' --json` 检查 coverage。只有 `requestedCoverage.recommendedAction === "sync"` 时才跑 `sync --selector '<json>'`。 16 16 17 17 缺少 cxs 索引时,`find` / `read-range` / `read-page` / `list` / `stats --json` 返回: 18 18 ··· 28 28 29 29 ```bash 30 30 "${CXSD_BIN:-cxsd}" status --json 31 + "${CXSD_BIN:-cxsd}" status --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/Users/me/work/foo"}' --json 31 32 ``` 33 + 34 + `status --selector` 是只读 coverage check。看 `requestedCoverage`: 35 + 36 + - `recommendedAction: "query"`: 目标范围已有 fresh complete coverage,可直接 `find/list` 37 + - `recommendedAction: "sync"`: coverage 缺失或 stale,先 `sync --selector` 38 + - fresh `{"kind":"all",...}` coverage 可以覆盖 cwd/date 子 selector;`stats.sessionCount` 只是 rows 数,不等于 coverage 完整证明 32 39 33 40 Selector shapes: 34 41 ··· 70 77 ```bash 71 78 "${CXSD_BIN:-cxsd}" find "cf tunnel" --json -n 5 72 79 "${CXSD_BIN:-cxsd}" find "cf tunnel" --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/Users/me/work/foo"}' --json -n 5 80 + "${CXSD_BIN:-cxsd}" find "xsearch" --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/Users/me/work/foo"}' --sort ended --exclude-session <current_uuid> --json -n 5 73 81 ``` 82 + 83 + Options: 84 + 85 + | option | 说明 | 86 + | --- | --- | 87 + | `--selector <json>` | 结构化查询范围 | 88 + | `--sort relevance|ended|started` | 默认 `relevance`;问"最新/最近 + 关键词"时用 `ended` | 89 + | `--exclude-session <uuid>` | 排除指定 session;可重复。用于排除当前会话/self-hit | 90 + | `-n, --limit <n>` | 返回条数 | 74 91 75 92 ## read-range 76 93
+24 -4
skill-packages/cxsd/references/failure-cookbook.md
··· 4 4 5 5 | 症状 | 先跑 | 处理 | 6 6 | --- | --- | --- | 7 - | `find` 零结果但用户坚持存在 | `status --json` | 看目标 selector 是否有 coverage;必要时 `sync --selector`;再带 selector 查询 | 7 + | `find` 零结果但用户坚持存在 | `status --selector '<json>' --json` | 看目标 selector 的 `requestedCoverage`;必要时 `sync --selector`;再带 selector 查询 | 8 8 | `sync` 非零退出带 per-file errors | `sync --selector '<json>' --json 2>&1` | 看 `errorDetails[]`;默认严格模式;只在允许部分成功时加 `--best-effort` | 9 9 | `sync` 返回 `selector_required` | 原命令补 `--selector` | selector 必须显式,不存在默认范围 | 10 10 | `find/list/stats/read-*` 输出 `index_unavailable` | `status --json` | 索引还没建立;选择 selector 后 `sync --selector` | 11 11 | `stats/list/find` 报 `database is locked` | 原命令重试一次 | 多半是 SQLite 忙;仍失败就先跳过 `stats` 直接读 | 12 12 | 同一主题多条 uuid | `find -n 10 --json` | 按 `startedAt`、`cwd`、`matchCount` 选 | 13 + | 最新/最近 + 关键词被当前会话抢结果 | `find <query> --sort ended --exclude-session <uuid>` | 默认 `find` 是 relevance 排序;时间问题显式用 `--sort ended` 并排除 self-hit | 13 14 | 中文/CJK 零结果 | 无 | 换至少两字中文、英文关键词,或先用 selector 缩范围 | 14 - | 用户问“最近本项目讨论了什么” | `status --json` | 用当前 repo cwd 构造 selector,同步后 `list --selector` | 15 + | 用户问“最近本项目讨论了什么” | `status --selector '<cwd selector>' --json` | coverage fresh 直接 `list --selector`;缺失/stale 才同步 | 15 16 | 用户说“在 X 项目里” | `status --json` | 从 `sourceInventory.cwdGroups` 选择 cwd selector | 16 17 | 从其他 cwd 调用找不到 db | `stats --json` | 看 `dbPath`;必要时显式传 `--db` | 17 18 ··· 21 22 "${CXSD_BIN:-cxsd}" status --json 22 23 ``` 23 24 24 - 如果目标范围没有 coverage,先同步明确 selector: 25 + 如果目标范围没有 fresh coverage,先同步明确 selector: 25 26 26 27 ```bash 28 + "${CXSD_BIN:-cxsd}" status --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/Users/me/work/foo"}' --json 27 29 "${CXSD_BIN:-cxsd}" sync --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/Users/me/work/foo"}' 28 30 ``` 31 + 32 + 如果 `status --selector` 返回 `recommendedAction: "query"`,跳过 `sync`。 29 33 30 34 然后查询时继续带同一个 selector。 31 35 ··· 51 55 "code": "index_unavailable", 52 56 "message": "index not found: ...", 53 57 "dbPath": "...", 54 - "hint": "Run `cxsd sync` first ..." 58 + "hint": "Run `cxs sync` first ..." 55 59 } 56 60 } 57 61 ``` ··· 77 81 78 82 ```bash 79 83 "${CXSD_BIN:-cxsd}" status --json 84 + "${CXSD_BIN:-cxsd}" status --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --json 80 85 "${CXSD_BIN:-cxsd}" sync --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --json 81 86 "${CXSD_BIN:-cxsd}" list --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --sort ended -n 8 --json 82 87 ``` 83 88 89 + 如果 `status --selector` 返回 `recommendedAction: "query"`,跳过 `sync`。 90 + 84 91 然后至少再看: 85 92 86 93 - `title` ··· 88 95 - `read-page` 开头 6 到 8 条 89 96 - `read-page` 结尾 6 到 8 条 90 97 98 + ## Recent keyword query 99 + 100 + 用户问“最新一次 X / 最近哪个 session 提到 X”时: 101 + 102 + ```bash 103 + "${CXSD_BIN:-cxsd}" status --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --json 104 + # recommendedAction 为 "sync" 时才同步 105 + "${CXSD_BIN:-cxsd}" find "X" --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --sort ended --exclude-session <current_session_uuid> --json -n 5 106 + ``` 107 + 108 + 不要直接用默认 `find "X"` 下“最新”结论;默认排序是 relevance。 109 + 91 110 ## --json error shape 速查 92 111 93 112 不同子命令在 `--json` 下的 error 形状不一致,解析时按命令分流: ··· 98 117 | `sync` invalid selector | stdout | `{ "error": { "code": "invalid_selector", "message": "..." } }` | 99 118 | `sync` per-file 错 | stderr | `SyncSummary`,看 `errors / errorDetails[]` | 100 119 | `sync` 锁超时 | stderr | `{ "error": <message string> }` | 120 + | `status` invalid selector | stdout | `{ "error": { "code": "invalid_selector", "message": "..." } }` | 101 121 | `find / read-range / read-page / list / stats` 索引不存在 | stdout | `{ "error": { "code": "index_unavailable", "message": "...", "dbPath": "...", "hint": "..." } }` | 102 122 | `find / read-range / read-page / list / stats` 其他异常 | 进程异常退出 | 直接非零退出 | 103 123
+19 -2
skill-packages/cxsd/references/json-schema.md
··· 1 - # cxs JSON Schema 1 + # cxsd JSON Schema 2 2 3 3 ## find 4 4 ··· 7 7 ```ts 8 8 { 9 9 query: string; 10 + sort: "relevance" | "ended" | "started"; 11 + excludedSessions: string[]; 10 12 results: FindResult[]; 11 13 coverage: CoverageStatus; 12 14 } ··· 115 117 dbSizeBytes: number; 116 118 lastSyncAt: string | null; 117 119 }; 118 - coverage: CoverageRecord[]; 120 + coverage: CoverageInventoryStatus[]; 121 + requestedCoverage?: RequestedCoverageStatus; 119 122 } 120 123 ``` 121 124 ··· 197 200 complete: boolean; 198 201 freshness: "not_checked"; 199 202 coveringSelectors: CoverageRecord[]; 203 + } 204 + ``` 205 + 206 + `RequestedCoverageStatus`: 207 + 208 + ```ts 209 + { 210 + requested: Selector; 211 + complete: boolean; 212 + freshness: "fresh" | "stale" | "missing"; 213 + sourceFingerprint: string; 214 + sourceFileCount: number; 215 + coveringSelectors: CoverageInventoryStatus[]; 216 + recommendedAction: "query" | "sync"; 200 217 } 201 218 ``` 202 219
+37 -5
skill-packages/cxsd/references/progressive-workflow.md
··· 3 3 ## 默认流程 4 4 5 5 1. `status --json` 拿 source inventory 和 coverage 6 - 2. 选择明确 selector 并 `sync --selector` 7 - 3. `find` 或 `list` 拿候选 session 和命中锚点 8 - 4. `read-range` 在最佳候选周围扩局部上下文 9 - 5. `read-page` 只在局部窗口仍不够时翻整页 6 + 2. 选择明确 selector,用 `status --selector '<json>' --json` 检查 `requestedCoverage` 7 + 3. 如果 `recommendedAction` 是 `"sync"` 才 `sync --selector`;如果是 `"query"` 直接查 8 + 4. `find` 或 `list` 拿候选 session 和命中锚点 9 + 5. `read-range` 在最佳候选周围扩局部上下文 10 + 6. `read-page` 只在局部窗口仍不够时翻整页 10 11 11 12 硬规则: 12 13 13 14 - 没有 `sessionUuid` 时,不要冷启动 `read-page` 14 - - 用户给了 `cwd` 或时间窗口:构造同范围 selector;先同步这个 selector;查询时继续带同一个 selector 15 + - `sync` 只负责更新 index/coverage;查找不需要每次 sync 16 + - 用户给了 `cwd` 或时间窗口:构造同范围 selector;先确认 coverage;缺失/stale 才同步;查询时继续带同一个 selector 17 + - 用户问"最新/最近 + 关键词":不要用默认 `find` 当时间结论;用 `find <query> --sort ended`,并用 `--exclude-session <current_uuid>` 排除当前会话/self-hit 15 18 - 已锁定 session 但锚点不对时,用 `read-range --query` 16 19 - `cwd` 只是候选过滤,不是主题真相;还要再看 `title`、`summaryText` 和开头几条 message 17 20 ··· 21 24 22 25 ```bash 23 26 "${CXSD_BIN:-cxsd}" status --json 27 + "${CXSD_BIN:-cxsd}" status --selector '{"kind":"all","root":"/Users/me/.codex/sessions"}' --json 24 28 "${CXSD_BIN:-cxsd}" sync --selector '{"kind":"all","root":"/Users/me/.codex/sessions"}' --json 25 29 "${CXSD_BIN:-cxsd}" find "cf tunnel" --json -n 5 26 30 ``` 31 + 32 + 如果 `status --selector` 返回 `recommendedAction: "query"`,跳过 `sync`。 27 33 28 34 然后: 29 35 ··· 45 51 46 52 ```bash 47 53 "${CXSD_BIN:-cxsd}" status --json 54 + "${CXSD_BIN:-cxsd}" status --selector '{"kind":"cwd_date_range","root":"/Users/me/.codex/sessions","cwd":"/Users/me/work/hammerspoon","fromDate":"2026-04-15","toDate":"2026-04-30"}' --json 48 55 "${CXSD_BIN:-cxsd}" sync --selector '{"kind":"cwd_date_range","root":"/Users/me/.codex/sessions","cwd":"/Users/me/work/hammerspoon","fromDate":"2026-04-15","toDate":"2026-04-30"}' --json 49 56 "${CXSD_BIN:-cxsd}" list --selector '{"kind":"cwd_date_range","root":"/Users/me/.codex/sessions","cwd":"/Users/me/work/hammerspoon","fromDate":"2026-04-15","toDate":"2026-04-30"}' --json 50 57 ``` 58 + 59 + 如果 `status --selector` 返回 `recommendedAction: "query"`,跳过 `sync`。 51 60 52 61 再在候选 session 内局部重定位: 53 62 ··· 63 72 64 73 ```bash 65 74 "${CXSD_BIN:-cxsd}" status --json 75 + "${CXSD_BIN:-cxsd}" status --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --json 66 76 "${CXSD_BIN:-cxsd}" sync --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --json 67 77 "${CXSD_BIN:-cxsd}" list --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --sort ended -n 8 --json 68 78 ``` 69 79 80 + 如果 `status --selector` 返回 `recommendedAction: "query"`,跳过 `sync`。 81 + 70 82 不要把 `cwd` 直接当主题真相。至少再看: 71 83 72 84 - `title` 73 85 - `summaryText` 74 86 - 开头几条 message 75 87 - 结尾几条 message 88 + 89 + ## Worked Scenario 4 90 + 91 + 用户说:`最新的一次 xsearch 是哪个 session` 92 + 93 + 这类是"最近 + 关键词",先按时间语义查,不要把默认相关性排序当最新: 94 + 95 + ```bash 96 + "${CXSD_BIN:-cxsd}" status --json 97 + "${CXSD_BIN:-cxsd}" status --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --json 98 + # 如果 recommendedAction 是 "sync": 99 + "${CXSD_BIN:-cxsd}" sync --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --json 100 + "${CXSD_BIN:-cxsd}" find "xsearch" --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --sort ended --exclude-session <current_session_uuid> --json -n 5 101 + ``` 102 + 103 + 如果不知道当前 session uuid,先从最近列表识别当前 self-hit,再把它传给 `--exclude-session`: 104 + 105 + ```bash 106 + "${CXSD_BIN:-cxsd}" list --selector '{"kind":"cwd","root":"/Users/me/.codex/sessions","cwd":"/absolute/path/to/current/repo"}' --sort ended -n 5 --json 107 + ``` 76 108 77 109 ## 来源 78 110
+116 -1
src/cli.test.ts
··· 77 77 ]); 78 78 }); 79 79 80 + test("status --selector reports requested coverage without writing index", async () => { 81 + const base = mkdtempSync(join(tmpdir(), "cxs-cli-status-selector-")); 82 + tempDirs.push(base); 83 + const root = join(base, "sessions"); 84 + const sessionsRoot = join(root, "2026", "04", "20"); 85 + mkdirSync(sessionsRoot, { recursive: true }); 86 + writeFileSync( 87 + join(sessionsRoot, "rollout-2026-04-20T10-00-00-10101010-1010-4010-8010-101010101010.jsonl"), 88 + [ 89 + line("session_meta", { id: "10101010-1010-4010-8010-101010101010", cwd: "/tmp/selector-alpha" }), 90 + line("event_msg", { type: "user_message", message: "selector alpha" }), 91 + ].join("\n"), 92 + ); 93 + 94 + const selector = JSON.stringify({ kind: "cwd", root, cwd: "/tmp/selector-alpha" }); 95 + const result = await runCli(["status", "--root", root, "--selector", selector, "--db", join(base, "missing.sqlite"), "--json"]); 96 + 97 + expect(result.exitCode).toBe(0); 98 + const payload = JSON.parse(result.stdout) as { 99 + index: { exists: boolean }; 100 + requestedCoverage: { complete: boolean; freshness: string; recommendedAction: string; sourceFileCount: number }; 101 + }; 102 + expect(payload.index.exists).toBe(false); 103 + expect(payload.requestedCoverage.complete).toBe(false); 104 + expect(payload.requestedCoverage.freshness).toBe("missing"); 105 + expect(payload.requestedCoverage.recommendedAction).toBe("sync"); 106 + expect(payload.requestedCoverage.sourceFileCount).toBe(1); 107 + }); 108 + 80 109 test("sync requires an explicit selector", async () => { 81 110 const base = mkdtempSync(join(tmpdir(), "cxs-cli-sync-selector-required-")); 82 111 tempDirs.push(base); ··· 164 193 expect(payload.coverage[0]?.freshness).toBe("stale"); 165 194 }); 166 195 196 + test("status --selector treats fresh all coverage as covering a cwd selector", async () => { 197 + const base = mkdtempSync(join(tmpdir(), "cxs-cli-coverage-all-covers-cwd-")); 198 + tempDirs.push(base); 199 + const root = join(base, "sessions"); 200 + const day = join(root, "2026", "04", "21"); 201 + mkdirSync(day, { recursive: true }); 202 + writeFileSync( 203 + join(day, "rollout-2026-04-21T10-00-00-13131313-1313-4313-8313-131313131313.jsonl"), 204 + [ 205 + line("session_meta", { id: "13131313-1313-4313-8313-131313131313", cwd: "/tmp/covered-by-all" }), 206 + line("event_msg", { type: "user_message", message: "covered by all" }), 207 + ].join("\n"), 208 + ); 209 + 210 + const dbPath = join(base, "index.sqlite"); 211 + await syncSessions({ dbPath, selector: { kind: "all", root } }); 212 + 213 + const selector = JSON.stringify({ kind: "cwd", root, cwd: "/tmp/covered-by-all" }); 214 + const status = await runCli(["status", "--root", root, "--selector", selector, "--db", dbPath, "--json"]); 215 + 216 + expect(status.exitCode).toBe(0); 217 + const payload = JSON.parse(status.stdout) as { 218 + requestedCoverage: { 219 + complete: boolean; 220 + freshness: string; 221 + recommendedAction: string; 222 + coveringSelectors: Array<{ selector: { kind: string } }>; 223 + }; 224 + }; 225 + expect(payload.requestedCoverage.complete).toBe(true); 226 + expect(payload.requestedCoverage.freshness).toBe("fresh"); 227 + expect(payload.requestedCoverage.recommendedAction).toBe("query"); 228 + expect(payload.requestedCoverage.coveringSelectors[0]?.selector.kind).toBe("all"); 229 + }); 230 + 167 231 test("find text output points to read-range", async () => { 168 232 const base = mkdtempSync(join(tmpdir(), "cxs-cli-")); 169 233 tempDirs.push(base); ··· 187 251 expect(result.exitCode).toBe(0); 188 252 expect(result.stdout).toContain("next: cxs read-range 44444444-4444-4444-8444-444444444444 --seq 0"); 189 253 expect(result.stdout).not.toContain("next: cxs window"); 254 + }); 255 + 256 + test("find can sort by recent time and exclude the current session", async () => { 257 + const base = mkdtempSync(join(tmpdir(), "cxs-cli-find-recent-")); 258 + tempDirs.push(base); 259 + const sessionsRoot = join(base, "sessions", "2026", "04", "21"); 260 + mkdirSync(sessionsRoot, { recursive: true }); 261 + 262 + writeFileSync( 263 + join(sessionsRoot, "rollout-2026-04-21T10-00-00-aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa.jsonl"), 264 + [ 265 + lineAt("2026-04-21T10:00:00.000Z", "session_meta", { id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", cwd: "/tmp/recent-keyword" }), 266 + lineAt("2026-04-21T10:01:00.000Z", "event_msg", { type: "user_message", message: "$xsearch older target" }), 267 + ].join("\n"), 268 + ); 269 + writeFileSync( 270 + join(sessionsRoot, "rollout-2026-04-21T12-00-00-bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb.jsonl"), 271 + [ 272 + lineAt("2026-04-21T12:00:00.000Z", "session_meta", { id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", cwd: "/tmp/recent-keyword" }), 273 + lineAt("2026-04-21T12:01:00.000Z", "event_msg", { type: "user_message", message: "latest question mentions xsearch" }), 274 + ].join("\n"), 275 + ); 276 + 277 + const dbPath = join(base, "index.sqlite"); 278 + await syncSessions({ dbPath, rootDir: join(base, "sessions") }); 279 + 280 + const newest = await runCli(["find", "xsearch", "--sort", "ended", "--db", dbPath, "--json"]); 281 + expect(newest.exitCode).toBe(0); 282 + const newestPayload = JSON.parse(newest.stdout) as { sort: string; results: Array<{ sessionUuid: string }> }; 283 + expect(newestPayload.sort).toBe("ended"); 284 + expect(newestPayload.results[0]?.sessionUuid).toBe("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"); 285 + 286 + const excluded = await runCli([ 287 + "find", 288 + "xsearch", 289 + "--sort", 290 + "ended", 291 + "--exclude-session", 292 + "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", 293 + "--db", 294 + dbPath, 295 + "--json", 296 + ]); 297 + expect(excluded.exitCode).toBe(0); 298 + const excludedPayload = JSON.parse(excluded.stdout) as { excludedSessions: string[]; results: Array<{ sessionUuid: string }> }; 299 + expect(excludedPayload.excludedSessions).toEqual(["bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"]); 300 + expect(excludedPayload.results[0]?.sessionUuid).toBe("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); 190 301 }); 191 302 192 303 test("find emits friendly guidance when index is missing", async () => { ··· 388 499 }); 389 500 390 501 function line(type: string, payload: Record<string, unknown>): string { 502 + return lineAt(new Date("2026-04-21T00:00:00.000Z").toISOString(), type, payload); 503 + } 504 + 505 + function lineAt(timestamp: string, type: string, payload: Record<string, unknown>): string { 391 506 return JSON.stringify({ 392 - timestamp: new Date("2026-04-21T00:00:00.000Z").toISOString(), 507 + timestamp, 393 508 type, 394 509 payload, 395 510 });
+33 -7
src/cli.ts
··· 30 30 import { parseSelectorJson, SelectorParseError } from "./selector"; 31 31 import { collectStatus } from "./status"; 32 32 import { SyncLockTimeoutError } from "./sync-lock"; 33 - import type { Selector, SessionListSort } from "./types"; 33 + import type { FindSort, Selector, SessionListSort } from "./types"; 34 34 35 35 const program = new Command(); 36 36 ··· 43 43 .command("status") 44 44 .description("返回执行上下文、source inventory、index 与 coverage 状态") 45 45 .option("--root <dir>", "覆盖默认 sessions 根目录") 46 + .option("--selector <json>", "检查指定 selector 的 coverage/freshness(只读,不同步)") 46 47 .option("--db <path>", "覆盖默认数据库路径", DEFAULT_DB_PATH) 47 48 .option("--json", "输出 JSON") 48 49 .action((options) => { 49 - const status = collectStatus({ rootDir: options.root, dbPath: options.db, cwd: process.cwd() }); 50 - if (options.json) { 51 - console.log(JSON.stringify(status, null, 2)); 52 - return; 50 + try { 51 + const selector = optionalSelector(options.selector); 52 + const status = collectStatus({ rootDir: options.root, dbPath: options.db, cwd: process.cwd(), selector: selector ?? undefined }); 53 + if (options.json) { 54 + console.log(JSON.stringify(status, null, 2)); 55 + return; 56 + } 57 + printStatus(status); 58 + } catch (error) { 59 + if (error instanceof SelectorParseError) { 60 + emitSelectorError(error, Boolean(options.json)); 61 + return; 62 + } 63 + throw error; 53 64 } 54 - printStatus(status); 55 65 }); 56 66 57 67 program ··· 106 116 .description("搜索相关 session,返回最小必要命中") 107 117 .option("-n, --limit <n>", "返回条数", "10") 108 118 .option("--selector <json>", "结构化查询范围 JSON") 119 + .option("--sort <key>", "排序键:relevance|ended|started", "relevance") 120 + .option("--exclude-session <uuid>", "排除指定 session_uuid;可重复", collectValues, []) 109 121 .option("--db <path>", "覆盖默认数据库路径", DEFAULT_DB_PATH) 110 122 .option("--json", "输出 JSON") 111 123 .action((query, options) => { 112 124 runReadCommand(Boolean(options.json), () => { 113 125 const limit = parsePositiveInt(options.limit, 10); 114 126 const selector = optionalSelector(options.selector); 115 - const result = findSessions(options.db, query, limit, selector); 127 + const sort = normalizeFindSort(options.sort); 128 + const result = findSessions(options.db, query, limit, selector, { 129 + sort, 130 + excludeSessions: options.excludeSession ?? [], 131 + }); 116 132 if (options.json) { 117 133 console.log(JSON.stringify(result, null, 2)); 118 134 return; ··· 247 263 function normalizeListSort(value: string | undefined): SessionListSort { 248 264 if (value === "started" || value === "messages") return value; 249 265 return "ended"; 266 + } 267 + 268 + function normalizeFindSort(value: string | undefined): FindSort { 269 + if (value === "ended" || value === "started") return value; 270 + return "relevance"; 271 + } 272 + 273 + function collectValues(value: string, previous: string[]): string[] { 274 + previous.push(value); 275 + return previous; 250 276 } 251 277 252 278 function runReadCommand(jsonMode: boolean, action: () => void): void {
+6
src/format.ts
··· 141 141 console.log(`sessions: ${status.index.sessionCount}`); 142 142 console.log(`messages: ${status.index.messageCount}`); 143 143 console.log(`coverage_count: ${status.coverage.length}`); 144 + if (status.requestedCoverage) { 145 + console.log(`requested_coverage: ${status.requestedCoverage.freshness}`); 146 + console.log(`recommended_action: ${status.requestedCoverage.recommendedAction}`); 147 + console.log(`source_file_count: ${status.requestedCoverage.sourceFileCount}`); 148 + console.log(`covering_selectors: ${status.requestedCoverage.coveringSelectors.length}`); 149 + } 144 150 if (status.sourceInventory.cwdGroups.length > 0) { 145 151 console.log(); 146 152 console.log(chalk.bold("source cwd groups"));
+41 -7
src/query/find.ts
··· 1 1 import { withReadDb } from "../db"; 2 2 import { rerankHits } from "../ranking"; 3 - import type { FindResult, Selector } from "../types"; 3 + import type { FindResult, FindSort, Selector } from "../types"; 4 4 import { buildCoverageStatus } from "./coverage"; 5 5 import { searchMessageHits, searchSessionHits } from "./search"; 6 6 7 + export interface FindSessionsOptions { 8 + sort?: FindSort; 9 + excludeSessions?: string[]; 10 + } 11 + 7 12 export function findSessions( 8 13 dbPath: string, 9 14 query: string, 10 15 limit: number, 11 16 selector: Selector | null = null, 12 - ): { query: string; results: FindResult[]; coverage: ReturnType<typeof buildCoverageStatus> } { 17 + options: FindSessionsOptions = {}, 18 + ): { 19 + query: string; 20 + sort: FindSort; 21 + excludedSessions: string[]; 22 + results: FindResult[]; 23 + coverage: ReturnType<typeof buildCoverageStatus>; 24 + } { 13 25 return withReadDb(dbPath, (db) => { 14 - const recallLimit = Math.max(limit * 12, 50); 26 + const sort = options.sort ?? "relevance"; 27 + const excludedSessions = uniqueNonEmpty(options.excludeSessions ?? []); 28 + const recallLimit = sort === "relevance" ? Math.max(limit * 12, 50) : Math.max(limit * 100, 1000); 15 29 const rawRows = [ 16 - ...searchMessageHits(db, query, recallLimit, undefined, selector), 17 - ...searchSessionHits(db, query, recallLimit, selector), 30 + ...searchMessageHits(db, query, recallLimit, undefined, selector, { sort, excludeSessions: excludedSessions }), 31 + ...searchSessionHits(db, query, recallLimit, selector, { sort, excludeSessions: excludedSessions }), 18 32 ]; 19 - const results = rerankHits(rawRows, query, limit); 20 - return { query, results, coverage: buildCoverageStatus(db, selector) }; 33 + const ranked = rerankHits(rawRows, query, Math.max(rawRows.length, limit)); 34 + const results = sort === "relevance" 35 + ? ranked.slice(0, limit) 36 + : ranked.sort((left, right) => compareByTime(left, right, sort)).slice(0, limit) 37 + .map((result, index) => ({ ...result, rank: index + 1 })); 38 + return { query, sort, excludedSessions, results, coverage: buildCoverageStatus(db, selector) }; 21 39 }); 22 40 } 41 + 42 + function compareByTime(left: FindResult, right: FindResult, sort: FindSort): number { 43 + const leftTime = Date.parse(sort === "started" ? left.startedAt : left.endedAt); 44 + const rightTime = Date.parse(sort === "started" ? right.startedAt : right.endedAt); 45 + const primary = safeTime(rightTime) - safeTime(leftTime); 46 + if (primary !== 0) return primary; 47 + return right.score - left.score; 48 + } 49 + 50 + function safeTime(value: number): number { 51 + return Number.isNaN(value) ? 0 : value; 52 + } 53 + 54 + function uniqueNonEmpty(values: string[]): string[] { 55 + return [...new Set(values.map((value) => value.trim()).filter(Boolean))]; 56 + }
+54 -10
src/query/search.ts
··· 1 1 import { selectorWhereSql } from "../db"; 2 2 import type { RawHitRow } from "../ranking"; 3 3 import { hasCjk, queryTerms } from "../tokenize"; 4 - import type { Selector } from "../types"; 4 + import type { FindSort, Selector } from "../types"; 5 5 import type { Db, SqlParams } from "../db"; 6 6 import { makeLikeSnippet, makeRawSnippet } from "./snippet"; 7 7 8 + export interface SearchOptions { 9 + sort?: FindSort; 10 + excludeSessions?: string[]; 11 + } 12 + 8 13 export function searchMessageHits( 9 14 db: Db, 10 15 query: string, 11 16 limit: number, 12 17 sessionUuid?: string, 13 18 selector: Selector | null = null, 19 + options: SearchOptions = {}, 14 20 ): RawHitRow[] { 15 21 const normalized = query.trim(); 16 22 if (!normalized) return []; ··· 21 27 // back to a bounded LIKE scan so single-character CJK probes still work 22 28 // even though they are discouraged. 23 29 if (terms.length === 0) { 24 - if (hasCjk(normalized)) return searchByLike(db, normalized, limit, sessionUuid, selector); 30 + if (hasCjk(normalized)) return searchByLike(db, normalized, limit, sessionUuid, selector, options); 25 31 return []; 26 32 } 27 33 28 - return searchByFts(db, terms, limit, sessionUuid, selector); 34 + return searchByFts(db, terms, limit, sessionUuid, selector, options); 29 35 } 30 36 31 - export function searchSessionHits(db: Db, query: string, limit: number, selector: Selector | null): RawHitRow[] { 37 + export function searchSessionHits( 38 + db: Db, 39 + query: string, 40 + limit: number, 41 + selector: Selector | null, 42 + options: SearchOptions = {}, 43 + ): RawHitRow[] { 32 44 const normalized = query.trim(); 33 45 if (!normalized || !tableExists(db, "sessions_fts")) return []; 34 46 35 47 const terms = queryTerms(normalized); 36 48 if (terms.length === 0) { 37 - if (hasCjk(normalized)) return searchSessionsByLike(db, normalized, limit, selector); 49 + if (hasCjk(normalized)) return searchSessionsByLike(db, normalized, limit, selector, options); 38 50 return []; 39 51 } 40 52 41 - return searchSessionsByFts(db, normalized, terms, limit, selector); 53 + return searchSessionsByFts(db, normalized, terms, limit, selector, options); 42 54 } 43 55 44 56 function searchByFts( ··· 47 59 limit: number, 48 60 sessionUuid?: string, 49 61 selector: Selector | null = null, 62 + options: SearchOptions = {}, 50 63 ): RawHitRow[] { 51 64 const matchExpr = buildFtsMatch(terms); 52 65 const conditions = [`messages_fts MATCH ?`]; ··· 61 74 conditions.push("m.session_uuid = ?"); 62 75 params.push(sessionUuid); 63 76 } 77 + addExcludedSessions(conditions, params, "m", options.excludeSessions); 64 78 params.push(limit); 79 + 80 + const orderBy = orderBySql(options.sort, "score", "s", "m"); 65 81 66 82 return db 67 83 .prepare<typeof params, RawHitRow>(` ··· 83 99 JOIN messages m ON m.id = messages_fts.rowid 84 100 JOIN sessions s ON s.id = m.session_id 85 101 WHERE ${conditions.join(" AND ")} 86 - ORDER BY score 102 + ORDER BY ${orderBy} 87 103 LIMIT ? 88 104 `) 89 105 .all(...params) as RawHitRow[]; ··· 95 111 terms: string[], 96 112 limit: number, 97 113 selector: Selector | null, 114 + options: SearchOptions = {}, 98 115 ): RawHitRow[] { 99 116 const matchExpr = buildFtsMatch(terms); 100 117 const conditions = ["sessions_fts MATCH ?"]; ··· 104 121 conditions.push(...selectorWhere.conditions); 105 122 params.push(...selectorWhere.params); 106 123 } 124 + addExcludedSessions(conditions, params, "s", options.excludeSessions); 107 125 params.push(limit); 126 + const orderBy = orderBySql(options.sort, "score", "s"); 108 127 const rows = db 109 128 .prepare<typeof params, RawHitRow>(` 110 129 SELECT ··· 124 143 FROM sessions_fts 125 144 JOIN sessions s ON s.id = sessions_fts.rowid 126 145 WHERE ${conditions.join(" AND ")} 127 - ORDER BY score 146 + ORDER BY ${orderBy} 128 147 LIMIT ? 129 148 `) 130 149 .all(...params) as RawHitRow[]; ··· 140 159 query: string, 141 160 limit: number, 142 161 selector: Selector | null, 162 + options: SearchOptions = {}, 143 163 ): RawHitRow[] { 144 164 const like = `%${escapeLike(query.toLowerCase())}%`; 145 165 const conditions = [ ··· 156 176 conditions.push(...selectorWhere.conditions); 157 177 params.push(...selectorWhere.params); 158 178 } 179 + addExcludedSessions(conditions, params, "s", options.excludeSessions); 159 180 params.push(limit); 181 + const orderBy = orderBySql(options.sort, "s.started_at DESC", "s"); 160 182 161 183 const rows = db 162 184 .prepare<typeof params, RawHitRow & { contentText: string }>(` ··· 174 196 s.title || char(10) || s.summary_text || char(10) || s.compact_text || char(10) || s.reasoning_summary_text AS contentText 175 197 FROM sessions s 176 198 WHERE ${conditions.join(" AND ")} 177 - ORDER BY s.started_at DESC 199 + ORDER BY ${orderBy} 178 200 LIMIT ? 179 201 `) 180 202 .all(...params) as Array<RawHitRow & { contentText: string }>; ··· 192 214 limit: number, 193 215 sessionUuid?: string, 194 216 selector: Selector | null = null, 217 + options: SearchOptions = {}, 195 218 ): RawHitRow[] { 196 219 const conditions = ["lower(m.content_text) LIKE ? ESCAPE '\\'"]; 197 220 const params: SqlParams = [`%${escapeLike(query.toLowerCase())}%`]; ··· 204 227 conditions.push("m.session_uuid = ?"); 205 228 params.push(sessionUuid); 206 229 } 230 + addExcludedSessions(conditions, params, "m", options.excludeSessions); 207 231 params.push(limit); 232 + const orderBy = orderBySql(options.sort, "s.started_at DESC, m.seq ASC", "s", "m"); 208 233 209 234 const rows = db 210 235 .prepare<typeof params, RawHitRow & { contentText: string }>(` ··· 223 248 FROM messages m 224 249 JOIN sessions s ON s.id = m.session_id 225 250 WHERE ${conditions.join(" AND ")} 226 - ORDER BY s.started_at DESC, m.seq ASC 251 + ORDER BY ${orderBy} 227 252 LIMIT ? 228 253 `) 229 254 .all(...params) as Array<RawHitRow & { contentText: string }>; ··· 236 261 // code that touches this raw score won't see a sign mismatch. 237 262 score: -(index + 1), 238 263 })); 264 + } 265 + 266 + function addExcludedSessions( 267 + conditions: string[], 268 + params: SqlParams, 269 + alias: string, 270 + excludedSessions: string[] | undefined, 271 + ): void { 272 + const unique = [...new Set((excludedSessions ?? []).map((value) => value.trim()).filter(Boolean))]; 273 + if (unique.length === 0) return; 274 + conditions.push(`${alias}.session_uuid NOT IN (${unique.map(() => "?").join(", ")})`); 275 + params.push(...unique); 276 + } 277 + 278 + function orderBySql(sort: FindSort | undefined, relevanceOrder: string, sessionAlias: string, messageAlias?: string): string { 279 + if (sort === "ended") return `${sessionAlias}.ended_at DESC, ${relevanceOrder}`; 280 + if (sort === "started") return `${sessionAlias}.started_at DESC, ${relevanceOrder}`; 281 + if (messageAlias && relevanceOrder === "score") return `score, ${sessionAlias}.ended_at DESC, ${messageAlias}.seq ASC`; 282 + return relevanceOrder; 239 283 } 240 284 241 285 function tableExists(db: Db, tableName: string): boolean {
+35 -4
src/status.ts
··· 2 2 import { collectSourceInventory, collectSourceSnapshot } from "./source-inventory"; 3 3 import { INDEX_VERSION, DEFAULT_DB_PATH, resolveCodexDir } from "./env"; 4 4 import { getStatsCounts, listCoverageRecords, withReadDb } from "./db"; 5 - import type { CoverageInventoryStatus, CoverageRecord, StatusSummary } from "./types"; 5 + import { selectorImplies } from "./selector"; 6 + import type { CoverageInventoryStatus, CoverageRecord, RequestedCoverageStatus, Selector, StatusSummary } from "./types"; 6 7 7 - export function collectStatus(options: { rootDir?: string; dbPath?: string; cwd?: string } = {}): StatusSummary { 8 + export function collectStatus(options: { rootDir?: string; dbPath?: string; cwd?: string; selector?: Selector } = {}): StatusSummary { 8 9 const root = resolveCodexDir(options.rootDir); 9 10 const dbPath = options.dbPath ?? DEFAULT_DB_PATH; 10 11 const sourceInventory = collectSourceInventory(root); 11 12 const index = collectIndexStatus(dbPath); 12 13 const coverage = existsSync(dbPath) ? withReadDb(dbPath, (db) => listCoverageRecords(db)) : []; 13 - return { 14 + const coverageStatus = coverage.map(toCoverageInventoryStatus); 15 + const summary: StatusSummary = { 14 16 context: { 15 17 cwd: options.cwd ?? process.cwd(), 16 18 root, ··· 19 21 }, 20 22 sourceInventory, 21 23 index, 22 - coverage: coverage.map(toCoverageInventoryStatus), 24 + coverage: coverageStatus, 23 25 }; 26 + if (options.selector) { 27 + summary.requestedCoverage = requestedCoverageStatus(options.selector, coverageStatus); 28 + } 29 + return summary; 24 30 } 25 31 26 32 function collectIndexStatus(dbPath: string): StatusSummary["index"] { ··· 67 73 currentSourceFileCount: snapshot.fileCount, 68 74 }; 69 75 } 76 + 77 + function requestedCoverageStatus( 78 + selector: Selector, 79 + coverage: CoverageInventoryStatus[], 80 + ): RequestedCoverageStatus { 81 + const snapshot = collectSourceSnapshot(selector); 82 + const coveringSelectors = coverage.filter((entry) => 83 + entry.indexVersion === INDEX_VERSION && selectorImplies(entry.selector, selector) 84 + ); 85 + const hasFreshCovering = coveringSelectors.some((entry) => entry.freshness === "fresh"); 86 + const freshness: RequestedCoverageStatus["freshness"] = hasFreshCovering 87 + ? "fresh" 88 + : coveringSelectors.length > 0 89 + ? "stale" 90 + : "missing"; 91 + return { 92 + requested: snapshot.selector, 93 + complete: freshness === "fresh", 94 + freshness, 95 + sourceFingerprint: snapshot.fingerprint, 96 + sourceFileCount: snapshot.fileCount, 97 + coveringSelectors, 98 + recommendedAction: freshness === "fresh" ? "query" : "sync", 99 + }; 100 + }
+12
src/types.ts
··· 107 107 coveringSelectors: CoverageRecord[]; 108 108 } 109 109 110 + export interface RequestedCoverageStatus { 111 + requested: Selector; 112 + complete: boolean; 113 + freshness: "fresh" | "stale" | "missing"; 114 + sourceFingerprint: string; 115 + sourceFileCount: number; 116 + coveringSelectors: CoverageInventoryStatus[]; 117 + recommendedAction: "query" | "sync"; 118 + } 119 + 110 120 export interface SessionRecord { 111 121 sessionUuid: string; 112 122 filePath: string; ··· 172 182 } 173 183 174 184 export type SessionListSort = "ended" | "started" | "messages"; 185 + export type FindSort = "relevance" | "ended" | "started"; 175 186 176 187 export interface SessionListQuery { 177 188 cwd?: string; ··· 217 228 lastSyncAt: string | null; 218 229 }; 219 230 coverage: CoverageInventoryStatus[]; 231 + requestedCoverage?: RequestedCoverageStatus; 220 232 }