@cortexkit/opencode-magic-context 0.46.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auto-search-worker.js +115 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/live-run-config.d.ts.map +1 -1
- package/dist/config/migrate-config-location.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +15 -7
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-7fj2b9jw.js → conflict-warning-hook-z44ynfz7.js} +5 -4
- package/dist/{context-8dkpszy5.js → context-wq4ffwyb.js} +164 -37
- package/dist/{derive-budgets-7nj7r1rv.js → derive-budgets-ke12jnnh.js} +1 -1
- package/dist/embedding-worker.js +2 -2
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-gates.d.ts.map +1 -1
- package/dist/features/magic-context/overflow-detection.d.ts +2 -0
- package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts +7 -0
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts +2 -0
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/session-project-storage.d.ts +8 -2
- package/dist/features/magic-context/session-project-storage.d.ts.map +1 -1
- package/dist/features/magic-context/storage-historian-runs.d.ts +2 -5
- package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +12 -0
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts +7 -2
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/storage.d.ts +1 -0
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts +3 -1
- package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
- package/dist/hooks/magic-context/active-anthropic-turn.d.ts +8 -0
- package/dist/hooks/magic-context/active-anthropic-turn.d.ts.map +1 -0
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-blocking.fixture.d.ts +2 -0
- package/dist/hooks/magic-context/auto-search-blocking.fixture.d.ts.map +1 -0
- package/dist/hooks/magic-context/auto-search-deadline.d.ts +7 -0
- package/dist/hooks/magic-context/auto-search-deadline.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-host-review.fixture.d.ts +3 -0
- package/dist/hooks/magic-context/auto-search-host-review.fixture.d.ts.map +1 -0
- package/dist/hooks/magic-context/auto-search-lock-review.fixture.d.ts +2 -0
- package/dist/hooks/magic-context/auto-search-lock-review.fixture.d.ts.map +1 -0
- package/dist/hooks/magic-context/auto-search-long-lock-review.fixture.d.ts +2 -0
- package/dist/hooks/magic-context/auto-search-long-lock-review.fixture.d.ts.map +1 -0
- package/dist/hooks/magic-context/auto-search-omp-host-review.fixture.d.ts +4 -0
- package/dist/hooks/magic-context/auto-search-omp-host-review.fixture.d.ts.map +1 -0
- package/dist/hooks/magic-context/auto-search-omp-loader.fixture.d.ts +3 -0
- package/dist/hooks/magic-context/auto-search-omp-loader.fixture.d.ts.map +1 -0
- package/dist/hooks/magic-context/auto-search-runner.d.ts +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-snapshot.fixture.d.ts +4 -0
- package/dist/hooks/magic-context/auto-search-snapshot.fixture.d.ts.map +1 -0
- package/dist/hooks/magic-context/auto-search-worker-client.d.ts +69 -0
- package/dist/hooks/magic-context/auto-search-worker-client.d.ts.map +1 -0
- package/dist/hooks/magic-context/auto-search-worker.d.ts +2 -0
- package/dist/hooks/magic-context/auto-search-worker.d.ts.map +1 -0
- package/dist/hooks/magic-context/caveman-cleanup.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +3 -5
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts +2 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-trigger.d.ts +3 -1
- package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
- package/dist/hooks/magic-context/historian-drain-gate.d.ts +11 -0
- package/dist/hooks/magic-context/historian-drain-gate.d.ts.map +1 -0
- package/dist/hooks/magic-context/hook.d.ts +2 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/latest-assistant-turn.d.ts +25 -0
- package/dist/hooks/magic-context/latest-assistant-turn.d.ts.map +1 -0
- package/dist/hooks/magic-context/latest-thinking-recovery.d.ts +61 -0
- package/dist/hooks/magic-context/latest-thinking-recovery.d.ts.map +1 -0
- package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
- package/dist/hooks/magic-context/opencode-proven-floor.d.ts +6 -0
- package/dist/hooks/magic-context/opencode-proven-floor.d.ts.map +1 -0
- package/dist/hooks/magic-context/pass-outcome.d.ts.map +1 -1
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
- package/dist/hooks/magic-context/reasoning-budget-status.d.ts +15 -0
- package/dist/hooks/magic-context/reasoning-budget-status.d.ts.map +1 -0
- package/dist/hooks/magic-context/reasoning-budget.d.ts +40 -0
- package/dist/hooks/magic-context/reasoning-budget.d.ts.map +1 -0
- package/dist/hooks/magic-context/reasoning-removal.d.ts +6 -6
- package/dist/hooks/magic-context/reasoning-removal.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/sentinel.d.ts +4 -0
- package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
- package/dist/hooks/magic-context/strip-content.d.ts +7 -3
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts +13 -0
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-drop-target.d.ts +5 -0
- package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -28
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +28 -6
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +3 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-0d3g3461.js → index-0c82743p.js} +2 -1
- package/dist/{index-e2dtv0ra.js → index-5a2r0p9f.js} +3 -3
- package/dist/{index-p821bjdb.js → index-6d9e1m08.js} +4726 -11050
- package/dist/{index-majxda46.js → index-6wkcswft.js} +1 -1
- package/dist/index-92bp57hg.js +2063 -0
- package/dist/{index-sfnj6h8h.js → index-9pk9exrb.js} +3 -3
- package/dist/{index-7qw742ev.js → index-adwfkpex.js} +5 -24
- package/dist/{index-3h0ybwah.js → index-bb36vgr3.js} +80 -12
- package/dist/{index-k4404na5.js → index-cwf6s3ay.js} +1072 -2422
- package/dist/index-fnn5pjtt.js +6554 -0
- package/dist/{index-j7fvnjye.js → index-gg8j07zz.js} +1 -1
- package/dist/{index-yasgb8ej.js → index-m5fe6jch.js} +9 -3
- package/dist/{v2/server-g0fjrkta.js → index-rrbbspwh.js} +42 -27
- package/dist/index-rrrsepyw.js +3464 -0
- package/dist/{index-qr6r7akd.js → index-w2t4ah5j.js} +950 -502
- package/dist/{v2/server-fg34g6a5.js → index-wx8a6bw3.js} +48 -8
- package/dist/{index-r5hmbx3t.js → index-x3kwrmqk.js} +146 -3571
- package/dist/index-yr3zj7e7.js +23 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +624 -65
- package/dist/{message-time-backfill-68gj6t3e.js → message-time-backfill-r5epkzj3.js} +1 -1
- package/dist/migration-worker.js +7 -5
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts +2 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/host-stall-profiler-report.d.ts +67 -0
- package/dist/plugin/host-stall-profiler-report.d.ts.map +1 -0
- package/dist/plugin/host-stall-profiler.d.ts +96 -0
- package/dist/plugin/host-stall-profiler.d.ts.map +1 -0
- package/dist/plugin/messages-transform.d.ts +21 -6
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/{read-session-chunk-egz7r68d.js → read-session-chunk-4jey7hpt.js} +4 -4
- package/dist/{recomp-orchestrator-ejq7ex5d.js → recomp-orchestrator-rervg6q4.js} +13 -10
- package/dist/{send-session-notification-dyw42dtr.js → send-session-notification-0m68ms3n.js} +4 -3
- package/dist/shared/auto-search-lifecycle.d.ts +7 -0
- package/dist/shared/auto-search-lifecycle.d.ts.map +1 -0
- package/dist/shared/historian-tool-expansions.d.ts +1 -1
- package/dist/shared/historian-tool-expansions.d.ts.map +1 -1
- package/dist/shared/historian-tool-template.d.ts +2 -2
- package/dist/shared/historian-tool-template.d.ts.map +1 -1
- package/dist/shared/sqlite.d.ts +16 -1
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/status-view.d.ts.map +1 -1
- package/dist/shared/window-geometry.d.ts +4 -0
- package/dist/shared/window-geometry.d.ts.map +1 -1
- package/dist/tools/ctx-expand/render.d.ts.map +1 -1
- package/dist/{transform-postprocess-phase-tz3pat2p.js → transform-postprocess-phase-z5hzpsxv.js} +13 -10
- package/dist/v2/auto-search-worker.js +115 -0
- package/dist/v2/{context-8vgtb6sz.js → context-14v1hr8b.js} +4918 -5760
- package/dist/v2/{derive-budgets-kdgp4s3r.js → derive-budgets-nc9avxz8.js} +2 -2
- package/dist/v2/embedding-worker.js +3 -3
- package/dist/v2/hidden-child-native.d.ts +7 -0
- package/dist/v2/hidden-child-native.d.ts.map +1 -1
- package/dist/v2/hidden-child-record.d.ts +8 -0
- package/dist/v2/hidden-child-record.d.ts.map +1 -1
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/dream-trigger.d.ts +15 -1
- package/dist/v2/hooks/dream-trigger.d.ts.map +1 -1
- package/dist/v2/hooks/hidden-child.d.ts +54 -3
- package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
- package/dist/v2/hooks/types.d.ts +12 -1
- package/dist/v2/hooks/types.d.ts.map +1 -1
- package/dist/v2/{message-time-backfill-r6qkem1x.js → message-time-backfill-1f9tzvcs.js} +1 -1
- package/dist/v2/migration-worker.js +7 -5
- package/dist/v2/{read-session-chunk-8vjp6581.js → read-session-chunk-gyyv7w2r.js} +4 -4
- package/dist/v2/{recomp-orchestrator-5xj0yzyk.js → recomp-orchestrator-42vjba7p.js} +17 -14
- package/dist/v2/{send-session-notification-qs0j78v6.js → send-session-notification-c0wtscea.js} +5 -4
- package/dist/v2/server-3trjk1hx.js +6360 -0
- package/dist/v2/{server-xtp687da.js → server-50hvkg3v.js} +9 -3
- package/dist/v2/{server-y8maxeft.js → server-cpepxy2q.js} +2 -1
- package/dist/v2/server-d0sraw7m.js +3403 -0
- package/dist/v2/server-e0c1b7a7.js +2063 -0
- package/dist/v2/{server-mcfawjgc.js → server-efhwzx05.js} +5 -24
- package/dist/v2/{server-3n4q9xme.js → server-fcdehk1s.js} +1 -1
- package/dist/{index-q4yp4b52.js → v2/server-fq7kq32k.js} +42 -27
- package/dist/v2/{server-vnh7r35f.js → server-nf82q6pt.js} +2459 -8603
- package/dist/v2/{server-frbm99q5.js → server-njt3np1k.js} +99 -3465
- package/dist/v2/{server-qp5hx1ad.js → server-nm9n3aqq.js} +107 -12
- package/dist/v2/{server-p2en78c8.js → server-rz514f14.js} +7 -2
- package/dist/v2/{server-wvjvfhck.js → server-vd4e7174.js} +1 -1
- package/dist/{index-fg34g6a5.js → v2/server-wx8a6bw3.js} +48 -8
- package/dist/v2/{server-4663ejam.js → server-y10vbq6y.js} +1 -1
- package/dist/v2/server-yr3zj7e7.js +23 -0
- package/dist/v2/server.js +5 -5
- package/dist/v2/{wrapup-orchestrator-r5t1pyj8.js → wrapup-orchestrator-nbz854vc.js} +16 -13
- package/dist/wrapup-orchestrator-b2dztrar.js +29 -0
- package/package.json +6 -5
- package/src/config/index.ts +6 -0
- package/src/config/live-run-config.test.ts +37 -1
- package/src/config/live-run-config.ts +28 -0
- package/src/config/migrate-config-location.ts +10 -3
- package/src/config/schema/magic-context.test.ts +4 -3
- package/src/config/schema/magic-context.ts +23 -6
- package/src/shared/auto-search-lifecycle.ts +27 -0
- package/src/shared/historian-tool-defaults.json +6 -6
- package/src/shared/historian-tool-expansions.ts +65 -8
- package/src/shared/historian-tool-preview-defaults.json +13 -0
- package/src/shared/historian-tool-template.ts +48 -14
- package/src/shared/sqlite-640-review-r2.test.ts +136 -0
- package/src/shared/sqlite-640-review.test.ts +183 -0
- package/src/shared/sqlite-acquisition-routing.test.ts +18 -0
- package/src/shared/sqlite.ts +84 -14
- package/src/shared/status-view.test.ts +6 -1
- package/src/shared/status-view.ts +15 -1
- package/src/shared/tag-transcript.ts +6 -0
- package/src/shared/window-geometry.ts +10 -0
- package/dist/wrapup-orchestrator-kw3x3myw.js +0 -26
- /package/dist/{index-en1k8h8m.js → index-5prbx026.js} +0 -0
- /package/dist/v2/{index-en1k8h8m.js → index-5prbx026.js} +0 -0
|
@@ -0,0 +1,2063 @@
|
|
|
1
|
+
import {
|
|
2
|
+
log
|
|
3
|
+
} from "./index-hybnrwq0.js";
|
|
4
|
+
import {
|
|
5
|
+
resolveWorkspaceShareCategories,
|
|
6
|
+
resolveWorkspaceIdentitySet,
|
|
7
|
+
expandWorkspaceIdentitySetWithAliases,
|
|
8
|
+
resolveStoredPathWorkspaceIdentity,
|
|
9
|
+
sourceNameForMemory
|
|
10
|
+
} from "./index-x3kwrmqk.js";
|
|
11
|
+
import {
|
|
12
|
+
loadCompartmentChunkEmbeddingsForSearch,
|
|
13
|
+
getIndexedMessageCorpusSize,
|
|
14
|
+
recordEmbeddingMeasurement,
|
|
15
|
+
getNotes,
|
|
16
|
+
getActivePrimers,
|
|
17
|
+
cosineSimilarity,
|
|
18
|
+
saveCommitEmbedding,
|
|
19
|
+
loadProjectCommitEmbeddings,
|
|
20
|
+
loadUnembeddedCommits,
|
|
21
|
+
countEmbeddedCommits,
|
|
22
|
+
upsertCommits,
|
|
23
|
+
getLatestIndexedCommitSha,
|
|
24
|
+
enforceProjectCap,
|
|
25
|
+
getProjectEmbeddings,
|
|
26
|
+
contentSha256,
|
|
27
|
+
getShadowEmbeddingMeasurementCohort,
|
|
28
|
+
getPrimaryEmbeddingMeasurementCohort,
|
|
29
|
+
embedShadowTextForProject,
|
|
30
|
+
enqueueShadowEmbeddingItems,
|
|
31
|
+
getProjectEmbeddingSnapshot,
|
|
32
|
+
embedItemsForProject,
|
|
33
|
+
isEmbeddingEnabled,
|
|
34
|
+
embedText,
|
|
35
|
+
ensureMemoryEmbeddings,
|
|
36
|
+
ModuleMemoryAuthorityError,
|
|
37
|
+
getMemoriesByProject,
|
|
38
|
+
getMemoriesByProjects,
|
|
39
|
+
updateMemoryRetrievalCount,
|
|
40
|
+
relaxedFtsQuery,
|
|
41
|
+
sanitizeFtsQuery,
|
|
42
|
+
searchMemoriesFTS,
|
|
43
|
+
searchMemoriesFTSUnion
|
|
44
|
+
} from "./index-fnn5pjtt.js";
|
|
45
|
+
import {
|
|
46
|
+
isKnownAutocommit
|
|
47
|
+
} from "./index-b35pxds2.js";
|
|
48
|
+
|
|
49
|
+
// src/features/magic-context/git-commits/git-log-reader.ts
|
|
50
|
+
import { execFile } from "node:child_process";
|
|
51
|
+
import { promisify } from "node:util";
|
|
52
|
+
var execFileAsync = promisify(execFile);
|
|
53
|
+
var GIT_TIMEOUT_MS = 1e4;
|
|
54
|
+
var DEFAULT_MAX_COMMITS = 5000;
|
|
55
|
+
var RECORD_SEPARATOR = "\x1E";
|
|
56
|
+
var FIELD_SEPARATOR = "\x1F";
|
|
57
|
+
var FULL_OBJECT_NAME = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
58
|
+
function classifyGitLogFailure(message) {
|
|
59
|
+
if (message.includes("not a git repository"))
|
|
60
|
+
return "not_a_repo";
|
|
61
|
+
if (message.includes("unknown revision or path not in the working tree") || message.includes("does not have any commits yet") || message.includes("bad revision")) {
|
|
62
|
+
return "no_head";
|
|
63
|
+
}
|
|
64
|
+
return "transient";
|
|
65
|
+
}
|
|
66
|
+
async function readGitCommitsResult(directory, options = {}) {
|
|
67
|
+
const revision = options.branch ?? "HEAD";
|
|
68
|
+
if (revision.startsWith("-")) {
|
|
69
|
+
throw new Error(`readGitCommits: refusing revision that looks like an option: "${revision}"`);
|
|
70
|
+
}
|
|
71
|
+
const projectLabel = options.projectIdentity ?? "<project>";
|
|
72
|
+
const exclusion = options.excludeReachableFrom && FULL_OBJECT_NAME.test(options.excludeReachableFrom) ? options.excludeReachableFrom : null;
|
|
73
|
+
const args = [
|
|
74
|
+
"log",
|
|
75
|
+
revision,
|
|
76
|
+
...exclusion ? [`^${exclusion}`] : [],
|
|
77
|
+
"--no-merges",
|
|
78
|
+
`--max-count=${options.maxCommits ?? DEFAULT_MAX_COMMITS}`,
|
|
79
|
+
`--format=%H${FIELD_SEPARATOR}%s${FIELD_SEPARATOR}%ae${FIELD_SEPARATOR}%ct${FIELD_SEPARATOR}%b${RECORD_SEPARATOR}`
|
|
80
|
+
];
|
|
81
|
+
if (options.sinceMs !== undefined && options.sinceMs > 0) {
|
|
82
|
+
const iso = new Date(options.sinceMs).toISOString();
|
|
83
|
+
args.push(`--since=${iso}`);
|
|
84
|
+
}
|
|
85
|
+
args.push("--");
|
|
86
|
+
let stdout;
|
|
87
|
+
try {
|
|
88
|
+
const result = await execFileAsync("git", args, {
|
|
89
|
+
windowsHide: true,
|
|
90
|
+
cwd: directory,
|
|
91
|
+
timeout: GIT_TIMEOUT_MS,
|
|
92
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
93
|
+
encoding: "utf8"
|
|
94
|
+
});
|
|
95
|
+
stdout = result.stdout;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
98
|
+
if (exclusion && message.includes("bad object")) {
|
|
99
|
+
log(`[git-commits] indexed tip ${exclusion.slice(0, 7)} is missing for ${projectLabel}; reading the full window`);
|
|
100
|
+
return readGitCommitsResult(directory, { ...options, excludeReachableFrom: undefined });
|
|
101
|
+
}
|
|
102
|
+
const failure = classifyGitLogFailure(message);
|
|
103
|
+
if (failure === "transient") {
|
|
104
|
+
log(`[git-commits] readGitCommits failed for ${projectLabel}: ${message.slice(0, 500)}`);
|
|
105
|
+
} else {
|
|
106
|
+
log(`[git-commits] ${projectLabel} is not indexable (${failure})`);
|
|
107
|
+
}
|
|
108
|
+
return { commits: [], failure };
|
|
109
|
+
}
|
|
110
|
+
if (stdout.trim().length === 0) {
|
|
111
|
+
log(`[git-commits] readGitCommits returned empty stdout for ${projectLabel} (sinceMs=${options.sinceMs ?? "none"} args=${args.slice(0, 4).join(" ")})`);
|
|
112
|
+
}
|
|
113
|
+
return { commits: parseGitLogOutput(stdout), failure: null };
|
|
114
|
+
}
|
|
115
|
+
function parseGitLogOutput(stdout) {
|
|
116
|
+
const commits = [];
|
|
117
|
+
const records = stdout.split(RECORD_SEPARATOR);
|
|
118
|
+
for (const rawRecord of records) {
|
|
119
|
+
const record = rawRecord.replace(/^\s+/, "");
|
|
120
|
+
if (!record)
|
|
121
|
+
continue;
|
|
122
|
+
const fields = [];
|
|
123
|
+
let remaining = record;
|
|
124
|
+
for (let i = 0;i < 4; i++) {
|
|
125
|
+
const idx = remaining.indexOf(FIELD_SEPARATOR);
|
|
126
|
+
if (idx < 0)
|
|
127
|
+
break;
|
|
128
|
+
fields.push(remaining.slice(0, idx));
|
|
129
|
+
remaining = remaining.slice(idx + FIELD_SEPARATOR.length);
|
|
130
|
+
}
|
|
131
|
+
fields.push(remaining);
|
|
132
|
+
if (fields.length < 5)
|
|
133
|
+
continue;
|
|
134
|
+
const sha = fields[0].trim();
|
|
135
|
+
const subject = fields[1].trim();
|
|
136
|
+
const author = fields[2].trim();
|
|
137
|
+
const timeSec = Number.parseInt(fields[3].trim(), 10);
|
|
138
|
+
const body = fields[4].trim();
|
|
139
|
+
if (!FULL_OBJECT_NAME.test(sha) || !Number.isFinite(timeSec) || timeSec <= 0) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const message = body.length > 0 ? `${subject}
|
|
143
|
+
|
|
144
|
+
${body}` : subject;
|
|
145
|
+
commits.push({
|
|
146
|
+
sha,
|
|
147
|
+
shortSha: sha.slice(0, 7),
|
|
148
|
+
message,
|
|
149
|
+
author: author.length > 0 ? author : null,
|
|
150
|
+
committedAtMs: timeSec * 1000
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return commits;
|
|
154
|
+
}
|
|
155
|
+
// src/features/magic-context/git-commits/indexer.ts
|
|
156
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
157
|
+
var EMBED_BATCH_SIZE = 16;
|
|
158
|
+
var EMBED_MAX_PER_SWEEP = 500;
|
|
159
|
+
var EMBED_SWEEP_MAX_WALL_CLOCK_MS = 5 * 60 * 1000;
|
|
160
|
+
var indexInProgress = new Set;
|
|
161
|
+
var embedInProgress = new Set;
|
|
162
|
+
async function indexCommitsForProject(db, projectPath, directory, options) {
|
|
163
|
+
const result = {
|
|
164
|
+
scanned: 0,
|
|
165
|
+
inserted: 0,
|
|
166
|
+
updated: 0,
|
|
167
|
+
evicted: 0,
|
|
168
|
+
embedded: 0,
|
|
169
|
+
nonIndexable: false
|
|
170
|
+
};
|
|
171
|
+
if (indexInProgress.has(projectPath)) {
|
|
172
|
+
log(`[git-commits] index already in progress for ${projectPath}, skipping`);
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
indexInProgress.add(projectPath);
|
|
176
|
+
try {
|
|
177
|
+
const latestIndexed = getLatestIndexedCommitSha(db, projectPath);
|
|
178
|
+
const sinceMs = Date.now() - options.sinceDays * MS_PER_DAY;
|
|
179
|
+
const read = await readGitCommitsResult(directory, {
|
|
180
|
+
sinceMs,
|
|
181
|
+
maxCommits: options.maxCommits,
|
|
182
|
+
projectIdentity: projectPath,
|
|
183
|
+
excludeReachableFrom: latestIndexed ?? undefined
|
|
184
|
+
});
|
|
185
|
+
const commits = read.commits;
|
|
186
|
+
result.scanned = commits.length;
|
|
187
|
+
if (read.failure === "not_a_repo" || read.failure === "no_head") {
|
|
188
|
+
result.nonIndexable = true;
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
if (commits.length === 0) {
|
|
192
|
+
result.evicted = enforceProjectCap(db, projectPath, options.maxCommits);
|
|
193
|
+
log(`[git-commits] no new commits for ${projectPath} (sinceMs=${sinceMs} latestIndexed=${latestIndexed?.slice(0, 7) ?? "none"} evicted=${result.evicted})`);
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
log(`[git-commits] read ${commits.length} commits for ${projectPath} (sinceMs=${sinceMs} latestIndexed=${latestIndexed?.slice(0, 7) ?? "none"})`);
|
|
197
|
+
const upsert = upsertCommits(db, projectPath, commits);
|
|
198
|
+
result.inserted = upsert.inserted;
|
|
199
|
+
result.updated = upsert.updated;
|
|
200
|
+
result.evicted = enforceProjectCap(db, projectPath, options.maxCommits);
|
|
201
|
+
const snapshot = getProjectEmbeddingSnapshot(projectPath);
|
|
202
|
+
if (options.skipEmbed || !snapshot?.gitCommitEnabled) {
|
|
203
|
+
log(`[git-commits] indexed ${projectPath}: scanned=${result.scanned} inserted=${result.inserted} updated=${result.updated} evicted=${result.evicted} embedded=0 (embedding skipped: skipEmbed=${options.skipEmbed === true} gitCommitEnabled=${snapshot?.gitCommitEnabled === true})`);
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
result.embedded = await embedUnembeddedCommits(db, projectPath);
|
|
207
|
+
log(`[git-commits] indexed ${projectPath}: scanned=${result.scanned} inserted=${result.inserted} updated=${result.updated} evicted=${result.evicted} embedded=${result.embedded}`);
|
|
208
|
+
return result;
|
|
209
|
+
} finally {
|
|
210
|
+
indexInProgress.delete(projectPath);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function embedUnembeddedCommits(db, projectPath) {
|
|
214
|
+
if (embedInProgress.has(projectPath)) {
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
const snapshot = getProjectEmbeddingSnapshot(projectPath);
|
|
218
|
+
if (!snapshot?.gitCommitEnabled) {
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
embedInProgress.add(projectPath);
|
|
222
|
+
const startedAt = Date.now();
|
|
223
|
+
const deadline = startedAt + EMBED_SWEEP_MAX_WALL_CLOCK_MS;
|
|
224
|
+
let total = 0;
|
|
225
|
+
try {
|
|
226
|
+
while (Date.now() < deadline && total < EMBED_MAX_PER_SWEEP) {
|
|
227
|
+
const rows = loadUnembeddedCommits(db, projectPath, snapshot.modelId, EMBED_BATCH_SIZE);
|
|
228
|
+
if (rows.length === 0)
|
|
229
|
+
break;
|
|
230
|
+
let embeddedThisBatch = 0;
|
|
231
|
+
try {
|
|
232
|
+
const result = await embedItemsForProject(projectPath, rows.map((row) => ({
|
|
233
|
+
id: `commit:${row.sha}`,
|
|
234
|
+
text: row.message,
|
|
235
|
+
contentSha256: contentSha256(row.message)
|
|
236
|
+
})), undefined, db, projectPath);
|
|
237
|
+
if (!result)
|
|
238
|
+
break;
|
|
239
|
+
db.transaction(() => {
|
|
240
|
+
for (const row of rows) {
|
|
241
|
+
const embedding = result.vectors.get(`commit:${row.sha}`);
|
|
242
|
+
if (!embedding)
|
|
243
|
+
continue;
|
|
244
|
+
saveCommitEmbedding(db, row.sha, embedding, result.modelId);
|
|
245
|
+
embeddedThisBatch += 1;
|
|
246
|
+
}
|
|
247
|
+
}).immediate();
|
|
248
|
+
enqueueShadowEmbeddingItems(projectPath, "commit", rows.filter((row) => result.vectors.has(`commit:${row.sha}`)).map((row) => row.sha));
|
|
249
|
+
} catch (error) {
|
|
250
|
+
log(`[git-commits] embed batch failed for ${projectPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
if (embeddedThisBatch === 0)
|
|
254
|
+
break;
|
|
255
|
+
total += embeddedThisBatch;
|
|
256
|
+
if (embeddedThisBatch < rows.length)
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
if (total > 0) {
|
|
260
|
+
const totalEmbedded = countEmbeddedCommits(db, projectPath, snapshot.modelId);
|
|
261
|
+
log(`[git-commits] embedded ${total} commits for ${projectPath} (total embedded: ${totalEmbedded})`);
|
|
262
|
+
}
|
|
263
|
+
return total;
|
|
264
|
+
} finally {
|
|
265
|
+
embedInProgress.delete(projectPath);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// src/features/magic-context/git-commits/search-git-commits.ts
|
|
269
|
+
var ftsStatements = new WeakMap;
|
|
270
|
+
var datedFtsStatements = new WeakMap;
|
|
271
|
+
var ftsPlainStatements = new WeakMap;
|
|
272
|
+
var datedFtsPlainStatements = new WeakMap;
|
|
273
|
+
var getBySHAsStatements = new WeakMap;
|
|
274
|
+
var datedGetBySHAsStatements = new WeakMap;
|
|
275
|
+
var embeddingCache = new WeakMap;
|
|
276
|
+
function getSearchEmbeddings(db, projectPath, modelId) {
|
|
277
|
+
if (!isKnownAutocommit(db)) {
|
|
278
|
+
embeddingCache.delete(db);
|
|
279
|
+
return loadProjectCommitEmbeddings(db, projectPath, modelId);
|
|
280
|
+
}
|
|
281
|
+
let cache = embeddingCache.get(db);
|
|
282
|
+
if (!cache) {
|
|
283
|
+
cache = {
|
|
284
|
+
revisionStatement: db.prepare(`SELECT total_changes() AS writes,
|
|
285
|
+
(SELECT data_version FROM pragma_data_version) AS dataVersion,
|
|
286
|
+
(SELECT schema_version FROM pragma_schema_version) AS schemaVersion`),
|
|
287
|
+
revision: "",
|
|
288
|
+
entries: new Map
|
|
289
|
+
};
|
|
290
|
+
embeddingCache.set(db, cache);
|
|
291
|
+
}
|
|
292
|
+
const revision = JSON.stringify(cache.revisionStatement.get());
|
|
293
|
+
if (revision !== cache.revision) {
|
|
294
|
+
cache.entries.clear();
|
|
295
|
+
cache.revision = revision;
|
|
296
|
+
}
|
|
297
|
+
const key = JSON.stringify([projectPath, modelId]);
|
|
298
|
+
const cached = cache.entries.get(key);
|
|
299
|
+
if (cached) {
|
|
300
|
+
cache.entries.delete(key);
|
|
301
|
+
cache.entries.set(key, cached);
|
|
302
|
+
return cached;
|
|
303
|
+
}
|
|
304
|
+
const vectors = loadProjectCommitEmbeddings(db, projectPath, modelId);
|
|
305
|
+
const size = (entries) => [...entries.values()].reduce((sum, vector) => sum + vector.byteLength, 0);
|
|
306
|
+
if (size(vectors) <= 32 * 1024 * 1024)
|
|
307
|
+
cache.entries.set(key, vectors);
|
|
308
|
+
while (cache.entries.size > 16 || [...cache.entries.values()].reduce((sum, entry) => sum + size(entry), 0) > 32 * 1024 * 1024) {
|
|
309
|
+
const oldest = cache.entries.keys().next().value;
|
|
310
|
+
if (oldest === undefined)
|
|
311
|
+
break;
|
|
312
|
+
cache.entries.delete(oldest);
|
|
313
|
+
}
|
|
314
|
+
return vectors;
|
|
315
|
+
}
|
|
316
|
+
function rowToCommit(row) {
|
|
317
|
+
return {
|
|
318
|
+
sha: row.sha,
|
|
319
|
+
shortSha: row.short_sha,
|
|
320
|
+
projectPath: row.project_path,
|
|
321
|
+
message: row.message,
|
|
322
|
+
author: row.author,
|
|
323
|
+
committedAtMs: row.committed_at,
|
|
324
|
+
indexedAtMs: row.indexed_at
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function getFtsStatement(db, dated = false) {
|
|
328
|
+
const statements = dated ? datedFtsStatements : ftsStatements;
|
|
329
|
+
let stmt = statements.get(db);
|
|
330
|
+
if (!stmt) {
|
|
331
|
+
stmt = db.prepare(`SELECT c.sha AS sha, c.project_path AS project_path, c.short_sha AS short_sha,
|
|
332
|
+
c.message AS message, c.author AS author,
|
|
333
|
+
c.committed_at AS committed_at, c.indexed_at AS indexed_at
|
|
334
|
+
FROM git_commits_fts
|
|
335
|
+
INNER JOIN git_commits c ON c.sha = git_commits_fts.sha
|
|
336
|
+
WHERE c.project_path = ?
|
|
337
|
+
${dated ? "AND c.committed_at BETWEEN ? AND ?" : ""}
|
|
338
|
+
AND git_commits_fts MATCH ?
|
|
339
|
+
ORDER BY bm25(git_commits_fts) LIMIT ?`);
|
|
340
|
+
statements.set(db, stmt);
|
|
341
|
+
}
|
|
342
|
+
return stmt;
|
|
343
|
+
}
|
|
344
|
+
function escapeLikePattern(text) {
|
|
345
|
+
return text.replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
346
|
+
}
|
|
347
|
+
function getLikeFallbackStatement(db, dated = false) {
|
|
348
|
+
const statements = dated ? datedFtsPlainStatements : ftsPlainStatements;
|
|
349
|
+
let stmt = statements.get(db);
|
|
350
|
+
if (!stmt) {
|
|
351
|
+
stmt = db.prepare(`SELECT sha, project_path, short_sha, message, author, committed_at, indexed_at
|
|
352
|
+
FROM git_commits
|
|
353
|
+
WHERE project_path = ?
|
|
354
|
+
${dated ? "AND committed_at BETWEEN ? AND ?" : ""}
|
|
355
|
+
AND lower(message) LIKE '%' || lower(?) || '%' ESCAPE '\\'
|
|
356
|
+
ORDER BY committed_at DESC LIMIT ?`);
|
|
357
|
+
statements.set(db, stmt);
|
|
358
|
+
}
|
|
359
|
+
return stmt;
|
|
360
|
+
}
|
|
361
|
+
function getBySHAsStatement(db, dated = false) {
|
|
362
|
+
const statements = dated ? datedGetBySHAsStatements : getBySHAsStatements;
|
|
363
|
+
let stmt = statements.get(db);
|
|
364
|
+
if (!stmt) {
|
|
365
|
+
stmt = db.prepare(`SELECT sha, project_path, short_sha, message, author, committed_at, indexed_at
|
|
366
|
+
FROM git_commits
|
|
367
|
+
WHERE project_path = ?
|
|
368
|
+
${dated ? "AND committed_at BETWEEN ? AND ?" : ""}
|
|
369
|
+
AND sha IN (SELECT value FROM json_each(?))`);
|
|
370
|
+
statements.set(db, stmt);
|
|
371
|
+
}
|
|
372
|
+
return stmt;
|
|
373
|
+
}
|
|
374
|
+
function clamp01(value) {
|
|
375
|
+
if (!Number.isFinite(value))
|
|
376
|
+
return 0;
|
|
377
|
+
return Math.min(1, Math.max(0, value));
|
|
378
|
+
}
|
|
379
|
+
function searchGitCommitsSync(db, projectPath, query, options) {
|
|
380
|
+
const trimmed = query.trim();
|
|
381
|
+
if (trimmed.length === 0 || options.limit <= 0)
|
|
382
|
+
return [];
|
|
383
|
+
const semanticWeight = options.semanticWeight ?? 0.7;
|
|
384
|
+
const ftsWeight = options.ftsWeight ?? 0.3;
|
|
385
|
+
const singleSourcePenalty = options.singleSourcePenalty ?? 0.8;
|
|
386
|
+
const fetchLimit = Math.max(options.limit * 3, 30);
|
|
387
|
+
const dated = options.from !== undefined || options.to !== undefined;
|
|
388
|
+
const from = options.from ?? Number.MIN_SAFE_INTEGER;
|
|
389
|
+
const to = options.to ?? Number.MAX_SAFE_INTEGER;
|
|
390
|
+
const ftsCandidates = [];
|
|
391
|
+
const sanitized = sanitizeFtsQuery(trimmed);
|
|
392
|
+
if (sanitized.length > 0) {
|
|
393
|
+
try {
|
|
394
|
+
for (const row of getFtsStatement(db, dated).all(projectPath, ...dated ? [from, to] : [], sanitized, fetchLimit)) {
|
|
395
|
+
ftsCandidates.push(rowToCommit(row));
|
|
396
|
+
}
|
|
397
|
+
} catch (error) {
|
|
398
|
+
log(`[git-commits] FTS query failed for "${trimmed}": ${error instanceof Error ? error.message : String(error)}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (ftsCandidates.length === 0) {
|
|
402
|
+
for (const row of getLikeFallbackStatement(db, dated).all(projectPath, ...dated ? [from, to] : [], escapeLikePattern(trimmed), fetchLimit)) {
|
|
403
|
+
ftsCandidates.push(rowToCommit(row));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const ftsScores = new Map;
|
|
407
|
+
ftsCandidates.forEach((commit, rank) => {
|
|
408
|
+
ftsScores.set(commit.sha, 1 / (rank + 1));
|
|
409
|
+
});
|
|
410
|
+
const semanticScores = new Map;
|
|
411
|
+
if (options.queryEmbedding && options.queryModelId && options.queryModelId !== "off") {
|
|
412
|
+
const embeddings = getSearchEmbeddings(db, projectPath, options.queryModelId);
|
|
413
|
+
for (const [sha, embedding] of embeddings.entries()) {
|
|
414
|
+
const similarity = clamp01(cosineSimilarity(options.queryEmbedding, embedding));
|
|
415
|
+
if (similarity > 0) {
|
|
416
|
+
semanticScores.set(sha, similarity);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const bySha = new Map;
|
|
421
|
+
for (const commit of ftsCandidates)
|
|
422
|
+
bySha.set(commit.sha, commit);
|
|
423
|
+
const semanticOnlyShas = [...semanticScores.keys()].filter((sha) => !bySha.has(sha));
|
|
424
|
+
if (semanticOnlyShas.length > 0) {
|
|
425
|
+
const rows = getBySHAsStatement(db, dated).all(projectPath, ...dated ? [from, to] : [], JSON.stringify(semanticOnlyShas));
|
|
426
|
+
for (const row of rows)
|
|
427
|
+
bySha.set(row.sha, rowToCommit(row));
|
|
428
|
+
}
|
|
429
|
+
const results = [];
|
|
430
|
+
for (const [sha, commit] of bySha.entries()) {
|
|
431
|
+
const sem = semanticScores.get(sha);
|
|
432
|
+
const fts = ftsScores.get(sha);
|
|
433
|
+
let score = 0;
|
|
434
|
+
let matchType = "fts";
|
|
435
|
+
if (sem !== undefined && fts !== undefined) {
|
|
436
|
+
score = semanticWeight * sem + ftsWeight * fts;
|
|
437
|
+
matchType = "hybrid";
|
|
438
|
+
} else if (sem !== undefined) {
|
|
439
|
+
score = sem * singleSourcePenalty;
|
|
440
|
+
matchType = "semantic";
|
|
441
|
+
} else if (fts !== undefined) {
|
|
442
|
+
score = fts * singleSourcePenalty;
|
|
443
|
+
matchType = "fts";
|
|
444
|
+
}
|
|
445
|
+
if (score <= 0)
|
|
446
|
+
continue;
|
|
447
|
+
results.push({ commit, score, matchType });
|
|
448
|
+
}
|
|
449
|
+
results.sort((left, right) => {
|
|
450
|
+
if (right.score !== left.score)
|
|
451
|
+
return right.score - left.score;
|
|
452
|
+
return right.commit.committedAtMs - left.commit.committedAtMs;
|
|
453
|
+
});
|
|
454
|
+
return results.slice(0, options.limit);
|
|
455
|
+
}
|
|
456
|
+
// src/features/magic-context/literal-probes.ts
|
|
457
|
+
var MAX_PROBES = 5;
|
|
458
|
+
var MIN_PROBE_LENGTH = 3;
|
|
459
|
+
var SLASH_COMMAND_RE = /\/[a-z][a-z0-9]*(?:-[a-z0-9]+)+/gi;
|
|
460
|
+
var KEBAB_SNAKE_RE = /[a-z][a-z0-9]*(?:[-_][a-z0-9]+)+/gi;
|
|
461
|
+
var DOTTED_RE = /[a-z0-9][a-z0-9_-]*(?:\.[a-z0-9_-]+)+/gi;
|
|
462
|
+
var CAMEL_RE = /\b[a-zA-Z][a-z0-9]*(?:[A-Z][a-z0-9]*)+\b/g;
|
|
463
|
+
var SHA_RE = /\b[0-9a-f]{7,40}\b/gi;
|
|
464
|
+
var ERROR_CODE_RE = /\b(?:TS\d{4,}|ERR_[A-Z][A-Z0-9_]*)\b/g;
|
|
465
|
+
var QUOTED_RE = /["`]([^"`]{3,80})["`]/g;
|
|
466
|
+
function looksLikeSha(token) {
|
|
467
|
+
return /[0-9]/.test(token) && /^[0-9a-f]{7,40}$/i.test(token);
|
|
468
|
+
}
|
|
469
|
+
function extractLiteralProbes(query) {
|
|
470
|
+
const trimmed = query.trim();
|
|
471
|
+
if (trimmed.length === 0)
|
|
472
|
+
return [];
|
|
473
|
+
const ordered = [];
|
|
474
|
+
const seen = new Set;
|
|
475
|
+
const add = (raw) => {
|
|
476
|
+
if (!raw)
|
|
477
|
+
return;
|
|
478
|
+
const probe = raw.trim();
|
|
479
|
+
if (probe.length < MIN_PROBE_LENGTH)
|
|
480
|
+
return;
|
|
481
|
+
const key = probe.toLowerCase();
|
|
482
|
+
if (seen.has(key))
|
|
483
|
+
return;
|
|
484
|
+
seen.add(key);
|
|
485
|
+
ordered.push(probe);
|
|
486
|
+
};
|
|
487
|
+
for (const m of trimmed.matchAll(QUOTED_RE))
|
|
488
|
+
add(m[1]);
|
|
489
|
+
for (const m of trimmed.matchAll(SLASH_COMMAND_RE))
|
|
490
|
+
add(m[0]);
|
|
491
|
+
for (const m of trimmed.matchAll(ERROR_CODE_RE))
|
|
492
|
+
add(m[0]);
|
|
493
|
+
for (const m of trimmed.matchAll(DOTTED_RE))
|
|
494
|
+
add(m[0]);
|
|
495
|
+
for (const m of trimmed.matchAll(KEBAB_SNAKE_RE))
|
|
496
|
+
add(m[0]);
|
|
497
|
+
for (const m of trimmed.matchAll(CAMEL_RE))
|
|
498
|
+
add(m[0]);
|
|
499
|
+
for (const m of trimmed.matchAll(SHA_RE)) {
|
|
500
|
+
if (looksLikeSha(m[0]))
|
|
501
|
+
add(m[0]);
|
|
502
|
+
}
|
|
503
|
+
return ordered.slice(0, MAX_PROBES);
|
|
504
|
+
}
|
|
505
|
+
function containsProbeVerbatim(text, probes) {
|
|
506
|
+
if (probes.length === 0)
|
|
507
|
+
return false;
|
|
508
|
+
const haystack = text.toLowerCase();
|
|
509
|
+
return probes.some((probe) => haystack.includes(probe.toLowerCase()));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/features/magic-context/message-fts-session-filter.ts
|
|
513
|
+
var MESSAGE_FTS_SESSION_FILTER_SQL = `+message_history_fts.rowid IN (
|
|
514
|
+
SELECT fts_rowid FROM message_fts_rowid_map WHERE session_id = ?1
|
|
515
|
+
) AND `;
|
|
516
|
+
var coverageCaches = new WeakMap;
|
|
517
|
+
function getCoverageCache(db) {
|
|
518
|
+
let cache = coverageCaches.get(db);
|
|
519
|
+
if (!cache) {
|
|
520
|
+
cache = {
|
|
521
|
+
pin: db.prepare("SELECT fts_rowid FROM message_fts_rowid_map LIMIT 1"),
|
|
522
|
+
stamp: db.prepare("SELECT total_changes() AS changes, data_version AS version FROM pragma_data_version"),
|
|
523
|
+
missing: db.prepare(`SELECT DISTINCT session_id AS sessionId
|
|
524
|
+
FROM message_history_fts WHERE rowid IN (
|
|
525
|
+
SELECT id FROM message_history_fts_docsize
|
|
526
|
+
EXCEPT SELECT fts_rowid FROM message_fts_rowid_map
|
|
527
|
+
)`)
|
|
528
|
+
};
|
|
529
|
+
coverageCaches.set(db, cache);
|
|
530
|
+
}
|
|
531
|
+
return cache;
|
|
532
|
+
}
|
|
533
|
+
function withMessageFtsSessionFilter(db, sessionId, read) {
|
|
534
|
+
let cacheable = false;
|
|
535
|
+
try {
|
|
536
|
+
const transaction = db;
|
|
537
|
+
cacheable = transaction.inTransaction === false || transaction.isTransaction === false;
|
|
538
|
+
if (!cacheable)
|
|
539
|
+
coverageCaches.delete(db);
|
|
540
|
+
} catch {
|
|
541
|
+
coverageCaches.delete(db);
|
|
542
|
+
}
|
|
543
|
+
return db.transaction(() => {
|
|
544
|
+
let sessionFirst = false;
|
|
545
|
+
try {
|
|
546
|
+
const cache = getCoverageCache(db);
|
|
547
|
+
cache.pin.get();
|
|
548
|
+
const stamp = cache.stamp.get();
|
|
549
|
+
let incomplete = cache.incompleteSessions;
|
|
550
|
+
if (!cacheable || !incomplete || cache.version !== stamp.version || cache.changes !== stamp.changes) {
|
|
551
|
+
const rows = cache.missing.all();
|
|
552
|
+
incomplete = new Set(rows.map((row) => row.sessionId));
|
|
553
|
+
if (cacheable) {
|
|
554
|
+
cache.version = stamp.version;
|
|
555
|
+
cache.changes = stamp.changes;
|
|
556
|
+
cache.incompleteSessions = incomplete;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
sessionFirst = !incomplete.has(sessionId);
|
|
560
|
+
} catch {
|
|
561
|
+
coverageCaches.delete(db);
|
|
562
|
+
}
|
|
563
|
+
return read(sessionFirst);
|
|
564
|
+
}).deferred();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/features/magic-context/search-measurement.ts
|
|
568
|
+
import { createHash } from "node:crypto";
|
|
569
|
+
function resultId(result) {
|
|
570
|
+
switch (result.source) {
|
|
571
|
+
case "memory":
|
|
572
|
+
return `memory:${result.memoryId}`;
|
|
573
|
+
case "message":
|
|
574
|
+
return `message:${result.messageId}`;
|
|
575
|
+
case "compartment":
|
|
576
|
+
return `chunk:${result.compartmentId}`;
|
|
577
|
+
case "git_commit":
|
|
578
|
+
return `commit:${result.sha}`;
|
|
579
|
+
case "primer":
|
|
580
|
+
return `primer:${result.primerId}`;
|
|
581
|
+
case "note":
|
|
582
|
+
return `note:${result.noteId}`;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async function recordShadowMeasurement(args) {
|
|
586
|
+
try {
|
|
587
|
+
const shadowCohort = getShadowEmbeddingMeasurementCohort(args.projectPath);
|
|
588
|
+
if (!shadowCohort)
|
|
589
|
+
return;
|
|
590
|
+
const primaryCohort = getPrimaryEmbeddingMeasurementCohort(args.projectPath);
|
|
591
|
+
const shadowStartedAt = Date.now();
|
|
592
|
+
let shadowFailed = false;
|
|
593
|
+
let shadowResults = [];
|
|
594
|
+
try {
|
|
595
|
+
const vector = await embedShadowTextForProject(args.projectPath, args.query);
|
|
596
|
+
if (!vector) {
|
|
597
|
+
shadowFailed = true;
|
|
598
|
+
} else {
|
|
599
|
+
shadowResults = await args.search(args.db, args.sessionId, args.projectPath, args.query, {
|
|
600
|
+
...args.options,
|
|
601
|
+
embedQuery: async () => vector,
|
|
602
|
+
isEmbeddingRuntimeEnabled: () => true,
|
|
603
|
+
embeddingEnabled: true,
|
|
604
|
+
embeddingModelIdOverride: shadowCohort.modelId,
|
|
605
|
+
chunkModelIdOverride: shadowCohort.chunkModelId,
|
|
606
|
+
measurementDisabled: true,
|
|
607
|
+
countRetrievals: false
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
} catch {
|
|
611
|
+
shadowFailed = true;
|
|
612
|
+
}
|
|
613
|
+
const primaryModelId = args.primaryQuery?.modelId ?? primaryCohort?.modelId ?? "";
|
|
614
|
+
const primaryFingerprint = primaryCohort?.fingerprint ?? "";
|
|
615
|
+
const primaryEpoch = primaryCohort?.epoch ?? 0;
|
|
616
|
+
const cohortKey = JSON.stringify({
|
|
617
|
+
primaryFingerprint,
|
|
618
|
+
primaryEpoch,
|
|
619
|
+
shadowFingerprint: shadowCohort.fingerprint,
|
|
620
|
+
shadowEpoch: shadowCohort.epoch
|
|
621
|
+
});
|
|
622
|
+
const primaryIds = args.primaryResults.map(resultId);
|
|
623
|
+
const shadowIds = shadowResults.map(resultId);
|
|
624
|
+
const corpusHash = sha256(JSON.stringify({ query: args.query, primaryIds, shadowIds }));
|
|
625
|
+
recordEmbeddingMeasurement(args.db, {
|
|
626
|
+
sessionId: args.sessionId,
|
|
627
|
+
projectPath: args.projectPath,
|
|
628
|
+
queryText: args.query,
|
|
629
|
+
cohortKey,
|
|
630
|
+
primaryResultIds: primaryIds,
|
|
631
|
+
shadowResultIds: shadowIds,
|
|
632
|
+
primaryLatencyMs: args.primaryLatencyMs,
|
|
633
|
+
shadowLatencyMs: Date.now() - shadowStartedAt,
|
|
634
|
+
primaryFailed: args.primaryQuery === null,
|
|
635
|
+
shadowFailed,
|
|
636
|
+
primaryModelId,
|
|
637
|
+
shadowModelId: shadowCohort.modelId,
|
|
638
|
+
primaryFingerprint,
|
|
639
|
+
shadowFingerprint: shadowCohort.fingerprint,
|
|
640
|
+
primaryEpoch,
|
|
641
|
+
shadowEpoch: shadowCohort.epoch,
|
|
642
|
+
corpusHash,
|
|
643
|
+
coverage: {
|
|
644
|
+
primaryResultCount: primaryIds.length,
|
|
645
|
+
shadowResultCount: shadowIds.length,
|
|
646
|
+
primaryAnswered: args.primaryQuery !== null,
|
|
647
|
+
shadowAnswered: !shadowFailed
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
} catch (error) {
|
|
651
|
+
log("[magic-context] shadow embedding measurement failed:", error);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
function sha256(value) {
|
|
655
|
+
return createHash("sha256").update(value).digest("hex");
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/features/magic-context/search.ts
|
|
659
|
+
var DEFAULT_UNIFIED_SEARCH_LIMIT = 10;
|
|
660
|
+
var MAX_UNIFIED_SEARCH_LIMIT = 25;
|
|
661
|
+
var FTS_SEMANTIC_CANDIDATE_LIMIT = 50;
|
|
662
|
+
var SEMANTIC_WEIGHT = 0.7;
|
|
663
|
+
var FTS_WEIGHT = 0.3;
|
|
664
|
+
var SINGLE_SOURCE_PENALTY = 0.8;
|
|
665
|
+
var RESULT_PREVIEW_LIMIT = 220;
|
|
666
|
+
var MEMORY_SOURCE_BOOST = 1.3;
|
|
667
|
+
var MESSAGE_SOURCE_BOOST = 1.275;
|
|
668
|
+
var GIT_COMMIT_SOURCE_BOOST = 1.2;
|
|
669
|
+
var PRIMER_SOURCE_BOOST = 1.25;
|
|
670
|
+
var messageSearchStatements = new WeakMap;
|
|
671
|
+
var sessionFirstMessageSearchStatements = new WeakMap;
|
|
672
|
+
var messageSearchStatementsWithCutoff = new WeakMap;
|
|
673
|
+
var sessionFirstMessageSearchStatementsWithCutoff = new WeakMap;
|
|
674
|
+
var messageSearchStatementsWithDateRange = new WeakMap;
|
|
675
|
+
var messageSearchDiagnosticStatements = new WeakMap;
|
|
676
|
+
var sessionFirstMessageSearchDiagnosticStatements = new WeakMap;
|
|
677
|
+
var messageSearchDiagnosticStatementsWithDateRange = new WeakMap;
|
|
678
|
+
var batchedMessageSearchStatements = new WeakMap;
|
|
679
|
+
var batchedFtsCountStatements = new WeakMap;
|
|
680
|
+
function createUnifiedSearchDiagnostics() {
|
|
681
|
+
return {
|
|
682
|
+
suppressedVisibleMemoryIds: [],
|
|
683
|
+
suppressedLiveMessageMatches: 0,
|
|
684
|
+
gitCommitUnavailable: null
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function normalizeLimit(limit) {
|
|
688
|
+
if (typeof limit !== "number" || !Number.isFinite(limit)) {
|
|
689
|
+
return DEFAULT_UNIFIED_SEARCH_LIMIT;
|
|
690
|
+
}
|
|
691
|
+
return Math.min(MAX_UNIFIED_SEARCH_LIMIT, Math.max(1, Math.floor(limit)));
|
|
692
|
+
}
|
|
693
|
+
function normalizeDateRange(from, to) {
|
|
694
|
+
if (from === undefined && to === undefined)
|
|
695
|
+
return null;
|
|
696
|
+
return {
|
|
697
|
+
from: typeof from === "number" && Number.isSafeInteger(from) ? from : Number.MIN_SAFE_INTEGER,
|
|
698
|
+
to: typeof to === "number" && Number.isSafeInteger(to) ? to : Number.MAX_SAFE_INTEGER
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
function timestampIsInRange(timestamp, range) {
|
|
702
|
+
return range === null || timestamp >= range.from && timestamp <= range.to;
|
|
703
|
+
}
|
|
704
|
+
var ID_SHAPED_QUERY_MAX_TOKENS = 5;
|
|
705
|
+
var ID_SHAPED_TOKEN = /^#?\d+$/;
|
|
706
|
+
function parseIdShapedQuery(query) {
|
|
707
|
+
const trimmed = query.trim();
|
|
708
|
+
if (trimmed.length === 0) {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
const tokens = trimmed.split(/[\s,]+/).filter((token) => token.length > 0);
|
|
712
|
+
if (tokens.length === 0 || tokens.length > ID_SHAPED_QUERY_MAX_TOKENS) {
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
const ids = [];
|
|
716
|
+
for (const token of tokens) {
|
|
717
|
+
if (!ID_SHAPED_TOKEN.test(token)) {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
const parsed = Number.parseInt(token.replace(/^#/, ""), 10);
|
|
721
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
ids.push(parsed);
|
|
725
|
+
}
|
|
726
|
+
return ids;
|
|
727
|
+
}
|
|
728
|
+
function normalizeCosineScore(score) {
|
|
729
|
+
if (!Number.isFinite(score)) {
|
|
730
|
+
return 0;
|
|
731
|
+
}
|
|
732
|
+
return Math.min(1, Math.max(0, score));
|
|
733
|
+
}
|
|
734
|
+
function previewText(text) {
|
|
735
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
736
|
+
if (normalized.length <= RESULT_PREVIEW_LIMIT) {
|
|
737
|
+
return normalized;
|
|
738
|
+
}
|
|
739
|
+
return `${normalized.slice(0, RESULT_PREVIEW_LIMIT - 1).trimEnd()}…`;
|
|
740
|
+
}
|
|
741
|
+
function resolveSearchWorkspaceContext(db, projectPath, identitySet) {
|
|
742
|
+
const resolved = identitySet ?? resolveWorkspaceIdentitySet(db, projectPath);
|
|
743
|
+
const isWorkspaced = resolved.identities.length > 1;
|
|
744
|
+
const expanded = expandWorkspaceIdentitySetWithAliases(db, resolved.identities);
|
|
745
|
+
const expandedIdentities = isWorkspaced ? expanded.expandedIdentities : resolved.identities;
|
|
746
|
+
const canonicalIdentityByStoredPath = isWorkspaced ? expanded.canonicalIdentityByStoredPath : new Map(resolved.identities.map((identity) => [identity, identity]));
|
|
747
|
+
const ownIdentities = expandedIdentities.filter((identity) => canonicalIdentityByStoredPath.get(identity) === projectPath);
|
|
748
|
+
return {
|
|
749
|
+
identities: resolved.identities,
|
|
750
|
+
expandedIdentities,
|
|
751
|
+
ownIdentities,
|
|
752
|
+
shareCategories: isWorkspaced ? resolveWorkspaceShareCategories(db, projectPath) : null,
|
|
753
|
+
namesByIdentity: resolved.namesByIdentity,
|
|
754
|
+
canonicalIdentityByStoredPath,
|
|
755
|
+
isWorkspaced
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function memoryWorkspaceIdentity(memory, workspace) {
|
|
759
|
+
return resolveStoredPathWorkspaceIdentity(memory.projectPath, workspace.identities, workspace.canonicalIdentityByStoredPath);
|
|
760
|
+
}
|
|
761
|
+
function sourceNamesForSearchMemories(args) {
|
|
762
|
+
if (!args.workspace.isWorkspaced)
|
|
763
|
+
return;
|
|
764
|
+
const sourceNames = new Map;
|
|
765
|
+
for (const memory of args.memories) {
|
|
766
|
+
const source = sourceNameForMemory(memory.projectPath, args.projectPath, args.workspace.identities, args.workspace.namesByIdentity, args.workspace.canonicalIdentityByStoredPath);
|
|
767
|
+
if (source)
|
|
768
|
+
sourceNames.set(memory.id, source);
|
|
769
|
+
}
|
|
770
|
+
return sourceNames.size > 0 ? sourceNames : undefined;
|
|
771
|
+
}
|
|
772
|
+
function getMessageSearchStatement(db, sessionFirst) {
|
|
773
|
+
const statements = sessionFirst ? sessionFirstMessageSearchStatements : messageSearchStatements;
|
|
774
|
+
let stmt = statements.get(db);
|
|
775
|
+
if (!stmt) {
|
|
776
|
+
stmt = db.prepare(`SELECT message_ordinal AS messageOrdinal, message_id AS messageId, role, content FROM message_history_fts WHERE ${sessionFirst ? MESSAGE_FTS_SESSION_FILTER_SQL : ""}session_id = ?1 AND message_history_fts MATCH ? ORDER BY bm25(message_history_fts), CAST(message_ordinal AS INTEGER) ASC LIMIT ?`);
|
|
777
|
+
statements.set(db, stmt);
|
|
778
|
+
}
|
|
779
|
+
return stmt;
|
|
780
|
+
}
|
|
781
|
+
function getMessageSearchStatementWithCutoff(db, sessionFirst) {
|
|
782
|
+
const statements = sessionFirst ? sessionFirstMessageSearchStatementsWithCutoff : messageSearchStatementsWithCutoff;
|
|
783
|
+
let stmt = statements.get(db);
|
|
784
|
+
if (!stmt) {
|
|
785
|
+
stmt = db.prepare(`SELECT message_ordinal AS messageOrdinal, message_id AS messageId, role, content FROM message_history_fts WHERE ${sessionFirst ? MESSAGE_FTS_SESSION_FILTER_SQL : ""}session_id = ?1 AND message_history_fts MATCH ? AND CAST(message_ordinal AS INTEGER) <= ? ORDER BY bm25(message_history_fts), CAST(message_ordinal AS INTEGER) ASC LIMIT ?`);
|
|
786
|
+
statements.set(db, stmt);
|
|
787
|
+
}
|
|
788
|
+
return stmt;
|
|
789
|
+
}
|
|
790
|
+
function getMessageSearchStatementWithDateRange(db, withCutoff) {
|
|
791
|
+
let statements = messageSearchStatementsWithDateRange.get(db);
|
|
792
|
+
if (!statements) {
|
|
793
|
+
statements = new Map;
|
|
794
|
+
messageSearchStatementsWithDateRange.set(db, statements);
|
|
795
|
+
}
|
|
796
|
+
const key = withCutoff ? "cutoff" : "all";
|
|
797
|
+
let statement = statements.get(key);
|
|
798
|
+
if (!statement) {
|
|
799
|
+
statement = db.prepare(`SELECT message_history_fts.message_ordinal AS messageOrdinal,
|
|
800
|
+
message_history_fts.message_id AS messageId,
|
|
801
|
+
message_history_fts.role AS role,
|
|
802
|
+
message_history_fts.content AS content
|
|
803
|
+
FROM message_history_fts
|
|
804
|
+
JOIN message_fts_rowid_map AS map
|
|
805
|
+
ON map.session_id = message_history_fts.session_id
|
|
806
|
+
AND map.fts_rowid = message_history_fts.rowid
|
|
807
|
+
WHERE ${MESSAGE_FTS_SESSION_FILTER_SQL}message_history_fts.session_id = ?1
|
|
808
|
+
AND message_history_fts MATCH ?
|
|
809
|
+
AND map.message_time_ms BETWEEN ? AND ?
|
|
810
|
+
${withCutoff ? "AND CAST(message_history_fts.message_ordinal AS INTEGER) <= ?" : ""}
|
|
811
|
+
ORDER BY bm25(message_history_fts),
|
|
812
|
+
CAST(message_history_fts.message_ordinal AS INTEGER) ASC
|
|
813
|
+
LIMIT ?`);
|
|
814
|
+
statements.set(key, statement);
|
|
815
|
+
}
|
|
816
|
+
return statement;
|
|
817
|
+
}
|
|
818
|
+
function getMessageSearchDiagnosticStatement(db, sessionFirst) {
|
|
819
|
+
const statements = sessionFirst ? sessionFirstMessageSearchDiagnosticStatements : messageSearchDiagnosticStatements;
|
|
820
|
+
let stmt = statements.get(db);
|
|
821
|
+
if (!stmt) {
|
|
822
|
+
stmt = db.prepare(`
|
|
823
|
+
WITH matches AS MATERIALIZED (
|
|
824
|
+
SELECT
|
|
825
|
+
message_ordinal AS messageOrdinal,
|
|
826
|
+
message_id AS messageId,
|
|
827
|
+
role,
|
|
828
|
+
content,
|
|
829
|
+
CAST(message_ordinal AS INTEGER) AS ordinalValue,
|
|
830
|
+
bm25(message_history_fts) AS ftsRank
|
|
831
|
+
FROM message_history_fts
|
|
832
|
+
WHERE ${sessionFirst ? MESSAGE_FTS_SESSION_FILTER_SQL : ""}session_id = ?1 AND message_history_fts MATCH ?
|
|
833
|
+
),
|
|
834
|
+
eligible AS (
|
|
835
|
+
SELECT * FROM matches
|
|
836
|
+
WHERE ordinalValue <= ?
|
|
837
|
+
ORDER BY ftsRank, ordinalValue ASC
|
|
838
|
+
LIMIT ?
|
|
839
|
+
),
|
|
840
|
+
summary AS (
|
|
841
|
+
SELECT COUNT(*) AS suppressedCount
|
|
842
|
+
FROM matches
|
|
843
|
+
WHERE ordinalValue > ?
|
|
844
|
+
)
|
|
845
|
+
SELECT
|
|
846
|
+
eligible.messageOrdinal,
|
|
847
|
+
eligible.messageId,
|
|
848
|
+
eligible.role,
|
|
849
|
+
eligible.content,
|
|
850
|
+
eligible.ftsRank,
|
|
851
|
+
summary.suppressedCount,
|
|
852
|
+
0 AS summaryOnly
|
|
853
|
+
FROM eligible CROSS JOIN summary
|
|
854
|
+
UNION ALL
|
|
855
|
+
SELECT NULL, NULL, NULL, NULL, NULL, summary.suppressedCount, 1
|
|
856
|
+
FROM summary
|
|
857
|
+
WHERE NOT EXISTS (SELECT 1 FROM eligible)
|
|
858
|
+
ORDER BY summaryOnly ASC, ftsRank ASC, messageOrdinal ASC
|
|
859
|
+
`);
|
|
860
|
+
statements.set(db, stmt);
|
|
861
|
+
}
|
|
862
|
+
return stmt;
|
|
863
|
+
}
|
|
864
|
+
function getMessageSearchDiagnosticStatementWithDateRange(db) {
|
|
865
|
+
let statement = messageSearchDiagnosticStatementsWithDateRange.get(db);
|
|
866
|
+
if (!statement) {
|
|
867
|
+
statement = db.prepare(`
|
|
868
|
+
WITH matches AS MATERIALIZED (
|
|
869
|
+
SELECT
|
|
870
|
+
message_history_fts.message_ordinal AS messageOrdinal,
|
|
871
|
+
message_history_fts.message_id AS messageId,
|
|
872
|
+
message_history_fts.role AS role,
|
|
873
|
+
message_history_fts.content AS content,
|
|
874
|
+
CAST(message_history_fts.message_ordinal AS INTEGER) AS ordinalValue,
|
|
875
|
+
bm25(message_history_fts) AS ftsRank
|
|
876
|
+
FROM message_history_fts
|
|
877
|
+
JOIN message_fts_rowid_map AS map
|
|
878
|
+
ON map.session_id = message_history_fts.session_id
|
|
879
|
+
AND map.fts_rowid = message_history_fts.rowid
|
|
880
|
+
WHERE ${MESSAGE_FTS_SESSION_FILTER_SQL}message_history_fts.session_id = ?1
|
|
881
|
+
AND message_history_fts MATCH ?
|
|
882
|
+
AND map.message_time_ms BETWEEN ? AND ?
|
|
883
|
+
),
|
|
884
|
+
eligible AS (
|
|
885
|
+
SELECT * FROM matches
|
|
886
|
+
WHERE ordinalValue <= ?
|
|
887
|
+
ORDER BY ftsRank, ordinalValue ASC
|
|
888
|
+
LIMIT ?
|
|
889
|
+
),
|
|
890
|
+
summary AS (
|
|
891
|
+
SELECT COUNT(*) AS suppressedCount
|
|
892
|
+
FROM matches
|
|
893
|
+
WHERE ordinalValue > ?
|
|
894
|
+
)
|
|
895
|
+
SELECT
|
|
896
|
+
eligible.messageOrdinal,
|
|
897
|
+
eligible.messageId,
|
|
898
|
+
eligible.role,
|
|
899
|
+
eligible.content,
|
|
900
|
+
eligible.ftsRank,
|
|
901
|
+
summary.suppressedCount,
|
|
902
|
+
0 AS summaryOnly
|
|
903
|
+
FROM eligible CROSS JOIN summary
|
|
904
|
+
UNION ALL
|
|
905
|
+
SELECT NULL, NULL, NULL, NULL, NULL, summary.suppressedCount, 1
|
|
906
|
+
FROM summary
|
|
907
|
+
WHERE NOT EXISTS (SELECT 1 FROM eligible)
|
|
908
|
+
ORDER BY summaryOnly ASC, ftsRank ASC, messageOrdinal ASC
|
|
909
|
+
`);
|
|
910
|
+
messageSearchDiagnosticStatementsWithDateRange.set(db, statement);
|
|
911
|
+
}
|
|
912
|
+
return statement;
|
|
913
|
+
}
|
|
914
|
+
function getBatchedFtsCountStatement(db, queryCount, cutoff, dateRange, sessionFirst) {
|
|
915
|
+
let statements = batchedFtsCountStatements.get(db);
|
|
916
|
+
if (!statements) {
|
|
917
|
+
statements = new Map;
|
|
918
|
+
batchedFtsCountStatements.set(db, statements);
|
|
919
|
+
}
|
|
920
|
+
const key = `${queryCount}:${cutoff === null ? "all" : "cutoff"}:${dateRange === null ? "all-dates" : "dated"}:${sessionFirst}`;
|
|
921
|
+
let statement = statements.get(key);
|
|
922
|
+
if (!statement) {
|
|
923
|
+
const cutoffSql = cutoff === null ? "" : " AND CAST(message_history_fts.message_ordinal AS INTEGER) <= ?";
|
|
924
|
+
const joinSql = dateRange === null ? "" : ` JOIN message_fts_rowid_map AS map
|
|
925
|
+
ON map.session_id = message_history_fts.session_id
|
|
926
|
+
AND map.fts_rowid = message_history_fts.rowid`;
|
|
927
|
+
const dateSql = dateRange === null ? "" : " AND map.message_time_ms BETWEEN ? AND ?";
|
|
928
|
+
statement = db.prepare(Array.from({ length: queryCount }, (_, index) => `SELECT ${index} AS queryIndex, COUNT(*) AS count
|
|
929
|
+
FROM message_history_fts${joinSql}
|
|
930
|
+
WHERE ${sessionFirst || dateRange !== null ? MESSAGE_FTS_SESSION_FILTER_SQL : ""}message_history_fts.session_id = ${index === 0 ? "?1" : "?"}
|
|
931
|
+
AND message_history_fts MATCH ?${dateSql}${cutoffSql}`).join(`
|
|
932
|
+
UNION ALL
|
|
933
|
+
`));
|
|
934
|
+
statements.set(key, statement);
|
|
935
|
+
}
|
|
936
|
+
return statement;
|
|
937
|
+
}
|
|
938
|
+
function contentOnlyMessageQuery(ftsQuery) {
|
|
939
|
+
return ftsQuery.length === 0 ? "" : `content : (${ftsQuery})`;
|
|
940
|
+
}
|
|
941
|
+
function countSessionFtsMatchesBatch(db, sessionId, ftsQueries, cutoff, dateRange, sessionFirst) {
|
|
942
|
+
if (ftsQueries.length === 0)
|
|
943
|
+
return [];
|
|
944
|
+
const bindings = [];
|
|
945
|
+
for (const query of ftsQueries) {
|
|
946
|
+
bindings.push(sessionId, contentOnlyMessageQuery(query));
|
|
947
|
+
if (dateRange !== null)
|
|
948
|
+
bindings.push(dateRange.from, dateRange.to);
|
|
949
|
+
if (cutoff !== null)
|
|
950
|
+
bindings.push(cutoff);
|
|
951
|
+
}
|
|
952
|
+
try {
|
|
953
|
+
const rows = getBatchedFtsCountStatement(db, ftsQueries.length, cutoff, dateRange, sessionFirst).all(...bindings);
|
|
954
|
+
const counts = Array.from({ length: ftsQueries.length }, () => 0);
|
|
955
|
+
for (const row of rows) {
|
|
956
|
+
if (typeof row.queryIndex === "number" && row.queryIndex >= 0 && row.queryIndex < counts.length && typeof row.count === "number") {
|
|
957
|
+
counts[row.queryIndex] = row.count;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return counts;
|
|
961
|
+
} catch {
|
|
962
|
+
return Array.from({ length: ftsQueries.length }, () => 0);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
function getMessageOrdinal(value) {
|
|
966
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
967
|
+
return value;
|
|
968
|
+
}
|
|
969
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
970
|
+
const parsed = Number.parseInt(value, 10);
|
|
971
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
972
|
+
}
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
async function getSemanticScores(args) {
|
|
976
|
+
const semanticScores = new Map;
|
|
977
|
+
if (!args.queryEmbedding || args.memories.length === 0 || !args.queryModelId || args.queryModelId === "off") {
|
|
978
|
+
return semanticScores;
|
|
979
|
+
}
|
|
980
|
+
if (!args.workspace?.isWorkspaced) {
|
|
981
|
+
const cachedEmbeddings = getProjectEmbeddings(args.db, args.projectPath, args.queryModelId);
|
|
982
|
+
const embeddings = args.backfillMemoryEmbeddings === false ? cachedEmbeddings : await ensureMemoryEmbeddings({
|
|
983
|
+
db: args.db,
|
|
984
|
+
projectIdentity: args.projectPath,
|
|
985
|
+
memories: args.memories,
|
|
986
|
+
existingEmbeddings: cachedEmbeddings
|
|
987
|
+
});
|
|
988
|
+
for (const memory of args.memories) {
|
|
989
|
+
const memoryEmbedding = embeddings.get(memory.id);
|
|
990
|
+
if (!memoryEmbedding) {
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
semanticScores.set(memory.id, normalizeCosineScore(cosineSimilarity(args.queryEmbedding, memoryEmbedding.embedding)));
|
|
994
|
+
}
|
|
995
|
+
return semanticScores;
|
|
996
|
+
}
|
|
997
|
+
const workspace = args.workspace;
|
|
998
|
+
const memoriesByIdentity = new Map;
|
|
999
|
+
for (const memory of args.memories) {
|
|
1000
|
+
const identity = memoryWorkspaceIdentity(memory, workspace);
|
|
1001
|
+
if (!identity)
|
|
1002
|
+
continue;
|
|
1003
|
+
const list = memoriesByIdentity.get(identity) ?? [];
|
|
1004
|
+
list.push(memory);
|
|
1005
|
+
memoriesByIdentity.set(identity, list);
|
|
1006
|
+
}
|
|
1007
|
+
const ownMemories = memoriesByIdentity.get(args.projectPath) ?? [];
|
|
1008
|
+
if (ownMemories.length > 0 && args.backfillMemoryEmbeddings !== false) {
|
|
1009
|
+
const ownEmbeddings = getProjectEmbeddings(args.db, args.projectPath, args.queryModelId);
|
|
1010
|
+
await ensureMemoryEmbeddings({
|
|
1011
|
+
db: args.db,
|
|
1012
|
+
projectIdentity: args.projectPath,
|
|
1013
|
+
memories: ownMemories,
|
|
1014
|
+
existingEmbeddings: ownEmbeddings
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
for (const identity of workspace.identities) {
|
|
1018
|
+
const memberMemories = memoriesByIdentity.get(identity) ?? [];
|
|
1019
|
+
if (memberMemories.length === 0)
|
|
1020
|
+
continue;
|
|
1021
|
+
const cachedEmbeddings = getProjectEmbeddings(args.db, identity, args.queryModelId);
|
|
1022
|
+
for (const memory of memberMemories) {
|
|
1023
|
+
const memoryEmbedding = cachedEmbeddings.get(memory.id);
|
|
1024
|
+
if (!memoryEmbedding || memoryEmbedding.modelId !== args.queryModelId)
|
|
1025
|
+
continue;
|
|
1026
|
+
semanticScores.set(memory.id, normalizeCosineScore(cosineSimilarity(args.queryEmbedding, memoryEmbedding.embedding)));
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
return semanticScores;
|
|
1030
|
+
}
|
|
1031
|
+
function getFtsMatches(args) {
|
|
1032
|
+
try {
|
|
1033
|
+
return args.workspace?.isWorkspaced ? searchMemoriesFTSUnion(args.db, args.workspace.expandedIdentities, args.query, args.limit, args.workspace.ownIdentities, args.workspace.shareCategories, args.dateRange) : searchMemoriesFTS(args.db, args.projectPath, args.query, args.limit, args.dateRange);
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
log(`[search] FTS query failed for "${args.query}": ${error instanceof Error ? error.message : String(error)}`);
|
|
1036
|
+
return [];
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function getFtsScores(matches) {
|
|
1040
|
+
return new Map(matches.map((memory, rank) => [memory.id, 1 / (rank + 1)]));
|
|
1041
|
+
}
|
|
1042
|
+
function selectSemanticCandidates(args) {
|
|
1043
|
+
if (args.ftsMatches.length === 0) {
|
|
1044
|
+
return args.memories;
|
|
1045
|
+
}
|
|
1046
|
+
const candidateIds = new Set(args.ftsMatches.map((memory) => memory.id));
|
|
1047
|
+
if (args.queryModelId && args.queryModelId !== "off") {
|
|
1048
|
+
const embeddingProjects = args.workspace?.isWorkspaced ? args.workspace.identities : [args.projectPath];
|
|
1049
|
+
for (const projectPath of embeddingProjects) {
|
|
1050
|
+
const storedEmbeddings = getProjectEmbeddings(args.db, projectPath, args.queryModelId);
|
|
1051
|
+
for (const memoryId of storedEmbeddings.keys()) {
|
|
1052
|
+
candidateIds.add(memoryId);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return args.memories.filter((memory) => candidateIds.has(memory.id));
|
|
1057
|
+
}
|
|
1058
|
+
function mergeMemoryResults(args) {
|
|
1059
|
+
const memoryById = new Map(args.memories.map((memory) => [memory.id, memory]));
|
|
1060
|
+
const candidateIds = new Set([...args.semanticScores.keys(), ...args.ftsScores.keys()]);
|
|
1061
|
+
const results = [];
|
|
1062
|
+
const suppressedVisibleIds = [];
|
|
1063
|
+
for (const id of candidateIds) {
|
|
1064
|
+
const memory = memoryById.get(id);
|
|
1065
|
+
if (!memory) {
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
const semanticScore = args.semanticScores.get(id);
|
|
1069
|
+
const ftsScore = args.ftsScores.get(id);
|
|
1070
|
+
let score = 0;
|
|
1071
|
+
let matchType = "fts";
|
|
1072
|
+
if (semanticScore !== undefined && ftsScore !== undefined) {
|
|
1073
|
+
score = SEMANTIC_WEIGHT * semanticScore + FTS_WEIGHT * ftsScore;
|
|
1074
|
+
matchType = "hybrid";
|
|
1075
|
+
} else if (semanticScore !== undefined) {
|
|
1076
|
+
score = semanticScore * SINGLE_SOURCE_PENALTY;
|
|
1077
|
+
matchType = "semantic";
|
|
1078
|
+
} else if (ftsScore !== undefined) {
|
|
1079
|
+
score = ftsScore * SINGLE_SOURCE_PENALTY;
|
|
1080
|
+
matchType = "fts";
|
|
1081
|
+
}
|
|
1082
|
+
if (score <= 0) {
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
if (args.visibleMemoryIds?.has(id)) {
|
|
1086
|
+
suppressedVisibleIds.push(id);
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
results.push({
|
|
1090
|
+
source: "memory",
|
|
1091
|
+
content: previewText(memory.content),
|
|
1092
|
+
score,
|
|
1093
|
+
memoryId: memory.id,
|
|
1094
|
+
category: memory.category,
|
|
1095
|
+
matchType,
|
|
1096
|
+
sourceName: args.sourceNameByMemoryId?.get(memory.id)
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
return {
|
|
1100
|
+
results: results.sort((left, right) => {
|
|
1101
|
+
if (right.score !== left.score) {
|
|
1102
|
+
return right.score - left.score;
|
|
1103
|
+
}
|
|
1104
|
+
return left.memoryId - right.memoryId;
|
|
1105
|
+
}).slice(0, args.limit),
|
|
1106
|
+
suppressedVisibleIds: suppressedVisibleIds.sort((left, right) => left - right)
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
async function searchMemories(args) {
|
|
1110
|
+
if (!args.memoryEnabled) {
|
|
1111
|
+
return { results: [], suppressedVisibleIds: [] };
|
|
1112
|
+
}
|
|
1113
|
+
const unfilteredMemories = args.workspace?.isWorkspaced ? getMemoriesByProjects(args.db, args.workspace.expandedIdentities, ["active", "permanent"], Date.now(), args.workspace.ownIdentities, args.workspace.shareCategories) : getMemoriesByProject(args.db, args.projectPath);
|
|
1114
|
+
const memories = unfilteredMemories.filter((memory) => timestampIsInRange(memory.createdAt, args.dateRange));
|
|
1115
|
+
if (memories.length === 0) {
|
|
1116
|
+
return { results: [], suppressedVisibleIds: [] };
|
|
1117
|
+
}
|
|
1118
|
+
const ftsMatches = getFtsMatches({
|
|
1119
|
+
db: args.db,
|
|
1120
|
+
projectPath: args.projectPath,
|
|
1121
|
+
query: args.query,
|
|
1122
|
+
limit: FTS_SEMANTIC_CANDIDATE_LIMIT,
|
|
1123
|
+
workspace: args.workspace,
|
|
1124
|
+
dateRange: args.dateRange
|
|
1125
|
+
});
|
|
1126
|
+
const ftsScores = getFtsScores(ftsMatches);
|
|
1127
|
+
const semanticCandidates = selectSemanticCandidates({
|
|
1128
|
+
db: args.db,
|
|
1129
|
+
memories,
|
|
1130
|
+
projectPath: args.projectPath,
|
|
1131
|
+
ftsMatches,
|
|
1132
|
+
queryModelId: args.queryModelId,
|
|
1133
|
+
workspace: args.workspace
|
|
1134
|
+
});
|
|
1135
|
+
const semanticScores = await getSemanticScores({
|
|
1136
|
+
db: args.db,
|
|
1137
|
+
projectPath: args.projectPath,
|
|
1138
|
+
memories: semanticCandidates,
|
|
1139
|
+
backfillMemoryEmbeddings: args.backfillMemoryEmbeddings,
|
|
1140
|
+
queryEmbedding: args.queryEmbedding,
|
|
1141
|
+
queryModelId: args.queryModelId,
|
|
1142
|
+
workspace: args.workspace
|
|
1143
|
+
});
|
|
1144
|
+
return mergeMemoryResults({
|
|
1145
|
+
memories,
|
|
1146
|
+
semanticScores,
|
|
1147
|
+
ftsScores,
|
|
1148
|
+
limit: args.limit,
|
|
1149
|
+
visibleMemoryIds: args.visibleMemoryIds,
|
|
1150
|
+
sourceNameByMemoryId: sourceNamesForSearchMemories({
|
|
1151
|
+
memories,
|
|
1152
|
+
projectPath: args.projectPath,
|
|
1153
|
+
workspace: args.workspace ?? {
|
|
1154
|
+
identities: [args.projectPath],
|
|
1155
|
+
expandedIdentities: [args.projectPath],
|
|
1156
|
+
namesByIdentity: new Map,
|
|
1157
|
+
canonicalIdentityByStoredPath: new Map([[args.projectPath, args.projectPath]]),
|
|
1158
|
+
ownIdentities: [args.projectPath],
|
|
1159
|
+
shareCategories: null,
|
|
1160
|
+
isWorkspaced: false
|
|
1161
|
+
}
|
|
1162
|
+
})
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
function linearDecayScore(rank, total) {
|
|
1166
|
+
if (total <= 0)
|
|
1167
|
+
return 0;
|
|
1168
|
+
return Math.max(0, 1 - rank / total);
|
|
1169
|
+
}
|
|
1170
|
+
function normalizeMessageSearchRow(row, cutoff) {
|
|
1171
|
+
const messageOrdinal = getMessageOrdinal(row.messageOrdinal);
|
|
1172
|
+
if (messageOrdinal === null || typeof row.messageId !== "string" || typeof row.role !== "string" || typeof row.content !== "string") {
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
if (cutoff !== null && messageOrdinal > cutoff)
|
|
1176
|
+
return null;
|
|
1177
|
+
return {
|
|
1178
|
+
messageOrdinal,
|
|
1179
|
+
messageId: row.messageId,
|
|
1180
|
+
role: row.role,
|
|
1181
|
+
content: row.content
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
function runMessageFtsQuery(db, sessionId, ftsQuery, fetchLimit, cutoff, dateRange, sessionFirst) {
|
|
1185
|
+
if (ftsQuery.length === 0)
|
|
1186
|
+
return [];
|
|
1187
|
+
let rawRows;
|
|
1188
|
+
const matchQuery = contentOnlyMessageQuery(ftsQuery);
|
|
1189
|
+
if (dateRange !== null) {
|
|
1190
|
+
const bindings = [sessionId, matchQuery, dateRange.from, dateRange.to];
|
|
1191
|
+
if (cutoff !== null)
|
|
1192
|
+
bindings.push(cutoff);
|
|
1193
|
+
bindings.push(fetchLimit);
|
|
1194
|
+
rawRows = getMessageSearchStatementWithDateRange(db, cutoff !== null).all(...bindings);
|
|
1195
|
+
} else {
|
|
1196
|
+
rawRows = cutoff !== null ? getMessageSearchStatementWithCutoff(db, sessionFirst).all(sessionId, matchQuery, cutoff, fetchLimit) : getMessageSearchStatement(db, sessionFirst).all(sessionId, matchQuery, fetchLimit);
|
|
1197
|
+
}
|
|
1198
|
+
const rows = rawRows.map((row) => row);
|
|
1199
|
+
const result = [];
|
|
1200
|
+
for (const row of rows) {
|
|
1201
|
+
const normalized = normalizeMessageSearchRow(row, cutoff);
|
|
1202
|
+
if (normalized)
|
|
1203
|
+
result.push(normalized);
|
|
1204
|
+
}
|
|
1205
|
+
return result;
|
|
1206
|
+
}
|
|
1207
|
+
function runMessageFtsQueryWithDiagnostics(args) {
|
|
1208
|
+
if (args.ftsQuery.length === 0)
|
|
1209
|
+
return { rows: [], suppressedCount: 0 };
|
|
1210
|
+
const matchQuery = contentOnlyMessageQuery(args.ftsQuery);
|
|
1211
|
+
const rawRows = (args.dateRange === null ? getMessageSearchDiagnosticStatement(args.db, args.sessionFirst).all(args.sessionId, matchQuery, args.cutoff, args.fetchLimit, args.cutoff) : getMessageSearchDiagnosticStatementWithDateRange(args.db).all(args.sessionId, matchQuery, args.dateRange.from, args.dateRange.to, args.cutoff, args.fetchLimit, args.cutoff)).map((row) => row);
|
|
1212
|
+
const suppressedCount = rawRows[0]?.suppressedCount ?? 0;
|
|
1213
|
+
const rows = [];
|
|
1214
|
+
for (const row of rawRows) {
|
|
1215
|
+
if (row.summaryOnly === 1)
|
|
1216
|
+
continue;
|
|
1217
|
+
const normalized = normalizeMessageSearchRow(row, args.cutoff);
|
|
1218
|
+
if (normalized)
|
|
1219
|
+
rows.push(normalized);
|
|
1220
|
+
}
|
|
1221
|
+
return { rows, suppressedCount };
|
|
1222
|
+
}
|
|
1223
|
+
function getBatchedMessageSearchStatement(db, queryCount, cutoff, dateRange, sessionFirst) {
|
|
1224
|
+
let statements = batchedMessageSearchStatements.get(db);
|
|
1225
|
+
if (!statements) {
|
|
1226
|
+
statements = new Map;
|
|
1227
|
+
batchedMessageSearchStatements.set(db, statements);
|
|
1228
|
+
}
|
|
1229
|
+
const key = `${queryCount}:${cutoff === null ? "all" : "cutoff"}:${dateRange === null ? "all-dates" : "dated"}:${sessionFirst}`;
|
|
1230
|
+
let statement = statements.get(key);
|
|
1231
|
+
if (!statement) {
|
|
1232
|
+
const cutoffSql = cutoff === null ? "" : " AND CAST(message_history_fts.message_ordinal AS INTEGER) <= ?";
|
|
1233
|
+
const joinSql = dateRange === null ? "" : ` JOIN message_fts_rowid_map AS map
|
|
1234
|
+
ON map.session_id = message_history_fts.session_id
|
|
1235
|
+
AND map.fts_rowid = message_history_fts.rowid`;
|
|
1236
|
+
const dateSql = dateRange === null ? "" : " AND map.message_time_ms BETWEEN ? AND ?";
|
|
1237
|
+
const branches = Array.from({ length: queryCount }, (_, index) => `SELECT * FROM (
|
|
1238
|
+
SELECT ${index} AS queryIndex,
|
|
1239
|
+
message_history_fts.message_ordinal AS messageOrdinal,
|
|
1240
|
+
message_history_fts.message_id AS messageId,
|
|
1241
|
+
message_history_fts.role AS role,
|
|
1242
|
+
message_history_fts.content AS content,
|
|
1243
|
+
bm25(message_history_fts) AS ftsRank
|
|
1244
|
+
FROM message_history_fts${joinSql}
|
|
1245
|
+
WHERE ${sessionFirst || dateRange !== null ? MESSAGE_FTS_SESSION_FILTER_SQL : ""}message_history_fts.session_id = ${index === 0 ? "?1" : "?"}
|
|
1246
|
+
AND message_history_fts MATCH ?${dateSql}${cutoffSql}
|
|
1247
|
+
ORDER BY ftsRank
|
|
1248
|
+
LIMIT ?
|
|
1249
|
+
)`);
|
|
1250
|
+
statement = db.prepare(`${branches.join(`
|
|
1251
|
+
UNION ALL
|
|
1252
|
+
`)}
|
|
1253
|
+
ORDER BY queryIndex ASC, ftsRank ASC`);
|
|
1254
|
+
statements.set(key, statement);
|
|
1255
|
+
}
|
|
1256
|
+
return statement;
|
|
1257
|
+
}
|
|
1258
|
+
function runMessageFtsQueriesBatch(db, sessionId, ftsQueries, fetchLimit, cutoff, dateRange, sessionFirst) {
|
|
1259
|
+
if (ftsQueries.length === 0)
|
|
1260
|
+
return [];
|
|
1261
|
+
const bindings = [];
|
|
1262
|
+
for (const query of ftsQueries) {
|
|
1263
|
+
bindings.push(sessionId, contentOnlyMessageQuery(query));
|
|
1264
|
+
if (dateRange !== null)
|
|
1265
|
+
bindings.push(dateRange.from, dateRange.to);
|
|
1266
|
+
if (cutoff !== null)
|
|
1267
|
+
bindings.push(cutoff);
|
|
1268
|
+
bindings.push(fetchLimit);
|
|
1269
|
+
}
|
|
1270
|
+
const rows = getBatchedMessageSearchStatement(db, ftsQueries.length, cutoff, dateRange, sessionFirst).all(...bindings);
|
|
1271
|
+
const result = Array.from({ length: ftsQueries.length }, () => []);
|
|
1272
|
+
for (const row of rows) {
|
|
1273
|
+
if (typeof row.queryIndex !== "number" || row.queryIndex < 0 || row.queryIndex >= result.length) {
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
const normalized = normalizeMessageSearchRow(row, cutoff);
|
|
1277
|
+
if (normalized)
|
|
1278
|
+
result[row.queryIndex].push(normalized);
|
|
1279
|
+
}
|
|
1280
|
+
return result;
|
|
1281
|
+
}
|
|
1282
|
+
var RRF_K = 60;
|
|
1283
|
+
var VERBATIM_RANK_BONUS = 1 / RRF_K;
|
|
1284
|
+
var IDF_FALLOFF = 100;
|
|
1285
|
+
function probeDiscriminationWeight(df, corpusSize) {
|
|
1286
|
+
if (corpusSize <= 0 || df <= 0)
|
|
1287
|
+
return 1;
|
|
1288
|
+
return 1 / (1 + IDF_FALLOFF * df / corpusSize);
|
|
1289
|
+
}
|
|
1290
|
+
function searchMessages(args) {
|
|
1291
|
+
return withMessageFtsSessionFilter(args.db, args.sessionId, (sessionFirst) => searchMessagesInSnapshot(args, sessionFirst));
|
|
1292
|
+
}
|
|
1293
|
+
function searchMessagesInSnapshot(args, sessionFirst) {
|
|
1294
|
+
const cutoff = args.maxOrdinal != null && args.maxOrdinal >= 0 ? args.maxOrdinal : null;
|
|
1295
|
+
const fetchLimit = args.maxOrdinal != null && args.maxOrdinal >= 0 ? args.limit * 3 : args.limit;
|
|
1296
|
+
const baseQuery = sanitizeFtsQuery(args.query.trim());
|
|
1297
|
+
const probes = args.probes ?? [];
|
|
1298
|
+
if (probes.length === 0) {
|
|
1299
|
+
const outcome = args.diagnostics && cutoff !== null ? runMessageFtsQueryWithDiagnostics({
|
|
1300
|
+
db: args.db,
|
|
1301
|
+
sessionId: args.sessionId,
|
|
1302
|
+
ftsQuery: baseQuery,
|
|
1303
|
+
fetchLimit,
|
|
1304
|
+
cutoff,
|
|
1305
|
+
dateRange: args.dateRange,
|
|
1306
|
+
sessionFirst
|
|
1307
|
+
}) : {
|
|
1308
|
+
rows: runMessageFtsQuery(args.db, args.sessionId, baseQuery, fetchLimit, cutoff, args.dateRange, sessionFirst),
|
|
1309
|
+
suppressedCount: 0
|
|
1310
|
+
};
|
|
1311
|
+
if (args.diagnostics) {
|
|
1312
|
+
args.diagnostics.suppressedLiveMessageMatches = outcome.suppressedCount;
|
|
1313
|
+
}
|
|
1314
|
+
const rows = outcome.rows.length > 0 || !args.relaxedRecall ? outcome.rows : runMessageFtsQuery(args.db, args.sessionId, relaxedFtsQuery(args.query), fetchLimit, cutoff, args.dateRange, sessionFirst);
|
|
1315
|
+
const filtered = rows.slice(0, args.limit);
|
|
1316
|
+
return filtered.map((row, rank) => ({
|
|
1317
|
+
source: "message",
|
|
1318
|
+
content: previewText(row.content),
|
|
1319
|
+
score: linearDecayScore(rank, filtered.length),
|
|
1320
|
+
messageOrdinal: row.messageOrdinal,
|
|
1321
|
+
messageId: row.messageId,
|
|
1322
|
+
role: row.role
|
|
1323
|
+
}));
|
|
1324
|
+
}
|
|
1325
|
+
const sanitizedProbes = probes.map((probe) => ({ probe, query: sanitizeFtsQuery(probe) })).filter((entry) => entry.query.length > 0);
|
|
1326
|
+
const corpusSize = getIndexedMessageCorpusSize(args.db, args.sessionId, cutoff);
|
|
1327
|
+
const probeCounts = countSessionFtsMatchesBatch(args.db, args.sessionId, sanitizedProbes.map((entry) => entry.query), cutoff, args.dateRange, sessionFirst);
|
|
1328
|
+
const collectBaseDiagnostics = args.diagnostics !== undefined && cutoff !== null;
|
|
1329
|
+
const baseOutcome = collectBaseDiagnostics && baseQuery.length > 0 ? runMessageFtsQueryWithDiagnostics({
|
|
1330
|
+
db: args.db,
|
|
1331
|
+
sessionId: args.sessionId,
|
|
1332
|
+
ftsQuery: baseQuery,
|
|
1333
|
+
fetchLimit,
|
|
1334
|
+
cutoff,
|
|
1335
|
+
dateRange: args.dateRange,
|
|
1336
|
+
sessionFirst
|
|
1337
|
+
}) : null;
|
|
1338
|
+
if (args.diagnostics) {
|
|
1339
|
+
args.diagnostics.suppressedLiveMessageMatches = baseOutcome?.suppressedCount ?? 0;
|
|
1340
|
+
}
|
|
1341
|
+
const searchQueries = [
|
|
1342
|
+
...!collectBaseDiagnostics && baseQuery.length > 0 ? [baseQuery] : [],
|
|
1343
|
+
...sanitizedProbes.map((entry) => entry.query)
|
|
1344
|
+
];
|
|
1345
|
+
const rowsByQuery = runMessageFtsQueriesBatch(args.db, args.sessionId, searchQueries, fetchLimit, cutoff, args.dateRange, sessionFirst);
|
|
1346
|
+
const queryLists = [];
|
|
1347
|
+
let queryIndex = 0;
|
|
1348
|
+
if (baseQuery.length > 0) {
|
|
1349
|
+
queryLists.push({
|
|
1350
|
+
rows: baseOutcome?.rows ?? rowsByQuery[queryIndex] ?? [],
|
|
1351
|
+
weight: 1
|
|
1352
|
+
});
|
|
1353
|
+
if (!collectBaseDiagnostics)
|
|
1354
|
+
queryIndex += 1;
|
|
1355
|
+
}
|
|
1356
|
+
const probeWeights = new Map;
|
|
1357
|
+
sanitizedProbes.forEach((entry, probeIndex) => {
|
|
1358
|
+
const weight = probeDiscriminationWeight(probeCounts[probeIndex] ?? 0, corpusSize);
|
|
1359
|
+
probeWeights.set(entry.probe, weight);
|
|
1360
|
+
queryLists.push({ rows: rowsByQuery[queryIndex] ?? [], weight });
|
|
1361
|
+
queryIndex += 1;
|
|
1362
|
+
});
|
|
1363
|
+
const fused = new Map;
|
|
1364
|
+
for (const list of queryLists) {
|
|
1365
|
+
list.rows.forEach((row, rank) => {
|
|
1366
|
+
const rrf = list.weight / (RRF_K + rank);
|
|
1367
|
+
const existing = fused.get(row.messageId);
|
|
1368
|
+
if (existing) {
|
|
1369
|
+
existing.score += rrf;
|
|
1370
|
+
} else {
|
|
1371
|
+
fused.set(row.messageId, { row, score: rrf });
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
for (const entry of fused.values()) {
|
|
1376
|
+
let best = 0;
|
|
1377
|
+
for (const probe of probes) {
|
|
1378
|
+
const weight = probeWeights.get(probe) ?? 0;
|
|
1379
|
+
if (weight > best && containsProbeVerbatim(entry.row.content, [probe])) {
|
|
1380
|
+
best = weight;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
if (best > 0) {
|
|
1384
|
+
entry.score += best * VERBATIM_RANK_BONUS;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
const ranked = [...fused.values()].sort((a, b) => b.score !== a.score ? b.score - a.score : a.row.messageOrdinal - b.row.messageOrdinal).slice(0, args.limit);
|
|
1388
|
+
return ranked.map((entry, rank) => ({
|
|
1389
|
+
source: "message",
|
|
1390
|
+
content: previewText(entry.row.content),
|
|
1391
|
+
score: linearDecayScore(rank, ranked.length),
|
|
1392
|
+
messageOrdinal: entry.row.messageOrdinal,
|
|
1393
|
+
messageId: entry.row.messageId,
|
|
1394
|
+
role: entry.row.role
|
|
1395
|
+
}));
|
|
1396
|
+
}
|
|
1397
|
+
var NOTE_SEARCHABLE_STATUSES = ["active", "pending", "ready"];
|
|
1398
|
+
var MAX_NOTE_KEYWORD_SCORE = 3.5;
|
|
1399
|
+
function noteSearchText(note) {
|
|
1400
|
+
const reason = note.readyReason?.trim();
|
|
1401
|
+
return reason ? `${note.content}
|
|
1402
|
+
Reason: ${reason}` : note.content;
|
|
1403
|
+
}
|
|
1404
|
+
function tokenizeKeywordNeedle(text) {
|
|
1405
|
+
const matches = text.toLowerCase().match(/[a-z0-9/._:-]+/g) ?? [];
|
|
1406
|
+
const seen = new Set;
|
|
1407
|
+
const tokens = [];
|
|
1408
|
+
for (const match of matches) {
|
|
1409
|
+
if (match.length <= 1 || !/[a-z0-9]/.test(match) || seen.has(match)) {
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
seen.add(match);
|
|
1413
|
+
tokens.push(match);
|
|
1414
|
+
}
|
|
1415
|
+
return tokens;
|
|
1416
|
+
}
|
|
1417
|
+
function normalizeNoteKeywordScore(score) {
|
|
1418
|
+
return normalizeCosineScore(score / MAX_NOTE_KEYWORD_SCORE) * SINGLE_SOURCE_PENALTY;
|
|
1419
|
+
}
|
|
1420
|
+
function rankNotesForNeedle(notes, needle) {
|
|
1421
|
+
const normalizedNeedle = needle.trim().toLowerCase();
|
|
1422
|
+
if (normalizedNeedle.length === 0) {
|
|
1423
|
+
return [];
|
|
1424
|
+
}
|
|
1425
|
+
const needleTokens = tokenizeKeywordNeedle(normalizedNeedle);
|
|
1426
|
+
const ranked = [];
|
|
1427
|
+
for (const note of notes) {
|
|
1428
|
+
const text = noteSearchText(note);
|
|
1429
|
+
const normalizedText = text.toLowerCase();
|
|
1430
|
+
const noteTokens = new Set(tokenizeKeywordNeedle(normalizedText));
|
|
1431
|
+
const exact = normalizedText.includes(normalizedNeedle);
|
|
1432
|
+
const matchedTokens = needleTokens.filter((token) => noteTokens.has(token)).length;
|
|
1433
|
+
if (!exact && matchedTokens === 0) {
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
const matchedUniqueTokens = new Set(needleTokens.filter((token) => noteTokens.has(token))).size;
|
|
1437
|
+
const coverage = needleTokens.length > 0 ? matchedTokens / needleTokens.length : 0;
|
|
1438
|
+
const density = noteTokens.size > 0 ? matchedUniqueTokens / noteTokens.size : 0;
|
|
1439
|
+
const exactPhrase = exact && (needleTokens.length > 1 || normalizedText.trim() === normalizedNeedle);
|
|
1440
|
+
const allTokens = needleTokens.length > 1 && matchedTokens === needleTokens.length;
|
|
1441
|
+
const score = (exactPhrase ? 2 : 0) + coverage * density + (allTokens ? 0.5 * density : 0);
|
|
1442
|
+
ranked.push({ note, score, text });
|
|
1443
|
+
}
|
|
1444
|
+
return ranked.sort((left, right) => {
|
|
1445
|
+
if (right.score !== left.score) {
|
|
1446
|
+
return right.score - left.score;
|
|
1447
|
+
}
|
|
1448
|
+
if (right.note.createdAt !== left.note.createdAt) {
|
|
1449
|
+
return right.note.createdAt - left.note.createdAt;
|
|
1450
|
+
}
|
|
1451
|
+
return left.note.id - right.note.id;
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
function searchNotes(args) {
|
|
1455
|
+
if (args.limit <= 0) {
|
|
1456
|
+
return [];
|
|
1457
|
+
}
|
|
1458
|
+
const notes = [
|
|
1459
|
+
...getNotes(args.db, {
|
|
1460
|
+
sessionId: args.sessionId,
|
|
1461
|
+
type: "session",
|
|
1462
|
+
status: NOTE_SEARCHABLE_STATUSES
|
|
1463
|
+
}),
|
|
1464
|
+
...getNotes(args.db, {
|
|
1465
|
+
projectPath: args.projectPath,
|
|
1466
|
+
type: "smart",
|
|
1467
|
+
status: NOTE_SEARCHABLE_STATUSES
|
|
1468
|
+
})
|
|
1469
|
+
].filter((note) => timestampIsInRange(note.createdAt, args.dateRange));
|
|
1470
|
+
if (notes.length === 0) {
|
|
1471
|
+
return [];
|
|
1472
|
+
}
|
|
1473
|
+
const baseList = rankNotesForNeedle(notes, args.query);
|
|
1474
|
+
const probes = args.probes ?? [];
|
|
1475
|
+
if (probes.length === 0) {
|
|
1476
|
+
const ranked = baseList.slice(0, args.limit);
|
|
1477
|
+
return ranked.map((entry) => ({
|
|
1478
|
+
source: "note",
|
|
1479
|
+
content: previewText(entry.text),
|
|
1480
|
+
score: normalizeNoteKeywordScore(entry.score),
|
|
1481
|
+
noteId: entry.note.id,
|
|
1482
|
+
status: entry.note.status,
|
|
1483
|
+
createdAt: entry.note.createdAt,
|
|
1484
|
+
anchorOrdinal: entry.note.anchorOrdinal,
|
|
1485
|
+
sourceSessionId: entry.note.sessionId
|
|
1486
|
+
}));
|
|
1487
|
+
}
|
|
1488
|
+
const queryLists = [];
|
|
1489
|
+
if (baseList.length > 0) {
|
|
1490
|
+
queryLists.push({ rows: baseList, weight: 1 });
|
|
1491
|
+
}
|
|
1492
|
+
for (const probe of probes) {
|
|
1493
|
+
const rows = rankNotesForNeedle(notes, probe);
|
|
1494
|
+
if (rows.length === 0) {
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
const weight = probeDiscriminationWeight(rows.length, notes.length);
|
|
1498
|
+
queryLists.push({ rows, weight });
|
|
1499
|
+
}
|
|
1500
|
+
const fused = new Map;
|
|
1501
|
+
for (const list of queryLists) {
|
|
1502
|
+
for (const row of list.rows) {
|
|
1503
|
+
const relevance = row.score * list.weight;
|
|
1504
|
+
const existing = fused.get(row.note.id);
|
|
1505
|
+
if (existing) {
|
|
1506
|
+
if (relevance > existing.score) {
|
|
1507
|
+
existing.entry = row;
|
|
1508
|
+
existing.score = relevance;
|
|
1509
|
+
}
|
|
1510
|
+
} else {
|
|
1511
|
+
fused.set(row.note.id, { entry: row, score: relevance });
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
const ranked = [...fused.values()].sort((left, right) => {
|
|
1516
|
+
if (right.score !== left.score) {
|
|
1517
|
+
return right.score - left.score;
|
|
1518
|
+
}
|
|
1519
|
+
if (right.entry.note.createdAt !== left.entry.note.createdAt) {
|
|
1520
|
+
return right.entry.note.createdAt - left.entry.note.createdAt;
|
|
1521
|
+
}
|
|
1522
|
+
return left.entry.note.id - right.entry.note.id;
|
|
1523
|
+
}).slice(0, args.limit);
|
|
1524
|
+
return ranked.map((entry) => ({
|
|
1525
|
+
source: "note",
|
|
1526
|
+
content: previewText(entry.entry.text),
|
|
1527
|
+
score: normalizeNoteKeywordScore(entry.score),
|
|
1528
|
+
noteId: entry.entry.note.id,
|
|
1529
|
+
status: entry.entry.note.status,
|
|
1530
|
+
createdAt: entry.entry.note.createdAt,
|
|
1531
|
+
anchorOrdinal: entry.entry.note.anchorOrdinal,
|
|
1532
|
+
sourceSessionId: entry.entry.note.sessionId
|
|
1533
|
+
}));
|
|
1534
|
+
}
|
|
1535
|
+
function searchCompartmentChunks(args) {
|
|
1536
|
+
if (!args.queryEmbedding || args.limit <= 0 || !args.modelId || args.modelId === "off")
|
|
1537
|
+
return [];
|
|
1538
|
+
const cutoff = args.maxOrdinal != null && args.maxOrdinal >= 0 ? args.maxOrdinal : null;
|
|
1539
|
+
const rows = loadCompartmentChunkEmbeddingsForSearch(args.db, args.sessionId, args.projectPath, args.modelId, args.dateRange);
|
|
1540
|
+
if (rows.length === 0)
|
|
1541
|
+
return [];
|
|
1542
|
+
const byCompartment = new Map;
|
|
1543
|
+
for (const row of rows) {
|
|
1544
|
+
if (cutoff !== null && row.endOrdinal > cutoff) {
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
const score = normalizeCosineScore(cosineSimilarity(args.queryEmbedding, row.vector));
|
|
1548
|
+
if (score <= 0)
|
|
1549
|
+
continue;
|
|
1550
|
+
const existing = byCompartment.get(row.compartmentId);
|
|
1551
|
+
if (!existing || score > existing.score) {
|
|
1552
|
+
byCompartment.set(row.compartmentId, { row, score });
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
return [...byCompartment.values()].sort((left, right) => right.score !== left.score ? right.score - left.score : left.row.startOrdinal - right.row.startOrdinal).slice(0, args.limit).map(({ row, score }) => ({
|
|
1556
|
+
source: "compartment",
|
|
1557
|
+
content: previewText(row.title),
|
|
1558
|
+
score: score * SINGLE_SOURCE_PENALTY,
|
|
1559
|
+
compartmentId: row.compartmentId,
|
|
1560
|
+
sessionId: row.sessionId,
|
|
1561
|
+
title: row.title,
|
|
1562
|
+
startOrdinal: row.startOrdinal,
|
|
1563
|
+
endOrdinal: row.endOrdinal,
|
|
1564
|
+
matchType: "semantic"
|
|
1565
|
+
}));
|
|
1566
|
+
}
|
|
1567
|
+
function mergeMessageAndCompartmentResults(args) {
|
|
1568
|
+
if (args.compartments.length === 0)
|
|
1569
|
+
return args.messages;
|
|
1570
|
+
if (args.messages.length === 0)
|
|
1571
|
+
return args.compartments;
|
|
1572
|
+
const fused = new Map;
|
|
1573
|
+
const add = (key, result, score, tieOrdinal) => {
|
|
1574
|
+
const existing = fused.get(key);
|
|
1575
|
+
if (existing) {
|
|
1576
|
+
existing.score += score;
|
|
1577
|
+
return existing;
|
|
1578
|
+
}
|
|
1579
|
+
const entry = { result, score, tieOrdinal, snippetScore: -1 };
|
|
1580
|
+
fused.set(key, entry);
|
|
1581
|
+
return entry;
|
|
1582
|
+
};
|
|
1583
|
+
args.compartments.forEach((compartment, rank) => {
|
|
1584
|
+
add(`compartment:${compartment.compartmentId}`, compartment, 1 / (RRF_K + rank), compartment.startOrdinal);
|
|
1585
|
+
});
|
|
1586
|
+
for (const [rank, message] of args.messages.entries()) {
|
|
1587
|
+
const containing = args.compartments.find((compartment) => message.messageOrdinal >= compartment.startOrdinal && message.messageOrdinal <= compartment.endOrdinal);
|
|
1588
|
+
const contribution = 1 / (RRF_K + rank);
|
|
1589
|
+
if (!containing) {
|
|
1590
|
+
add(`message:${message.messageId}`, message, contribution, message.messageOrdinal);
|
|
1591
|
+
continue;
|
|
1592
|
+
}
|
|
1593
|
+
const entry = add(`compartment:${containing.compartmentId}`, containing, contribution, containing.startOrdinal);
|
|
1594
|
+
if (message.score > entry.snippetScore && entry.result.source === "compartment") {
|
|
1595
|
+
entry.snippetScore = message.score;
|
|
1596
|
+
entry.result = {
|
|
1597
|
+
...entry.result,
|
|
1598
|
+
matchType: "hybrid",
|
|
1599
|
+
snippet: message.content
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
const ranked = [...fused.values()].sort((left, right) => right.score !== left.score ? right.score - left.score : left.tieOrdinal - right.tieOrdinal).slice(0, args.limit);
|
|
1604
|
+
return ranked.map((entry, rank) => ({
|
|
1605
|
+
...entry.result,
|
|
1606
|
+
score: linearDecayScore(rank, ranked.length)
|
|
1607
|
+
}));
|
|
1608
|
+
}
|
|
1609
|
+
function getSourceBoost(result) {
|
|
1610
|
+
switch (result.source) {
|
|
1611
|
+
case "memory":
|
|
1612
|
+
return MEMORY_SOURCE_BOOST;
|
|
1613
|
+
case "message":
|
|
1614
|
+
case "compartment":
|
|
1615
|
+
return MESSAGE_SOURCE_BOOST;
|
|
1616
|
+
case "git_commit":
|
|
1617
|
+
return GIT_COMMIT_SOURCE_BOOST;
|
|
1618
|
+
case "primer":
|
|
1619
|
+
return PRIMER_SOURCE_BOOST;
|
|
1620
|
+
case "note":
|
|
1621
|
+
return 1;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
function compareUnifiedResults(left, right) {
|
|
1625
|
+
const leftEffective = left.score * getSourceBoost(left);
|
|
1626
|
+
const rightEffective = right.score * getSourceBoost(right);
|
|
1627
|
+
if (rightEffective !== leftEffective) {
|
|
1628
|
+
return rightEffective - leftEffective;
|
|
1629
|
+
}
|
|
1630
|
+
if (left.source === "memory" && right.source === "memory") {
|
|
1631
|
+
return left.memoryId - right.memoryId;
|
|
1632
|
+
}
|
|
1633
|
+
if (left.source === "message" && right.source === "message") {
|
|
1634
|
+
return left.messageOrdinal - right.messageOrdinal;
|
|
1635
|
+
}
|
|
1636
|
+
if (left.source === "compartment" && right.source === "compartment") {
|
|
1637
|
+
return left.startOrdinal - right.startOrdinal;
|
|
1638
|
+
}
|
|
1639
|
+
if (left.source === "git_commit" && right.source === "git_commit") {
|
|
1640
|
+
return right.committedAtMs - left.committedAtMs;
|
|
1641
|
+
}
|
|
1642
|
+
if (left.source === "primer" && right.source === "primer") {
|
|
1643
|
+
return right.support - left.support || left.primerId - right.primerId;
|
|
1644
|
+
}
|
|
1645
|
+
if (left.source === "note" && right.source === "note") {
|
|
1646
|
+
return right.createdAt - left.createdAt || left.noteId - right.noteId;
|
|
1647
|
+
}
|
|
1648
|
+
return 0;
|
|
1649
|
+
}
|
|
1650
|
+
function toGitCommitResult(hit) {
|
|
1651
|
+
return {
|
|
1652
|
+
source: "git_commit",
|
|
1653
|
+
content: previewText(hit.commit.message),
|
|
1654
|
+
score: hit.score,
|
|
1655
|
+
sha: hit.commit.sha,
|
|
1656
|
+
shortSha: hit.commit.shortSha,
|
|
1657
|
+
author: hit.commit.author,
|
|
1658
|
+
committedAtMs: hit.commit.committedAtMs,
|
|
1659
|
+
matchType: hit.matchType
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
function searchGitCommits(args) {
|
|
1663
|
+
if (args.limit <= 0)
|
|
1664
|
+
return [];
|
|
1665
|
+
const hits = searchGitCommitsSync(args.db, args.projectPath, args.query, {
|
|
1666
|
+
limit: args.limit,
|
|
1667
|
+
queryEmbedding: args.queryEmbedding,
|
|
1668
|
+
queryModelId: args.queryModelId,
|
|
1669
|
+
from: args.dateRange?.from,
|
|
1670
|
+
to: args.dateRange?.to
|
|
1671
|
+
});
|
|
1672
|
+
return hits.map(toGitCommitResult);
|
|
1673
|
+
}
|
|
1674
|
+
function primerText(primer) {
|
|
1675
|
+
const answer = primer.answer.trim();
|
|
1676
|
+
return answer ? `Q: ${primer.question}
|
|
1677
|
+
A: ${answer}` : `Q: ${primer.question}`;
|
|
1678
|
+
}
|
|
1679
|
+
function searchPrimers(args) {
|
|
1680
|
+
const primers = getActivePrimers(args.db, args.projectPath).filter((primer) => timestampIsInRange(primer.createdAt, args.dateRange));
|
|
1681
|
+
if (primers.length === 0 || args.limit <= 0)
|
|
1682
|
+
return [];
|
|
1683
|
+
const ftsQuery = sanitizeFtsQuery(args.query);
|
|
1684
|
+
const ftsRanks = new Map;
|
|
1685
|
+
if (ftsQuery) {
|
|
1686
|
+
const rows = args.db.prepare(`SELECT p.id AS id, bm25(primers_fts) AS rank
|
|
1687
|
+
FROM primers_fts
|
|
1688
|
+
JOIN primers p ON p.id = primers_fts.rowid
|
|
1689
|
+
WHERE primers_fts MATCH ? AND p.project_path = ? AND p.status = 'active'
|
|
1690
|
+
${args.dateRange === null ? "" : "AND p.created_at BETWEEN ? AND ?"}
|
|
1691
|
+
ORDER BY rank ASC
|
|
1692
|
+
LIMIT ?`).all(ftsQuery, args.projectPath, ...args.dateRange === null ? [] : [args.dateRange.from, args.dateRange.to], args.limit * 3);
|
|
1693
|
+
rows.forEach((row, index) => {
|
|
1694
|
+
ftsRanks.set(row.id, linearDecayScore(index, rows.length));
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
const scored = primers.map((primer) => {
|
|
1698
|
+
const semantic = args.queryEmbedding && primer.questionEmbedding && primer.questionEmbeddingModelId === args.queryModelId ? normalizeCosineScore(cosineSimilarity(args.queryEmbedding, primer.questionEmbedding)) : 0;
|
|
1699
|
+
const fts = ftsRanks.get(primer.id) ?? 0;
|
|
1700
|
+
if (semantic <= 0 && fts <= 0)
|
|
1701
|
+
return null;
|
|
1702
|
+
const score = semantic > 0 && fts > 0 ? semantic * SEMANTIC_WEIGHT + fts * FTS_WEIGHT : Math.max(semantic, fts);
|
|
1703
|
+
return {
|
|
1704
|
+
source: "primer",
|
|
1705
|
+
content: previewText(primerText(primer)),
|
|
1706
|
+
score,
|
|
1707
|
+
primerId: primer.id,
|
|
1708
|
+
question: primer.question,
|
|
1709
|
+
support: primer.totalSupport,
|
|
1710
|
+
lastObservedAt: primer.lastObservedAt,
|
|
1711
|
+
matchType: semantic > 0 && fts > 0 ? "hybrid" : semantic > 0 ? "semantic" : "fts"
|
|
1712
|
+
};
|
|
1713
|
+
}).filter((result) => result !== null).sort((a, b) => b.score - a.score || b.support - a.support || a.primerId - b.primerId).slice(0, args.limit);
|
|
1714
|
+
return scored;
|
|
1715
|
+
}
|
|
1716
|
+
function resolveSources(sources) {
|
|
1717
|
+
if (sources === undefined) {
|
|
1718
|
+
return new Set(["memory", "message", "git_commit", "primer", "note"]);
|
|
1719
|
+
}
|
|
1720
|
+
const set = new Set;
|
|
1721
|
+
for (const source of sources) {
|
|
1722
|
+
if (source === "memory" || source === "message" || source === "git_commit" || source === "primer" || source === "note") {
|
|
1723
|
+
set.add(source);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return set;
|
|
1727
|
+
}
|
|
1728
|
+
function memoriesToIdLookupResults(args) {
|
|
1729
|
+
const ordered = args.memories.slice(0, args.limit);
|
|
1730
|
+
return ordered.map((memory, rank) => ({
|
|
1731
|
+
source: "memory",
|
|
1732
|
+
content: previewText(memory.content),
|
|
1733
|
+
score: 1 - rank * 0.01,
|
|
1734
|
+
memoryId: memory.id,
|
|
1735
|
+
category: memory.category,
|
|
1736
|
+
matchType: "fts",
|
|
1737
|
+
sourceName: args.sourceNameByMemoryId?.get(memory.id)
|
|
1738
|
+
}));
|
|
1739
|
+
}
|
|
1740
|
+
function resolveMemoriesByIdsForSearch(args) {
|
|
1741
|
+
if (args.diagnostics) {
|
|
1742
|
+
args.diagnostics.suppressedVisibleMemoryIds = [];
|
|
1743
|
+
}
|
|
1744
|
+
if (args.ids.length === 0) {
|
|
1745
|
+
return null;
|
|
1746
|
+
}
|
|
1747
|
+
const workspace = resolveSearchWorkspaceContext(args.db, args.projectPath);
|
|
1748
|
+
const fetched = workspace.isWorkspaced ? getMemoriesByProjects(args.db, workspace.expandedIdentities, ["active", "permanent", "archived"], Date.now(), workspace.ownIdentities, workspace.shareCategories) : getMemoriesByProject(args.db, args.projectPath, ["active", "permanent", "archived"]);
|
|
1749
|
+
if (fetched.length === 0) {
|
|
1750
|
+
return null;
|
|
1751
|
+
}
|
|
1752
|
+
const dateRange = normalizeDateRange(args.from, args.to);
|
|
1753
|
+
const memoriesById = new Map(fetched.filter((memory) => timestampIsInRange(memory.createdAt, dateRange)).map((memory) => [memory.id, memory]));
|
|
1754
|
+
const ordered = [];
|
|
1755
|
+
const suppressedVisibleIds = new Set;
|
|
1756
|
+
for (const id of args.ids) {
|
|
1757
|
+
const memory = memoriesById.get(id);
|
|
1758
|
+
if (!memory)
|
|
1759
|
+
continue;
|
|
1760
|
+
if (args.visibleMemoryIds?.has(id)) {
|
|
1761
|
+
suppressedVisibleIds.add(id);
|
|
1762
|
+
continue;
|
|
1763
|
+
}
|
|
1764
|
+
ordered.push(memory);
|
|
1765
|
+
if (ordered.length >= args.limit)
|
|
1766
|
+
break;
|
|
1767
|
+
}
|
|
1768
|
+
if (args.diagnostics) {
|
|
1769
|
+
args.diagnostics.suppressedVisibleMemoryIds = [...suppressedVisibleIds].sort((left, right) => left - right);
|
|
1770
|
+
}
|
|
1771
|
+
if (ordered.length === 0) {
|
|
1772
|
+
return null;
|
|
1773
|
+
}
|
|
1774
|
+
return memoriesToIdLookupResults({
|
|
1775
|
+
memories: ordered,
|
|
1776
|
+
limit: args.limit,
|
|
1777
|
+
sourceNameByMemoryId: sourceNamesForSearchMemories({
|
|
1778
|
+
memories: ordered,
|
|
1779
|
+
projectPath: args.projectPath,
|
|
1780
|
+
workspace
|
|
1781
|
+
})
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
async function unifiedSearch(db, sessionId, projectPath, query, options = {}) {
|
|
1785
|
+
const trimmedQuery = query.trim();
|
|
1786
|
+
const measurementStartedAt = Date.now();
|
|
1787
|
+
if (trimmedQuery.length === 0 || options.signal?.aborted) {
|
|
1788
|
+
return [];
|
|
1789
|
+
}
|
|
1790
|
+
const limit = normalizeLimit(options.limit);
|
|
1791
|
+
const dateRange = normalizeDateRange(options.from, options.to);
|
|
1792
|
+
const tierLimit = Math.max(limit * 3, DEFAULT_UNIFIED_SEARCH_LIMIT);
|
|
1793
|
+
if (options.diagnostics) {
|
|
1794
|
+
options.diagnostics.suppressedVisibleMemoryIds = [];
|
|
1795
|
+
options.diagnostics.suppressedLiveMessageMatches = 0;
|
|
1796
|
+
options.diagnostics.gitCommitUnavailable = null;
|
|
1797
|
+
}
|
|
1798
|
+
const embeddingEnabled = options.embeddingEnabled ?? true;
|
|
1799
|
+
const embedQuery = options.embedQuery ?? embedText;
|
|
1800
|
+
const isEmbeddingRuntimeEnabled = options.isEmbeddingRuntimeEnabled ?? isEmbeddingEnabled;
|
|
1801
|
+
const gitCommitsEnabled = options.gitCommitsEnabled ?? false;
|
|
1802
|
+
const activeSources = resolveSources(options.sources);
|
|
1803
|
+
const memoryFeatureEnabled = options.memoryEnabled ?? true;
|
|
1804
|
+
const runMemory = activeSources.has("memory") && memoryFeatureEnabled;
|
|
1805
|
+
const runMessages = activeSources.has("message");
|
|
1806
|
+
const runGitCommits = activeSources.has("git_commit") && gitCommitsEnabled;
|
|
1807
|
+
if (options.diagnostics && activeSources.has("git_commit") && options.gitRepositoryAvailable === false) {
|
|
1808
|
+
options.diagnostics.gitCommitUnavailable = "no_git_repository";
|
|
1809
|
+
}
|
|
1810
|
+
const runPrimers = activeSources.has("primer") && memoryFeatureEnabled;
|
|
1811
|
+
const runNotes = activeSources.has("note");
|
|
1812
|
+
const runCompartmentChunks = runMessages && embeddingEnabled;
|
|
1813
|
+
const needsEmbedding = (runMemory || runGitCommits || runCompartmentChunks || runPrimers) && embeddingEnabled && isEmbeddingRuntimeEnabled();
|
|
1814
|
+
const queryEmbeddingPromise = needsEmbedding ? embedQuery(trimmedQuery, options.signal).catch((error) => {
|
|
1815
|
+
log(`[search] query embedding failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1816
|
+
return null;
|
|
1817
|
+
}) : Promise.resolve(null);
|
|
1818
|
+
await Promise.resolve();
|
|
1819
|
+
if (options.signal?.aborted)
|
|
1820
|
+
return [];
|
|
1821
|
+
const messageProbes = options.explicitSearch ? extractLiteralProbes(trimmedQuery) : [];
|
|
1822
|
+
const messageResults = runMessages ? searchMessages({
|
|
1823
|
+
db,
|
|
1824
|
+
sessionId,
|
|
1825
|
+
query: trimmedQuery,
|
|
1826
|
+
limit: tierLimit,
|
|
1827
|
+
maxOrdinal: options.maxMessageOrdinal,
|
|
1828
|
+
probes: messageProbes,
|
|
1829
|
+
relaxedRecall: options.explicitSearch,
|
|
1830
|
+
diagnostics: options.diagnostics,
|
|
1831
|
+
dateRange
|
|
1832
|
+
}) : [];
|
|
1833
|
+
const capturedQuery = await queryEmbeddingPromise;
|
|
1834
|
+
if (options.signal?.aborted)
|
|
1835
|
+
return [];
|
|
1836
|
+
const embeddingSnapshot = getProjectEmbeddingSnapshot(projectPath);
|
|
1837
|
+
const queryContract = capturedQuery instanceof Float32Array || capturedQuery === null ? null : capturedQuery;
|
|
1838
|
+
const generationIsCurrent = queryContract === null || embeddingSnapshot !== null && embeddingSnapshot.generation === queryContract.generation;
|
|
1839
|
+
const queryEmbedding = generationIsCurrent ? queryContract?.vector ?? (capturedQuery instanceof Float32Array ? capturedQuery : null) : null;
|
|
1840
|
+
const workspace = resolveSearchWorkspaceContext(db, projectPath);
|
|
1841
|
+
const embeddingModelId = queryContract?.modelId ?? options.embeddingModelIdOverride ?? embeddingSnapshot?.modelId;
|
|
1842
|
+
const chunkModelId = queryContract?.chunkModelId ?? options.chunkModelIdOverride ?? embeddingSnapshot?.chunkModelId;
|
|
1843
|
+
const compartmentResults = runCompartmentChunks ? searchCompartmentChunks({
|
|
1844
|
+
db,
|
|
1845
|
+
sessionId,
|
|
1846
|
+
projectPath,
|
|
1847
|
+
queryEmbedding,
|
|
1848
|
+
limit: tierLimit,
|
|
1849
|
+
maxOrdinal: options.maxMessageOrdinal,
|
|
1850
|
+
modelId: chunkModelId && chunkModelId !== "off" ? chunkModelId : null,
|
|
1851
|
+
dateRange
|
|
1852
|
+
}) : [];
|
|
1853
|
+
const messageLikeResults = mergeMessageAndCompartmentResults({
|
|
1854
|
+
messages: messageResults,
|
|
1855
|
+
compartments: compartmentResults,
|
|
1856
|
+
limit: tierLimit
|
|
1857
|
+
});
|
|
1858
|
+
const [memoryOutcome, gitCommitResults, primerResults, noteResults] = await Promise.all([
|
|
1859
|
+
runMemory ? searchMemories({
|
|
1860
|
+
db,
|
|
1861
|
+
projectPath,
|
|
1862
|
+
query: trimmedQuery,
|
|
1863
|
+
limit: tierLimit,
|
|
1864
|
+
memoryEnabled: true,
|
|
1865
|
+
queryEmbedding,
|
|
1866
|
+
queryModelId: embeddingModelId && embeddingModelId !== "off" ? embeddingModelId : null,
|
|
1867
|
+
workspace,
|
|
1868
|
+
visibleMemoryIds: options.visibleMemoryIds,
|
|
1869
|
+
backfillMemoryEmbeddings: options.backfillMemoryEmbeddings,
|
|
1870
|
+
dateRange
|
|
1871
|
+
}) : Promise.resolve({
|
|
1872
|
+
results: [],
|
|
1873
|
+
suppressedVisibleIds: []
|
|
1874
|
+
}),
|
|
1875
|
+
runGitCommits ? Promise.resolve(searchGitCommits({
|
|
1876
|
+
db,
|
|
1877
|
+
projectPath,
|
|
1878
|
+
query: trimmedQuery,
|
|
1879
|
+
limit: tierLimit,
|
|
1880
|
+
queryEmbedding,
|
|
1881
|
+
queryModelId: embeddingModelId && embeddingModelId !== "off" ? embeddingModelId : null,
|
|
1882
|
+
dateRange
|
|
1883
|
+
})) : Promise.resolve([]),
|
|
1884
|
+
runPrimers ? Promise.resolve(searchPrimers({
|
|
1885
|
+
db,
|
|
1886
|
+
projectPath,
|
|
1887
|
+
query: trimmedQuery,
|
|
1888
|
+
limit: tierLimit,
|
|
1889
|
+
queryEmbedding,
|
|
1890
|
+
queryModelId: embeddingModelId && embeddingModelId !== "off" ? embeddingModelId : null,
|
|
1891
|
+
dateRange
|
|
1892
|
+
})) : Promise.resolve([]),
|
|
1893
|
+
runNotes ? Promise.resolve(searchNotes({
|
|
1894
|
+
db,
|
|
1895
|
+
sessionId,
|
|
1896
|
+
projectPath,
|
|
1897
|
+
query: trimmedQuery,
|
|
1898
|
+
limit: tierLimit,
|
|
1899
|
+
probes: messageProbes,
|
|
1900
|
+
dateRange
|
|
1901
|
+
})) : Promise.resolve([])
|
|
1902
|
+
]);
|
|
1903
|
+
if (options.diagnostics) {
|
|
1904
|
+
options.diagnostics.suppressedVisibleMemoryIds = memoryOutcome.suppressedVisibleIds;
|
|
1905
|
+
}
|
|
1906
|
+
const results = [
|
|
1907
|
+
...memoryOutcome.results,
|
|
1908
|
+
...primerResults,
|
|
1909
|
+
...messageLikeResults,
|
|
1910
|
+
...gitCommitResults,
|
|
1911
|
+
...noteResults
|
|
1912
|
+
].sort(compareUnifiedResults).slice(0, limit);
|
|
1913
|
+
if (!options.measurementDisabled) {
|
|
1914
|
+
recordShadowMeasurement({
|
|
1915
|
+
db,
|
|
1916
|
+
sessionId,
|
|
1917
|
+
projectPath,
|
|
1918
|
+
query: trimmedQuery,
|
|
1919
|
+
options,
|
|
1920
|
+
primaryResults: results,
|
|
1921
|
+
primaryQuery: queryContract,
|
|
1922
|
+
primaryLatencyMs: Date.now() - measurementStartedAt,
|
|
1923
|
+
search: unifiedSearch
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
const countRetrievals = options.countRetrievals ?? true;
|
|
1927
|
+
if (countRetrievals) {
|
|
1928
|
+
const memoryIds = results.filter((result) => result.source === "memory").map((result) => result.memoryId);
|
|
1929
|
+
if (memoryIds.length > 0) {
|
|
1930
|
+
db.transaction(() => {
|
|
1931
|
+
for (const memoryId of memoryIds) {
|
|
1932
|
+
try {
|
|
1933
|
+
updateMemoryRetrievalCount(db, memoryId);
|
|
1934
|
+
} catch (error) {
|
|
1935
|
+
if (error instanceof ModuleMemoryAuthorityError)
|
|
1936
|
+
continue;
|
|
1937
|
+
throw error;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}).immediate();
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
return results;
|
|
1944
|
+
}
|
|
1945
|
+
var SEARCH_NOTE_EXPAND_HINT = "Use ctx_expand(start=N-10, end=N) around any note @msg anchor above to read the surrounding conversation context.";
|
|
1946
|
+
function formatSearchAge(timestampMs) {
|
|
1947
|
+
const ageMs = Date.now() - timestampMs;
|
|
1948
|
+
if (ageMs < 0)
|
|
1949
|
+
return "future";
|
|
1950
|
+
const days = Math.floor(ageMs / (24 * 60 * 60 * 1000));
|
|
1951
|
+
if (days <= 0)
|
|
1952
|
+
return "today";
|
|
1953
|
+
if (days === 1)
|
|
1954
|
+
return "1d ago";
|
|
1955
|
+
if (days < 30)
|
|
1956
|
+
return `${days}d ago`;
|
|
1957
|
+
const months = Math.floor(days / 30);
|
|
1958
|
+
if (months === 1)
|
|
1959
|
+
return "1mo ago";
|
|
1960
|
+
if (months < 12)
|
|
1961
|
+
return `${months}mo ago`;
|
|
1962
|
+
const years = Math.floor(days / 365);
|
|
1963
|
+
return years === 1 ? "1y ago" : `${years}y ago`;
|
|
1964
|
+
}
|
|
1965
|
+
function formatUnifiedSearchResult(result, index, currentSessionId) {
|
|
1966
|
+
if (result.source === "memory") {
|
|
1967
|
+
const source = result.sourceName ? ` source=${result.sourceName}` : "";
|
|
1968
|
+
return [
|
|
1969
|
+
`[${index}] [memory] score=${result.score.toFixed(2)} id=${result.memoryId} category=${result.category}${source} match=${result.matchType}`,
|
|
1970
|
+
result.content
|
|
1971
|
+
].join(`
|
|
1972
|
+
`);
|
|
1973
|
+
}
|
|
1974
|
+
if (result.source === "git_commit") {
|
|
1975
|
+
return [
|
|
1976
|
+
`[${index}] [git_commit] score=${result.score.toFixed(2)} sha=${result.shortSha} ${formatSearchAge(result.committedAtMs)} match=${result.matchType}`,
|
|
1977
|
+
result.content
|
|
1978
|
+
].join(`
|
|
1979
|
+
`);
|
|
1980
|
+
}
|
|
1981
|
+
if (result.source === "primer") {
|
|
1982
|
+
return [
|
|
1983
|
+
`[${index}] [primer] score=${result.score.toFixed(2)} id=${result.primerId} support=${result.support} match=${result.matchType}`,
|
|
1984
|
+
result.content
|
|
1985
|
+
].join(`
|
|
1986
|
+
`);
|
|
1987
|
+
}
|
|
1988
|
+
if (result.source === "note") {
|
|
1989
|
+
const anchor = result.anchorOrdinal !== null && result.sourceSessionId === currentSessionId ? ` @msg ${result.anchorOrdinal}` : "";
|
|
1990
|
+
return [
|
|
1991
|
+
`[${index}] [note] score=${result.score.toFixed(2)} id=#${result.noteId} status=${result.status} ${formatSearchAge(result.createdAt)}${anchor}`,
|
|
1992
|
+
result.content
|
|
1993
|
+
].join(`
|
|
1994
|
+
`);
|
|
1995
|
+
}
|
|
1996
|
+
if (result.source === "compartment") {
|
|
1997
|
+
return [
|
|
1998
|
+
`[${index}] [message] score=${result.score.toFixed(2)} compartment_id=${result.compartmentId} range=${result.startOrdinal}-${result.endOrdinal} match=${result.matchType} title=${result.title}`,
|
|
1999
|
+
result.snippet ? `Snippet: ${result.snippet}` : result.content
|
|
2000
|
+
].join(`
|
|
2001
|
+
`);
|
|
2002
|
+
}
|
|
2003
|
+
const expandStart = Math.max(1, result.messageOrdinal - 3);
|
|
2004
|
+
const expandEnd = result.messageOrdinal + 3;
|
|
2005
|
+
return [
|
|
2006
|
+
`[${index}] [message] score=${result.score.toFixed(2)} ordinal=${result.messageOrdinal} range=${expandStart}-${expandEnd} role=${result.role}`,
|
|
2007
|
+
result.content
|
|
2008
|
+
].join(`
|
|
2009
|
+
`);
|
|
2010
|
+
}
|
|
2011
|
+
function formatSearchDiagnosticLines(results, diagnostics) {
|
|
2012
|
+
if (!diagnostics)
|
|
2013
|
+
return [];
|
|
2014
|
+
const lines = [];
|
|
2015
|
+
const visibleIds = [...diagnostics.suppressedVisibleMemoryIds].sort((left, right) => left - right);
|
|
2016
|
+
if (visibleIds.length > 0) {
|
|
2017
|
+
const count = visibleIds.length;
|
|
2018
|
+
const noun = count === 1 ? "match" : "matches";
|
|
2019
|
+
const ids = visibleIds.join(", ");
|
|
2020
|
+
if (results.some((result) => result.source === "memory")) {
|
|
2021
|
+
lines.push(`Memories: ${count} additional ${noun} suppressed because ${count === 1 ? "it is" : "they are"} already visible in your project-memory block (ids ${ids}).`);
|
|
2022
|
+
} else {
|
|
2023
|
+
lines.push(`Memories: ${count} ${noun} found, all already visible in your project-memory block (ids ${ids}).`);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
if (diagnostics.suppressedLiveMessageMatches > 0) {
|
|
2027
|
+
const count = diagnostics.suppressedLiveMessageMatches;
|
|
2028
|
+
lines.push(`Message history: ${count} raw-message ${count === 1 ? "match is" : "matches are"} newer than the last compartment boundary (already in your context).`);
|
|
2029
|
+
}
|
|
2030
|
+
if (diagnostics.gitCommitUnavailable === "no_git_repository") {
|
|
2031
|
+
lines.push("Git commits: no git repository — commit search unavailable for this project.");
|
|
2032
|
+
}
|
|
2033
|
+
return lines;
|
|
2034
|
+
}
|
|
2035
|
+
function formatSearchResults(query, results, currentSessionId, diagnostics) {
|
|
2036
|
+
const diagnosticLines = formatSearchDiagnosticLines(results, diagnostics);
|
|
2037
|
+
if (results.length === 0) {
|
|
2038
|
+
if (diagnosticLines.length > 0) {
|
|
2039
|
+
return `No hidden results found for "${query}".
|
|
2040
|
+
|
|
2041
|
+
${diagnosticLines.join(`
|
|
2042
|
+
`)}`;
|
|
2043
|
+
}
|
|
2044
|
+
return `No results found for "${query}" across notes, memories, primers, git commits, or message history.`;
|
|
2045
|
+
}
|
|
2046
|
+
const bodyParts = results.map((result, index) => formatUnifiedSearchResult(result, index + 1, currentSessionId));
|
|
2047
|
+
if (diagnosticLines.length > 0)
|
|
2048
|
+
bodyParts.push(diagnosticLines.join(`
|
|
2049
|
+
`));
|
|
2050
|
+
if (results.some((result) => result.source === "message" || result.source === "compartment")) {
|
|
2051
|
+
bodyParts.push("Use ctx_expand(start, end) with the range from any message result above to read the full conversation context.");
|
|
2052
|
+
}
|
|
2053
|
+
if (results.some((result) => result.source === "note" && result.anchorOrdinal !== null && result.sourceSessionId === currentSessionId)) {
|
|
2054
|
+
bodyParts.push(SEARCH_NOTE_EXPAND_HINT);
|
|
2055
|
+
}
|
|
2056
|
+
return `Found ${results.length} result${results.length === 1 ? "" : "s"} for "${query}":
|
|
2057
|
+
|
|
2058
|
+
${bodyParts.join(`
|
|
2059
|
+
|
|
2060
|
+
`)}`;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
export { indexCommitsForProject, embedUnembeddedCommits, createUnifiedSearchDiagnostics, parseIdShapedQuery, resolveMemoriesByIdsForSearch, unifiedSearch, formatSearchResults };
|