fix(coding-agent): sorted ctrl+r search results by recency

- Removed early FTS-only return so prefix and substring matches merge.
- Sorted merged results globally by prompt recency before limiting.
- Added /force, /copy, and /shake tips.
This commit is contained in:
can1357
2026-06-04 15:40:37 +02:00
parent d32a33a4da
commit d7eee460d2
6 changed files with 26 additions and 30 deletions
+1
View File
@@ -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))
@@ -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.
*/
@@ -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
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
@@ -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<number>();
const merged: HistoryEntry[] = [];
const rowsById = new Map<number, HistoryRow>();
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<string>();
@@ -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 () => {
@@ -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"]);
});