@cortexkit/opencode-magic-context 0.46.1 → 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.
Files changed (203) hide show
  1. package/dist/auto-search-worker.js +115 -0
  2. package/dist/config/index.d.ts.map +1 -1
  3. package/dist/config/live-run-config.d.ts.map +1 -1
  4. package/dist/config/migrate-config-location.d.ts.map +1 -1
  5. package/dist/config/schema/magic-context.d.ts +11 -3
  6. package/dist/config/schema/magic-context.d.ts.map +1 -1
  7. package/dist/{conflict-warning-hook-7fj2b9jw.js → conflict-warning-hook-z44ynfz7.js} +5 -4
  8. package/dist/{context-sh2m2vde.js → context-wq4ffwyb.js} +108 -23
  9. package/dist/{derive-budgets-7nj7r1rv.js → derive-budgets-ke12jnnh.js} +1 -1
  10. package/dist/embedding-worker.js +2 -2
  11. package/dist/features/magic-context/dreamer/task-gates.d.ts.map +1 -1
  12. package/dist/features/magic-context/overflow-detection.d.ts +2 -0
  13. package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
  14. package/dist/features/magic-context/project-embedding-registry.d.ts +7 -0
  15. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  16. package/dist/features/magic-context/search.d.ts +2 -0
  17. package/dist/features/magic-context/search.d.ts.map +1 -1
  18. package/dist/features/magic-context/session-project-storage.d.ts +8 -2
  19. package/dist/features/magic-context/session-project-storage.d.ts.map +1 -1
  20. package/dist/features/magic-context/storage-historian-runs.d.ts +2 -5
  21. package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -1
  22. package/dist/features/magic-context/storage-meta-persisted.d.ts +12 -0
  23. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  24. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  25. package/dist/features/magic-context/storage-tags.d.ts +7 -2
  26. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  27. package/dist/features/magic-context/storage.d.ts +1 -0
  28. package/dist/features/magic-context/storage.d.ts.map +1 -1
  29. package/dist/features/magic-context/transform-decision-log.d.ts +3 -1
  30. package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
  31. package/dist/hooks/magic-context/active-anthropic-turn.d.ts +8 -0
  32. package/dist/hooks/magic-context/active-anthropic-turn.d.ts.map +1 -0
  33. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  34. package/dist/hooks/magic-context/auto-search-blocking.fixture.d.ts +2 -0
  35. package/dist/hooks/magic-context/auto-search-blocking.fixture.d.ts.map +1 -0
  36. package/dist/hooks/magic-context/auto-search-deadline.d.ts +7 -0
  37. package/dist/hooks/magic-context/auto-search-deadline.d.ts.map +1 -1
  38. package/dist/hooks/magic-context/auto-search-host-review.fixture.d.ts +3 -0
  39. package/dist/hooks/magic-context/auto-search-host-review.fixture.d.ts.map +1 -0
  40. package/dist/hooks/magic-context/auto-search-lock-review.fixture.d.ts +2 -0
  41. package/dist/hooks/magic-context/auto-search-lock-review.fixture.d.ts.map +1 -0
  42. package/dist/hooks/magic-context/auto-search-long-lock-review.fixture.d.ts +2 -0
  43. package/dist/hooks/magic-context/auto-search-long-lock-review.fixture.d.ts.map +1 -0
  44. package/dist/hooks/magic-context/auto-search-omp-host-review.fixture.d.ts +4 -0
  45. package/dist/hooks/magic-context/auto-search-omp-host-review.fixture.d.ts.map +1 -0
  46. package/dist/hooks/magic-context/auto-search-omp-loader.fixture.d.ts +3 -0
  47. package/dist/hooks/magic-context/auto-search-omp-loader.fixture.d.ts.map +1 -0
  48. package/dist/hooks/magic-context/auto-search-runner.d.ts +1 -1
  49. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  50. package/dist/hooks/magic-context/auto-search-snapshot.fixture.d.ts +4 -0
  51. package/dist/hooks/magic-context/auto-search-snapshot.fixture.d.ts.map +1 -0
  52. package/dist/hooks/magic-context/auto-search-worker-client.d.ts +69 -0
  53. package/dist/hooks/magic-context/auto-search-worker-client.d.ts.map +1 -0
  54. package/dist/hooks/magic-context/auto-search-worker.d.ts +2 -0
  55. package/dist/hooks/magic-context/auto-search-worker.d.ts.map +1 -0
  56. package/dist/hooks/magic-context/caveman-cleanup.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/compartment-runner-types.d.ts +3 -5
  58. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  59. package/dist/hooks/magic-context/compartment-runner.d.ts +2 -1
  60. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  61. package/dist/hooks/magic-context/compartment-trigger.d.ts +3 -1
  62. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  63. package/dist/hooks/magic-context/event-handler.d.ts +1 -1
  64. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  65. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  66. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  67. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
  68. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
  69. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  70. package/dist/hooks/magic-context/historian-drain-gate.d.ts +11 -0
  71. package/dist/hooks/magic-context/historian-drain-gate.d.ts.map +1 -0
  72. package/dist/hooks/magic-context/hook.d.ts +2 -1
  73. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  74. package/dist/hooks/magic-context/latest-assistant-turn.d.ts +25 -0
  75. package/dist/hooks/magic-context/latest-assistant-turn.d.ts.map +1 -0
  76. package/dist/hooks/magic-context/latest-thinking-recovery.d.ts +61 -0
  77. package/dist/hooks/magic-context/latest-thinking-recovery.d.ts.map +1 -0
  78. package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
  79. package/dist/hooks/magic-context/opencode-proven-floor.d.ts +6 -0
  80. package/dist/hooks/magic-context/opencode-proven-floor.d.ts.map +1 -0
  81. package/dist/hooks/magic-context/pass-outcome.d.ts.map +1 -1
  82. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  83. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  84. package/dist/hooks/magic-context/reasoning-budget-status.d.ts +15 -0
  85. package/dist/hooks/magic-context/reasoning-budget-status.d.ts.map +1 -0
  86. package/dist/hooks/magic-context/reasoning-budget.d.ts +40 -0
  87. package/dist/hooks/magic-context/reasoning-budget.d.ts.map +1 -0
  88. package/dist/hooks/magic-context/reasoning-removal.d.ts +6 -6
  89. package/dist/hooks/magic-context/reasoning-removal.d.ts.map +1 -1
  90. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/sentinel.d.ts +4 -0
  92. package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/strip-content.d.ts +7 -3
  94. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/tag-messages.d.ts +13 -0
  96. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  97. package/dist/hooks/magic-context/tool-drop-target.d.ts +5 -0
  98. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -28
  100. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  101. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +28 -6
  102. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  103. package/dist/hooks/magic-context/transform.d.ts +3 -1
  104. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  105. package/dist/{index-0d3g3461.js → index-0c82743p.js} +2 -1
  106. package/dist/{index-5p8jdswx.js → index-5a2r0p9f.js} +3 -3
  107. package/dist/{index-sjthx78w.js → index-6d9e1m08.js} +4647 -11000
  108. package/dist/{index-majxda46.js → index-6wkcswft.js} +1 -1
  109. package/dist/index-92bp57hg.js +2063 -0
  110. package/dist/{index-sfnj6h8h.js → index-9pk9exrb.js} +3 -3
  111. package/dist/{index-7qw742ev.js → index-adwfkpex.js} +5 -24
  112. package/dist/{index-3h0ybwah.js → index-bb36vgr3.js} +80 -12
  113. package/dist/{index-pkbcrdqs.js → index-cwf6s3ay.js} +1072 -2422
  114. package/dist/index-fnn5pjtt.js +6554 -0
  115. package/dist/{index-sgthv3f7.js → index-gg8j07zz.js} +1 -1
  116. package/dist/{index-8x2v7gg9.js → index-m5fe6jch.js} +7 -3
  117. package/dist/{v2/server-g0fjrkta.js → index-rrbbspwh.js} +42 -27
  118. package/dist/index-rrrsepyw.js +3464 -0
  119. package/dist/{index-hpecdzec.js → index-w2t4ah5j.js} +948 -501
  120. package/dist/{v2/server-fg34g6a5.js → index-wx8a6bw3.js} +48 -8
  121. package/dist/{index-r5hmbx3t.js → index-x3kwrmqk.js} +146 -3571
  122. package/dist/index-yr3zj7e7.js +23 -0
  123. package/dist/index.d.ts.map +1 -1
  124. package/dist/index.js +624 -65
  125. package/dist/{message-time-backfill-68gj6t3e.js → message-time-backfill-r5epkzj3.js} +1 -1
  126. package/dist/migration-worker.js +7 -5
  127. package/dist/plugin/hooks/create-session-hooks.d.ts +2 -1
  128. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  129. package/dist/plugin/host-stall-profiler-report.d.ts +67 -0
  130. package/dist/plugin/host-stall-profiler-report.d.ts.map +1 -0
  131. package/dist/plugin/host-stall-profiler.d.ts +96 -0
  132. package/dist/plugin/host-stall-profiler.d.ts.map +1 -0
  133. package/dist/plugin/messages-transform.d.ts +21 -6
  134. package/dist/plugin/messages-transform.d.ts.map +1 -1
  135. package/dist/{read-session-chunk-egz7r68d.js → read-session-chunk-4jey7hpt.js} +4 -4
  136. package/dist/{recomp-orchestrator-y15j0rb9.js → recomp-orchestrator-rervg6q4.js} +13 -10
  137. package/dist/{send-session-notification-dyw42dtr.js → send-session-notification-0m68ms3n.js} +4 -3
  138. package/dist/shared/auto-search-lifecycle.d.ts +7 -0
  139. package/dist/shared/auto-search-lifecycle.d.ts.map +1 -0
  140. package/dist/shared/historian-tool-expansions.d.ts +1 -1
  141. package/dist/shared/historian-tool-expansions.d.ts.map +1 -1
  142. package/dist/shared/historian-tool-template.d.ts +2 -2
  143. package/dist/shared/historian-tool-template.d.ts.map +1 -1
  144. package/dist/shared/sqlite.d.ts +16 -1
  145. package/dist/shared/sqlite.d.ts.map +1 -1
  146. package/dist/shared/status-view.d.ts.map +1 -1
  147. package/dist/shared/window-geometry.d.ts +4 -0
  148. package/dist/shared/window-geometry.d.ts.map +1 -1
  149. package/dist/tools/ctx-expand/render.d.ts.map +1 -1
  150. package/dist/{transform-postprocess-phase-bqhz6vdr.js → transform-postprocess-phase-z5hzpsxv.js} +13 -10
  151. package/dist/v2/auto-search-worker.js +115 -0
  152. package/dist/v2/{context-zecmzk5s.js → context-14v1hr8b.js} +3783 -4668
  153. package/dist/v2/{derive-budgets-kdgp4s3r.js → derive-budgets-nc9avxz8.js} +2 -2
  154. package/dist/v2/embedding-worker.js +3 -3
  155. package/dist/v2/hidden-completion.d.ts.map +1 -1
  156. package/dist/v2/hooks/context.d.ts.map +1 -1
  157. package/dist/v2/{message-time-backfill-r6qkem1x.js → message-time-backfill-1f9tzvcs.js} +1 -1
  158. package/dist/v2/migration-worker.js +7 -5
  159. package/dist/v2/{read-session-chunk-8vjp6581.js → read-session-chunk-gyyv7w2r.js} +4 -4
  160. package/dist/v2/{recomp-orchestrator-8ejxvrzz.js → recomp-orchestrator-42vjba7p.js} +17 -14
  161. package/dist/v2/{send-session-notification-qs0j78v6.js → send-session-notification-c0wtscea.js} +5 -4
  162. package/dist/v2/server-3trjk1hx.js +6360 -0
  163. package/dist/v2/{server-e29d7p7c.js → server-50hvkg3v.js} +7 -3
  164. package/dist/v2/{server-y8maxeft.js → server-cpepxy2q.js} +2 -1
  165. package/dist/v2/server-d0sraw7m.js +3403 -0
  166. package/dist/v2/server-e0c1b7a7.js +2063 -0
  167. package/dist/v2/{server-mcfawjgc.js → server-efhwzx05.js} +5 -24
  168. package/dist/v2/{server-3n4q9xme.js → server-fcdehk1s.js} +1 -1
  169. package/dist/{index-q4yp4b52.js → v2/server-fq7kq32k.js} +42 -27
  170. package/dist/v2/{server-jc7y02q0.js → server-nf82q6pt.js} +1117 -7290
  171. package/dist/v2/{server-frbm99q5.js → server-njt3np1k.js} +99 -3465
  172. package/dist/v2/{server-qp5hx1ad.js → server-nm9n3aqq.js} +107 -12
  173. package/dist/v2/{server-c5gz8wph.js → server-rz514f14.js} +7 -2
  174. package/dist/v2/{server-wvjvfhck.js → server-vd4e7174.js} +1 -1
  175. package/dist/{index-fg34g6a5.js → v2/server-wx8a6bw3.js} +48 -8
  176. package/dist/v2/{server-xa4xe3rv.js → server-y10vbq6y.js} +1 -1
  177. package/dist/v2/server-yr3zj7e7.js +23 -0
  178. package/dist/v2/server.js +5 -5
  179. package/dist/v2/{wrapup-orchestrator-0snhvwcv.js → wrapup-orchestrator-nbz854vc.js} +16 -13
  180. package/dist/wrapup-orchestrator-b2dztrar.js +29 -0
  181. package/package.json +3 -3
  182. package/src/config/index.ts +6 -0
  183. package/src/config/live-run-config.test.ts +37 -1
  184. package/src/config/live-run-config.ts +28 -0
  185. package/src/config/migrate-config-location.ts +10 -3
  186. package/src/config/schema/magic-context.test.ts +4 -3
  187. package/src/config/schema/magic-context.ts +18 -6
  188. package/src/shared/auto-search-lifecycle.ts +27 -0
  189. package/src/shared/historian-tool-defaults.json +6 -6
  190. package/src/shared/historian-tool-expansions.ts +65 -8
  191. package/src/shared/historian-tool-preview-defaults.json +13 -0
  192. package/src/shared/historian-tool-template.ts +48 -14
  193. package/src/shared/sqlite-640-review-r2.test.ts +136 -0
  194. package/src/shared/sqlite-640-review.test.ts +183 -0
  195. package/src/shared/sqlite-acquisition-routing.test.ts +18 -0
  196. package/src/shared/sqlite.ts +84 -14
  197. package/src/shared/status-view.test.ts +6 -1
  198. package/src/shared/status-view.ts +15 -1
  199. package/src/shared/tag-transcript.ts +6 -0
  200. package/src/shared/window-geometry.ts +10 -0
  201. package/dist/wrapup-orchestrator-39amhv9s.js +0 -26
  202. /package/dist/{index-en1k8h8m.js → index-5prbx026.js} +0 -0
  203. /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 };