diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 930c2db9c..cb4353431 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed Ctrl+R history search results to remain globally sorted by prompt recency after merging FTS prefix matches with substring fallback matches. - Fixed Exa web search with no stored or environment credential to use the public Exa MCP fallback again, preserving the auth storage → `EXA_API_KEY` → `mcp.exa.ai` resolution order ([#1860](https://github.com/can1357/oh-my-pi/issues/1860)). - Fixed `provider.appendOnlyContext: "auto"` staying inactive for Xiaomi Token Plan/SGLang endpoints, preserving prefix-cache hits without forcing append-only mode globally ([#1851](https://github.com/can1357/oh-my-pi/issues/1851)). - Fixed `models.yml` compatibility parsing to preserve `compat.cacheControlFormat: "anthropic"` for custom OpenAI-compatible Claude proxies. ([#1845](https://github.com/can1357/oh-my-pi/issues/1845)) diff --git a/packages/coding-agent/src/modes/components/session-selector.ts b/packages/coding-agent/src/modes/components/session-selector.ts index a3344b87e..c8d59f900 100644 --- a/packages/coding-agent/src/modes/components/session-selector.ts +++ b/packages/coding-agent/src/modes/components/session-selector.ts @@ -51,10 +51,10 @@ export type SessionHistoryMatcher = (query: string) => string[]; * * - `fuzzy` is the ordered fuzzy-filter result over session metadata (best first). * - `historyIds` are session IDs whose recorded prompts matched the query, - * ordered by history relevance (best first); duplicates are tolerated. + * ordered by prompt-history rank (typically newest matching prompt first); duplicates are tolerated. * * Ranking: sessions matched by **both** signals lead (keeping fuzzy order), then - * fuzzy-only matches, then history-only matches (by history order). A fuzzy match + * fuzzy-only matches, then history-only matches (by prompt-history order). A fuzzy match * is never dropped, and history matches not present in `allSessions` (e.g. deleted * or out-of-scope sessions) are ignored since they cannot be resumed from here. */ diff --git a/packages/coding-agent/src/modes/components/tips.txt b/packages/coding-agent/src/modes/components/tips.txt index 620e90bf4..e685aaf0c 100644 --- a/packages/coding-agent/src/modes/components/tips.txt +++ b/packages/coding-agent/src/modes/components/tips.txt @@ -12,4 +12,7 @@ Say `workflow` in your message to drive the task with parallel subagents in eval Log in to several accounts of the same provider — `/login` again — and omp load-balances across them automatically Run `omp auth-broker serve` once and every machine pulls live tokens over the wire — refresh keys never leave the host; `omp auth-gateway` fronts it as a drop-in proxy any OpenAI-compatible client can hit Press alt+p (or /switch) to switch provider, and ctrl+p to cycle role models smol -> slow -> etc -Press ctrl+r to search your prompt history and reuse a past message \ No newline at end of file +Press ctrl+r to search your prompt history and reuse a past message +`/force read` pins the next turn to one specific tool when the model keeps reaching for the wrong one +`/copy code` grabs the last code block to your clipboard — `/copy cmd` grabs the last shell/python command +`/shake` rips heavy tool results out of context to reclaim tokens without a full /compact — `/shake images` drops just images \ No newline at end of file diff --git a/packages/coding-agent/src/session/history-storage.ts b/packages/coding-agent/src/session/history-storage.ts index 1001d788e..e8e46a9ae 100644 --- a/packages/coding-agent/src/session/history-storage.ts +++ b/packages/coding-agent/src/session/history-storage.ts @@ -209,10 +209,6 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN logger.debug("HistoryStorage FTS query failed, using substring only", { error: String(error) }); } - if (ftsRows.length >= safeLimit) { - return ftsRows.map(row => this.#toEntry(row)); - } - // 2. Substring fallback (token-AND LIKE). Catches infix matches FTS5's // prefix-only wildcard cannot reach (e.g. "mit" -> "commit"). Bounded // by safeLimit, ordered by recency - no full-table load into JS. @@ -227,27 +223,24 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN return subRows.map(row => this.#toEntry(row)); } - const seen = new Set(); - const merged: HistoryEntry[] = []; + const rowsById = new Map(); for (const row of ftsRows) { - if (seen.has(row.id)) continue; - seen.add(row.id); - merged.push(this.#toEntry(row)); + rowsById.set(row.id, row); } for (const row of subRows) { - if (merged.length >= safeLimit) break; - if (seen.has(row.id)) continue; - seen.add(row.id); - merged.push(this.#toEntry(row)); + if (!rowsById.has(row.id)) rowsById.set(row.id, row); } - return merged; + + return [...rowsById.values()] + .sort((a, b) => b.created_at - a.created_at || b.id - a.id) + .slice(0, safeLimit) + .map(row => this.#toEntry(row)); } /** - * IDs of the sessions whose stored prompts match `query`, ordered by match - * relevance (most relevant/recent first) and de-duplicated. Prompts with no - * recorded session are skipped. Used to augment session ranking in the - * resume picker with prompts that the 4KB session-list prefix never sees. + * IDs of the sessions whose stored prompts match `query`, ordered by prompt + * recency and de-duplicated. Used to augment session ranking in the resume + * picker with prompts that the 4KB session-list prefix never sees. */ matchingSessionIds(query: string, limit = 500): string[] { const seen = new Set(); diff --git a/packages/coding-agent/test/history-storage-search.test.ts b/packages/coding-agent/test/history-storage-search.test.ts index ad78fb419..8e9ad3859 100644 --- a/packages/coding-agent/test/history-storage-search.test.ts +++ b/packages/coding-agent/test/history-storage-search.test.ts @@ -68,16 +68,16 @@ describe("HistoryStorage.search", () => { expect(results.map(r => r.prompt)).toEqual(["commit and amend the patch"]); }); - it("returns FTS matches before substring-only fallback matches", async () => { + it("returns merged FTS and substring fallback matches by recency", async () => { const storage = await freshStorage(); - // Insert oldest -> newest. Substring-only match is the most recent; - // FTS prefix match is older. FTS results must still come first. + // Insert oldest -> newest. The newest row is substring-only; it must not + // be pushed behind older FTS prefix matches in Ctrl+R results. await seed(storage, ["commit the changes", "precommit hook fix"]); const results = storage.search("commit", 10); expect(results.map(r => r.prompt)).toEqual([ - "commit the changes", // FTS prefix match on token `commit` "precommit hook fix", // substring-only (`commit` is infix of `precommit`) + "commit the changes", // FTS prefix match on token `commit` ]); }); @@ -106,14 +106,13 @@ describe("HistoryStorage.search", () => { expect(storage.search(" ", 10)).toEqual([]); }); - it("respects the limit when merging FTS and substring results", async () => { + it("respects the limit after globally sorting merged FTS and substring results", async () => { const storage = await freshStorage(); await seed(storage, ["commit one", "commit two", "precommit three", "precommit four"]); const results = storage.search("commit", 2); expect(results).toHaveLength(2); - // Both FTS matches should fill the limit before substring fallback runs. - expect(results.map(r => r.prompt)).toEqual(["commit two", "commit one"]); + expect(results.map(r => r.prompt)).toEqual(["precommit four", "precommit three"]); }); it("matches short tokens via the substring fallback", async () => { diff --git a/packages/coding-agent/test/session-ranking.test.ts b/packages/coding-agent/test/session-ranking.test.ts index 109162260..4be0da16e 100644 --- a/packages/coding-agent/test/session-ranking.test.ts +++ b/packages/coding-agent/test/session-ranking.test.ts @@ -37,10 +37,10 @@ describe("mergeSessionRanking", () => { expect(ids(mergeSessionRanking(all, fuzzy, ["b"]))).toEqual(["a", "b"]); }); - it("surfaces purely history-matched sessions ordered by history relevance", () => { + it("surfaces purely history-matched sessions ordered by prompt-history rank", () => { const all = ["a", "b", "c"].map(makeSession); - // No fuzzy match at all; c is the most relevant prompt match, then a. b is excluded. + // No fuzzy match at all; c is the best prompt-history match, then a. b is excluded. expect(ids(mergeSessionRanking(all, [], ["c", "a"]))).toEqual(["c", "a"]); });