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:
@@ -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"]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user