sensemaking 0.25.1 → 0.26.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 (316) hide show
  1. package/README.md +13 -9
  2. package/dist/cjs/cli/index.d.cts +1 -1
  3. package/dist/cjs/cli/index.d.ts +1 -1
  4. package/dist/cjs/cli/index.js +1 -1
  5. package/dist/cjs/cli/index.js.map +1 -1
  6. package/dist/cjs/cli/path.js +5 -29
  7. package/dist/cjs/cli/path.js.map +1 -1
  8. package/dist/cjs/cli/related.js +4 -3
  9. package/dist/cjs/cli/related.js.map +1 -1
  10. package/dist/cjs/commands/index.d.cts +6 -3
  11. package/dist/cjs/commands/index.d.ts +6 -3
  12. package/dist/cjs/commands/index.js +4 -0
  13. package/dist/cjs/commands/index.js.map +1 -1
  14. package/dist/cjs/commands/map.d.cts +14 -5
  15. package/dist/cjs/commands/map.d.ts +14 -5
  16. package/dist/cjs/commands/map.js +42 -35
  17. package/dist/cjs/commands/map.js.map +1 -1
  18. package/dist/cjs/commands/path.d.cts +14 -0
  19. package/dist/cjs/commands/path.d.ts +14 -0
  20. package/dist/cjs/commands/path.js +209 -0
  21. package/dist/cjs/commands/path.js.map +1 -0
  22. package/dist/cjs/commands/peek.d.cts +16 -1
  23. package/dist/cjs/commands/peek.d.ts +16 -1
  24. package/dist/cjs/commands/peek.js +71 -33
  25. package/dist/cjs/commands/peek.js.map +1 -1
  26. package/dist/cjs/commands/related.d.cts +16 -3
  27. package/dist/cjs/commands/related.d.ts +16 -3
  28. package/dist/cjs/commands/related.js +131 -98
  29. package/dist/cjs/commands/related.js.map +1 -1
  30. package/dist/cjs/commands/scope.d.cts +1 -0
  31. package/dist/cjs/commands/scope.d.ts +1 -0
  32. package/dist/cjs/commands/scope.js +149 -0
  33. package/dist/cjs/commands/scope.js.map +1 -1
  34. package/dist/cjs/commands/search.d.cts +26 -3
  35. package/dist/cjs/commands/search.d.ts +26 -3
  36. package/dist/cjs/commands/search.js +211 -71
  37. package/dist/cjs/commands/search.js.map +1 -1
  38. package/dist/cjs/commands/signals.d.cts +4 -3
  39. package/dist/cjs/commands/signals.d.ts +4 -3
  40. package/dist/cjs/commands/signals.js +13 -4
  41. package/dist/cjs/commands/signals.js.map +1 -1
  42. package/dist/cjs/commands/snapshot.d.cts +3 -0
  43. package/dist/cjs/commands/snapshot.d.ts +3 -0
  44. package/dist/cjs/commands/snapshot.js +180 -0
  45. package/dist/cjs/commands/snapshot.js.map +1 -0
  46. package/dist/cjs/embed/cohere.js +41 -16
  47. package/dist/cjs/embed/cohere.js.map +1 -1
  48. package/dist/cjs/embed/endpoint.d.cts +2 -0
  49. package/dist/cjs/embed/endpoint.d.ts +2 -0
  50. package/dist/cjs/embed/endpoint.js +30 -0
  51. package/dist/cjs/embed/endpoint.js.map +1 -0
  52. package/dist/cjs/embed/handoff.d.cts +7 -2
  53. package/dist/cjs/embed/handoff.d.ts +7 -2
  54. package/dist/cjs/embed/handoff.js +10 -0
  55. package/dist/cjs/embed/handoff.js.map +1 -1
  56. package/dist/cjs/embed/http.d.cts +18 -1
  57. package/dist/cjs/embed/http.d.ts +18 -1
  58. package/dist/cjs/embed/http.js +239 -40
  59. package/dist/cjs/embed/http.js.map +1 -1
  60. package/dist/cjs/embed/openai.js +39 -17
  61. package/dist/cjs/embed/openai.js.map +1 -1
  62. package/dist/cjs/embed/query.d.cts +9 -3
  63. package/dist/cjs/embed/query.d.ts +9 -3
  64. package/dist/cjs/embed/query.js +257 -90
  65. package/dist/cjs/embed/query.js.map +1 -1
  66. package/dist/cjs/embed/registry.d.cts +2 -2
  67. package/dist/cjs/embed/registry.d.ts +2 -2
  68. package/dist/cjs/embed/registry.js +36 -4
  69. package/dist/cjs/embed/registry.js.map +1 -1
  70. package/dist/cjs/embed/static.js +6 -2
  71. package/dist/cjs/embed/static.js.map +1 -1
  72. package/dist/cjs/embed/store.js +42 -77
  73. package/dist/cjs/embed/store.js.map +1 -1
  74. package/dist/cjs/embed/types.d.cts +5 -2
  75. package/dist/cjs/embed/types.d.ts +5 -2
  76. package/dist/cjs/embed/types.js.map +1 -1
  77. package/dist/cjs/features/embed.js +290 -43
  78. package/dist/cjs/features/embed.js.map +1 -1
  79. package/dist/cjs/graph/traverse.d.cts +2 -2
  80. package/dist/cjs/graph/traverse.d.ts +2 -2
  81. package/dist/cjs/graph/traverse.js +15 -5
  82. package/dist/cjs/graph/traverse.js.map +1 -1
  83. package/dist/cjs/index.d.cts +2 -2
  84. package/dist/cjs/index.d.ts +2 -2
  85. package/dist/cjs/index.js +6 -0
  86. package/dist/cjs/index.js.map +1 -1
  87. package/dist/cjs/output/output.d.cts +4 -36
  88. package/dist/cjs/output/output.d.ts +4 -36
  89. package/dist/cjs/output/output.js +1 -2
  90. package/dist/cjs/output/output.js.map +1 -1
  91. package/dist/cjs/scan/pool.d.cts +2 -1
  92. package/dist/cjs/scan/pool.d.ts +2 -1
  93. package/dist/cjs/scan/pool.js +116 -27
  94. package/dist/cjs/scan/pool.js.map +1 -1
  95. package/dist/cjs/scan/reparse.d.cts +1 -0
  96. package/dist/cjs/scan/reparse.d.ts +1 -0
  97. package/dist/cjs/scan/reparse.js +75 -21
  98. package/dist/cjs/scan/reparse.js.map +1 -1
  99. package/dist/cjs/store/builder.d.cts +11 -21
  100. package/dist/cjs/store/builder.d.ts +11 -21
  101. package/dist/cjs/store/builder.js +1077 -438
  102. package/dist/cjs/store/builder.js.map +1 -1
  103. package/dist/cjs/store/duckdb/connection.js +16 -2
  104. package/dist/cjs/store/duckdb/connection.js.map +1 -1
  105. package/dist/cjs/store/duckdb/open.d.cts +1 -1
  106. package/dist/cjs/store/duckdb/open.d.ts +1 -1
  107. package/dist/cjs/store/duckdb/open.js +73 -65
  108. package/dist/cjs/store/duckdb/open.js.map +1 -1
  109. package/dist/cjs/store/duckdb/reconcile.js +533 -15
  110. package/dist/cjs/store/duckdb/reconcile.js.map +1 -1
  111. package/dist/cjs/store/embed-scope.js +19 -9
  112. package/dist/cjs/store/embed-scope.js.map +1 -1
  113. package/dist/cjs/store/index.d.cts +6 -0
  114. package/dist/cjs/store/index.d.ts +6 -0
  115. package/dist/cjs/store/index.js +8 -4
  116. package/dist/cjs/store/index.js.map +1 -1
  117. package/dist/cjs/store/open.d.cts +6 -1
  118. package/dist/cjs/store/open.d.ts +6 -1
  119. package/dist/cjs/store/open.js +230 -307
  120. package/dist/cjs/store/open.js.map +1 -1
  121. package/dist/cjs/store/readiness.d.cts +5 -0
  122. package/dist/cjs/store/readiness.d.ts +5 -0
  123. package/dist/cjs/store/readiness.js +39 -0
  124. package/dist/cjs/store/readiness.js.map +1 -0
  125. package/dist/cjs/store/reconcile.d.cts +26 -1
  126. package/dist/cjs/store/reconcile.d.ts +26 -1
  127. package/dist/cjs/store/reconcile.js +670 -576
  128. package/dist/cjs/store/reconcile.js.map +1 -1
  129. package/dist/cjs/store/signature.d.cts +1 -0
  130. package/dist/cjs/store/signature.d.ts +1 -0
  131. package/dist/cjs/store/signature.js +9 -0
  132. package/dist/cjs/store/signature.js.map +1 -1
  133. package/dist/cjs/store/sqlite/open.d.cts +1 -1
  134. package/dist/cjs/store/sqlite/open.d.ts +1 -1
  135. package/dist/cjs/store/sqlite/open.js +84 -90
  136. package/dist/cjs/store/sqlite/open.js.map +1 -1
  137. package/dist/cjs/store/sqlite/reconcile.js.map +1 -1
  138. package/dist/cjs/store/sqlite/store.js +63 -60
  139. package/dist/cjs/store/sqlite/store.js.map +1 -1
  140. package/dist/cjs/store/sqlite/vectors.d.cts +2 -2
  141. package/dist/cjs/store/sqlite/vectors.d.ts +2 -2
  142. package/dist/cjs/store/sqlite/vectors.js +8 -4
  143. package/dist/cjs/store/sqlite/vectors.js.map +1 -1
  144. package/dist/cjs/store/stages.js.map +1 -1
  145. package/dist/cjs/store/turso/connection.d.cts +1 -0
  146. package/dist/cjs/store/turso/connection.d.ts +1 -0
  147. package/dist/cjs/store/turso/connection.js +319 -94
  148. package/dist/cjs/store/turso/connection.js.map +1 -1
  149. package/dist/cjs/store/turso/open.d.cts +1 -1
  150. package/dist/cjs/store/turso/open.d.ts +1 -1
  151. package/dist/cjs/store/turso/open.js +70 -74
  152. package/dist/cjs/store/turso/open.js.map +1 -1
  153. package/dist/cjs/store/turso/store.js +25 -23
  154. package/dist/cjs/store/turso/store.js.map +1 -1
  155. package/dist/cjs/store/turso/vectors.d.cts +2 -2
  156. package/dist/cjs/store/turso/vectors.d.ts +2 -2
  157. package/dist/cjs/store/turso/vectors.js +26 -16
  158. package/dist/cjs/store/turso/vectors.js.map +1 -1
  159. package/dist/cjs/store/types.d.cts +4 -1
  160. package/dist/cjs/store/types.d.ts +4 -1
  161. package/dist/cjs/store/types.js.map +1 -1
  162. package/dist/cjs/store/vectors.d.cts +14 -1
  163. package/dist/cjs/store/vectors.d.ts +14 -1
  164. package/dist/cjs/store/vectors.js +61 -0
  165. package/dist/cjs/store/vectors.js.map +1 -1
  166. package/dist/cjs/watch.d.cts +6 -0
  167. package/dist/cjs/watch.d.ts +6 -0
  168. package/dist/cjs/watch.js +42 -10
  169. package/dist/cjs/watch.js.map +1 -1
  170. package/dist/cjs/workers/parse.d.cts +4 -3
  171. package/dist/cjs/workers/parse.d.ts +4 -3
  172. package/dist/cjs/workers/parse.js +19 -17
  173. package/dist/cjs/workers/parse.js.map +1 -1
  174. package/dist/esm/cli/index.d.ts +1 -1
  175. package/dist/esm/cli/index.js +1 -1
  176. package/dist/esm/cli/index.js.map +1 -1
  177. package/dist/esm/cli/path.js +4 -10
  178. package/dist/esm/cli/path.js.map +1 -1
  179. package/dist/esm/cli/related.js +5 -3
  180. package/dist/esm/cli/related.js.map +1 -1
  181. package/dist/esm/commands/index.d.ts +6 -3
  182. package/dist/esm/commands/index.js +1 -0
  183. package/dist/esm/commands/index.js.map +1 -1
  184. package/dist/esm/commands/map.d.ts +14 -5
  185. package/dist/esm/commands/map.js +2 -0
  186. package/dist/esm/commands/map.js.map +1 -1
  187. package/dist/esm/commands/path.d.ts +14 -0
  188. package/dist/esm/commands/path.js +33 -0
  189. package/dist/esm/commands/path.js.map +1 -0
  190. package/dist/esm/commands/peek.d.ts +16 -1
  191. package/dist/esm/commands/peek.js +35 -9
  192. package/dist/esm/commands/peek.js.map +1 -1
  193. package/dist/esm/commands/related.d.ts +16 -3
  194. package/dist/esm/commands/related.js +45 -28
  195. package/dist/esm/commands/related.js.map +1 -1
  196. package/dist/esm/commands/scope.d.ts +1 -0
  197. package/dist/esm/commands/scope.js +58 -0
  198. package/dist/esm/commands/scope.js.map +1 -1
  199. package/dist/esm/commands/search.d.ts +26 -3
  200. package/dist/esm/commands/search.js +85 -18
  201. package/dist/esm/commands/search.js.map +1 -1
  202. package/dist/esm/commands/signals.d.ts +4 -3
  203. package/dist/esm/commands/signals.js +11 -4
  204. package/dist/esm/commands/signals.js.map +1 -1
  205. package/dist/esm/commands/snapshot.d.ts +3 -0
  206. package/dist/esm/commands/snapshot.js +25 -0
  207. package/dist/esm/commands/snapshot.js.map +1 -0
  208. package/dist/esm/embed/cohere.js +35 -10
  209. package/dist/esm/embed/cohere.js.map +1 -1
  210. package/dist/esm/embed/endpoint.d.ts +2 -0
  211. package/dist/esm/embed/endpoint.js +11 -0
  212. package/dist/esm/embed/endpoint.js.map +1 -0
  213. package/dist/esm/embed/handoff.d.ts +7 -2
  214. package/dist/esm/embed/handoff.js +7 -0
  215. package/dist/esm/embed/handoff.js.map +1 -1
  216. package/dist/esm/embed/http.d.ts +18 -1
  217. package/dist/esm/embed/http.js +137 -17
  218. package/dist/esm/embed/http.js.map +1 -1
  219. package/dist/esm/embed/openai.js +34 -12
  220. package/dist/esm/embed/openai.js.map +1 -1
  221. package/dist/esm/embed/query.d.ts +9 -3
  222. package/dist/esm/embed/query.js +98 -26
  223. package/dist/esm/embed/query.js.map +1 -1
  224. package/dist/esm/embed/registry.d.ts +2 -2
  225. package/dist/esm/embed/registry.js +34 -5
  226. package/dist/esm/embed/registry.js.map +1 -1
  227. package/dist/esm/embed/static.js +10 -2
  228. package/dist/esm/embed/static.js.map +1 -1
  229. package/dist/esm/embed/store.js +30 -21
  230. package/dist/esm/embed/store.js.map +1 -1
  231. package/dist/esm/embed/types.d.ts +5 -2
  232. package/dist/esm/embed/types.js.map +1 -1
  233. package/dist/esm/features/embed.js +82 -21
  234. package/dist/esm/features/embed.js.map +1 -1
  235. package/dist/esm/graph/traverse.d.ts +2 -2
  236. package/dist/esm/graph/traverse.js +4 -2
  237. package/dist/esm/graph/traverse.js.map +1 -1
  238. package/dist/esm/index.d.ts +2 -2
  239. package/dist/esm/index.js +1 -1
  240. package/dist/esm/index.js.map +1 -1
  241. package/dist/esm/output/output.d.ts +4 -36
  242. package/dist/esm/output/output.js +1 -2
  243. package/dist/esm/output/output.js.map +1 -1
  244. package/dist/esm/scan/pool.d.ts +2 -1
  245. package/dist/esm/scan/pool.js +80 -23
  246. package/dist/esm/scan/pool.js.map +1 -1
  247. package/dist/esm/scan/reparse.d.ts +1 -0
  248. package/dist/esm/scan/reparse.js +40 -12
  249. package/dist/esm/scan/reparse.js.map +1 -1
  250. package/dist/esm/store/builder.d.ts +11 -21
  251. package/dist/esm/store/builder.js +337 -133
  252. package/dist/esm/store/builder.js.map +1 -1
  253. package/dist/esm/store/duckdb/connection.js +9 -1
  254. package/dist/esm/store/duckdb/connection.js.map +1 -1
  255. package/dist/esm/store/duckdb/open.d.ts +1 -1
  256. package/dist/esm/store/duckdb/open.js +13 -9
  257. package/dist/esm/store/duckdb/open.js.map +1 -1
  258. package/dist/esm/store/duckdb/reconcile.js +145 -10
  259. package/dist/esm/store/duckdb/reconcile.js.map +1 -1
  260. package/dist/esm/store/embed-scope.js +16 -6
  261. package/dist/esm/store/embed-scope.js.map +1 -1
  262. package/dist/esm/store/index.d.ts +6 -0
  263. package/dist/esm/store/index.js +10 -4
  264. package/dist/esm/store/index.js.map +1 -1
  265. package/dist/esm/store/open.d.ts +6 -1
  266. package/dist/esm/store/open.js +77 -87
  267. package/dist/esm/store/open.js.map +1 -1
  268. package/dist/esm/store/readiness.d.ts +5 -0
  269. package/dist/esm/store/readiness.js +11 -0
  270. package/dist/esm/store/readiness.js.map +1 -0
  271. package/dist/esm/store/reconcile.d.ts +26 -1
  272. package/dist/esm/store/reconcile.js +142 -120
  273. package/dist/esm/store/reconcile.js.map +1 -1
  274. package/dist/esm/store/signature.d.ts +1 -0
  275. package/dist/esm/store/signature.js +8 -0
  276. package/dist/esm/store/signature.js.map +1 -1
  277. package/dist/esm/store/sqlite/open.d.ts +1 -1
  278. package/dist/esm/store/sqlite/open.js +12 -12
  279. package/dist/esm/store/sqlite/open.js.map +1 -1
  280. package/dist/esm/store/sqlite/reconcile.js +2 -2
  281. package/dist/esm/store/sqlite/reconcile.js.map +1 -1
  282. package/dist/esm/store/sqlite/store.js +21 -18
  283. package/dist/esm/store/sqlite/store.js.map +1 -1
  284. package/dist/esm/store/sqlite/vectors.d.ts +2 -2
  285. package/dist/esm/store/sqlite/vectors.js +7 -5
  286. package/dist/esm/store/sqlite/vectors.js.map +1 -1
  287. package/dist/esm/store/stages.js.map +1 -1
  288. package/dist/esm/store/turso/connection.d.ts +1 -0
  289. package/dist/esm/store/turso/connection.js +97 -28
  290. package/dist/esm/store/turso/connection.js.map +1 -1
  291. package/dist/esm/store/turso/open.d.ts +1 -1
  292. package/dist/esm/store/turso/open.js +9 -8
  293. package/dist/esm/store/turso/open.js.map +1 -1
  294. package/dist/esm/store/turso/store.js +16 -14
  295. package/dist/esm/store/turso/store.js.map +1 -1
  296. package/dist/esm/store/turso/vectors.d.ts +2 -2
  297. package/dist/esm/store/turso/vectors.js +27 -17
  298. package/dist/esm/store/turso/vectors.js.map +1 -1
  299. package/dist/esm/store/types.d.ts +4 -1
  300. package/dist/esm/store/types.js.map +1 -1
  301. package/dist/esm/store/vectors.d.ts +14 -1
  302. package/dist/esm/store/vectors.js +55 -0
  303. package/dist/esm/store/vectors.js.map +1 -1
  304. package/dist/esm/watch.d.ts +6 -0
  305. package/dist/esm/watch.js +40 -14
  306. package/dist/esm/watch.js.map +1 -1
  307. package/dist/esm/workers/parse.d.ts +4 -3
  308. package/dist/esm/workers/parse.js +19 -17
  309. package/dist/esm/workers/parse.js.map +1 -1
  310. package/package.json +1 -1
  311. package/skills/sense/SKILL.md +4 -2
  312. package/skills/sense/references/stores/duckdb.md +1 -1
  313. package/skills/sense-setup/SKILL.md +2 -2
  314. package/skills/sense-setup/references/embeddings.md +1 -1
  315. package/skills/sense-setup/references/store-benchmarks.md +5 -5
  316. package/skills/sense-setup/references/stores/duckdb.md +1 -1
@@ -1,33 +1,153 @@
1
+ import { SenseError } from '../errors.js';
1
2
  const MAX_ATTEMPTS = 3;
2
- function sleep(ms) {
3
- return new Promise((resolve)=>setTimeout(resolve, ms));
3
+ const MAX_RETRY_AFTER_MS = 10000;
4
+ const DEFAULT_BASE_DELAY_MS = 200;
5
+ // Initial policy bounds small API/metadata responses separately from model-file downloads.
6
+ export const HTTP_ATTEMPT_TIMEOUT_MS = 30000;
7
+ export const MODEL_FILE_ATTEMPT_TIMEOUT_MS = 5 * 60000;
8
+ function safeUrl(url) {
9
+ try {
10
+ const parsed = new URL(url);
11
+ parsed.username = '';
12
+ parsed.password = '';
13
+ parsed.search = '';
14
+ parsed.hash = '';
15
+ return parsed.href;
16
+ } catch {
17
+ return 'the configured embedding endpoint';
18
+ }
19
+ }
20
+ function attemptController(callerSignal, timeoutMs) {
21
+ callerSignal === null || callerSignal === void 0 ? void 0 : callerSignal.throwIfAborted();
22
+ const controller = new AbortController();
23
+ const timeoutReason = {};
24
+ const relayAbort = ()=>controller.abort(callerSignal === null || callerSignal === void 0 ? void 0 : callerSignal.reason);
25
+ callerSignal === null || callerSignal === void 0 ? void 0 : callerSignal.addEventListener('abort', relayAbort, {
26
+ once: true
27
+ });
28
+ if (callerSignal === null || callerSignal === void 0 ? void 0 : callerSignal.aborted) relayAbort();
29
+ const timer = setTimeout(()=>controller.abort(timeoutReason), timeoutMs);
30
+ return {
31
+ controller,
32
+ timeoutReason,
33
+ cleanup: ()=>{
34
+ clearTimeout(timer);
35
+ callerSignal === null || callerSignal === void 0 ? void 0 : callerSignal.removeEventListener('abort', relayAbort);
36
+ }
37
+ };
38
+ }
39
+ function disposeResponse(response, controller) {
40
+ const cancellation = response.body && !response.bodyUsed ? response.body.cancel() : undefined;
41
+ controller.abort();
42
+ // The controller owns socket cleanup; observe stream cancellation without delaying the attempt.
43
+ if (cancellation) void cancellation.then(()=>undefined, ()=>undefined);
44
+ }
45
+ function sleep(ms, signal) {
46
+ signal === null || signal === void 0 ? void 0 : signal.throwIfAborted();
47
+ if (!signal) return new Promise((resolve)=>setTimeout(resolve, ms));
48
+ return new Promise((resolve, reject)=>{
49
+ let timer;
50
+ const cleanup = ()=>{
51
+ if (timer) clearTimeout(timer);
52
+ signal.removeEventListener('abort', onAbort);
53
+ };
54
+ const onAbort = ()=>{
55
+ cleanup();
56
+ reject(signal.reason);
57
+ };
58
+ signal.addEventListener('abort', onAbort, {
59
+ once: true
60
+ });
61
+ timer = setTimeout(()=>{
62
+ cleanup();
63
+ resolve();
64
+ }, ms);
65
+ if (signal.aborted) onAbort();
66
+ });
67
+ }
68
+ function failedRequest(url, phase, attempts, timeoutMs, timedOut) {
69
+ const endpoint = safeUrl(url);
70
+ if (timedOut) {
71
+ return new SenseError('EMBED_MODEL', `${endpoint} timed out after ${timeoutMs}ms while waiting for ${phase} (attempt ${attempts}/${MAX_ATTEMPTS}); check the embedding service and network`);
72
+ }
73
+ return new SenseError('EMBED_MODEL', `${endpoint} failed while waiting for ${phase} after ${attempts} attempts; check the embedding service and network`);
74
+ }
75
+ function buffered(response, body, requestUrl) {
76
+ return {
77
+ ok: response.ok,
78
+ status: response.status,
79
+ headers: response.headers,
80
+ body,
81
+ url: safeUrl(response.url || requestUrl)
82
+ };
4
83
  }
5
- // Shared retry glue for the HTTP providers: 429/5xx/network failure retry with exponential
6
- // backoff honoring Retry-After; any other status returns immediately for the caller to handle.
7
- export async function fetchWithRetry(url, init, baseDelayMs = 200) {
84
+ // Each attempt covers response headers and the complete successful body. Retry cleanup and
85
+ // backoff honor one caller signal without transferring ownership of that signal.
86
+ export async function fetchWithRetry(url, init, options = {}) {
87
+ var _options_attemptTimeoutMs, _options_baseDelayMs;
88
+ const timeoutMs = (_options_attemptTimeoutMs = options.attemptTimeoutMs) !== null && _options_attemptTimeoutMs !== void 0 ? _options_attemptTimeoutMs : HTTP_ATTEMPT_TIMEOUT_MS;
89
+ const baseDelayMs = (_options_baseDelayMs = options.baseDelayMs) !== null && _options_baseDelayMs !== void 0 ? _options_baseDelayMs : DEFAULT_BASE_DELAY_MS;
8
90
  for(let attempt = 0; attempt < MAX_ATTEMPTS; attempt++){
91
+ var _options_signal;
92
+ (_options_signal = options.signal) === null || _options_signal === void 0 ? void 0 : _options_signal.throwIfAborted();
93
+ const { controller, timeoutReason, cleanup } = attemptController(options.signal, timeoutMs);
94
+ let response;
95
+ let phase = 'response headers';
96
+ let retryDelayMs;
9
97
  try {
10
- const res = await fetch(url, init);
11
- if (res.ok || res.status !== 429 && res.status < 500 || attempt === MAX_ATTEMPTS - 1) return res;
12
- const retryAfter = Number(res.headers.get('retry-after'));
13
- // Capped: a server naming a huge Retry-After must not stall an embed run for it.
14
- await sleep(retryAfter > 0 ? Math.min(retryAfter * 1000, 10000) : baseDelayMs * 2 ** attempt);
15
- } catch (err) {
16
- if (attempt === MAX_ATTEMPTS - 1) throw err;
17
- await sleep(baseDelayMs * 2 ** attempt);
98
+ response = await fetch(url, {
99
+ ...init,
100
+ signal: controller.signal
101
+ });
102
+ if (!response.ok) {
103
+ const retryable = response.status === 429 || response.status >= 500;
104
+ if (retryable && attempt < MAX_ATTEMPTS - 1) {
105
+ const retryAfter = Number(response.headers.get('retry-after'));
106
+ retryDelayMs = retryAfter > 0 ? Math.min(retryAfter * 1000, MAX_RETRY_AFTER_MS) : baseDelayMs * 2 ** attempt;
107
+ disposeResponse(response, controller);
108
+ } else {
109
+ var _options_signal1;
110
+ disposeResponse(response, controller);
111
+ (_options_signal1 = options.signal) === null || _options_signal1 === void 0 ? void 0 : _options_signal1.throwIfAborted();
112
+ return buffered(response, new ArrayBuffer(0), url);
113
+ }
114
+ } else {
115
+ var _options_signal2;
116
+ phase = 'response body';
117
+ const body = await response.arrayBuffer();
118
+ (_options_signal2 = options.signal) === null || _options_signal2 === void 0 ? void 0 : _options_signal2.throwIfAborted();
119
+ return buffered(response, body, url);
120
+ }
121
+ } catch {
122
+ try {
123
+ var _options_signal3;
124
+ (_options_signal3 = options.signal) === null || _options_signal3 === void 0 ? void 0 : _options_signal3.throwIfAborted();
125
+ } finally{
126
+ if (response) disposeResponse(response, controller);
127
+ }
128
+ const timedOut = controller.signal.reason === timeoutReason;
129
+ if (attempt === MAX_ATTEMPTS - 1) throw failedRequest(url, phase, attempt + 1, timeoutMs, timedOut);
130
+ retryDelayMs = baseDelayMs * 2 ** attempt;
131
+ } finally{
132
+ cleanup();
18
133
  }
134
+ await sleep(retryDelayMs !== null && retryDelayMs !== void 0 ? retryDelayMs : baseDelayMs * 2 ** attempt, options.signal);
19
135
  }
20
136
  throw new Error('unreachable');
21
137
  }
22
- // Any HTTP response means the endpoint is up; only a network failure or the timeout counts as
23
- // unreachable, so an air-gapped machine never waits more than timeoutMs on `sense status`.
138
+ // Any HTTP response means the endpoint is up. Abort it immediately because status does not
139
+ // consume its body, and bound the headers wait for an air-gapped machine.
24
140
  export async function probeReachable(url, timeoutMs = 1500) {
141
+ const { controller, cleanup } = attemptController(undefined, timeoutMs);
25
142
  try {
26
- await fetch(url, {
27
- signal: AbortSignal.timeout(timeoutMs)
143
+ const response = await fetch(url, {
144
+ signal: controller.signal
28
145
  });
146
+ disposeResponse(response, controller);
29
147
  return true;
30
148
  } catch {
31
149
  return false;
150
+ } finally{
151
+ cleanup();
32
152
  }
33
153
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/http.ts"],"sourcesContent":["const MAX_ATTEMPTS = 3;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// Shared retry glue for the HTTP providers: 429/5xx/network failure retry with exponential\n// backoff honoring Retry-After; any other status returns immediately for the caller to handle.\nexport async function fetchWithRetry(url: string, init: RequestInit, baseDelayMs = 200): Promise<Response> {\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {\n try {\n const res = await fetch(url, init);\n if (res.ok || (res.status !== 429 && res.status < 500) || attempt === MAX_ATTEMPTS - 1) return res;\n const retryAfter = Number(res.headers.get('retry-after'));\n // Capped: a server naming a huge Retry-After must not stall an embed run for it.\n await sleep(retryAfter > 0 ? Math.min(retryAfter * 1000, 10_000) : baseDelayMs * 2 ** attempt);\n } catch (err) {\n if (attempt === MAX_ATTEMPTS - 1) throw err;\n await sleep(baseDelayMs * 2 ** attempt);\n }\n }\n throw new Error('unreachable');\n}\n\n// Any HTTP response means the endpoint is up; only a network failure or the timeout counts as\n// unreachable, so an air-gapped machine never waits more than timeoutMs on `sense status`.\nexport async function probeReachable(url: string, timeoutMs = 1500): Promise<boolean> {\n try {\n await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });\n return true;\n } catch {\n return false;\n }\n}\n"],"names":["MAX_ATTEMPTS","sleep","ms","Promise","resolve","setTimeout","fetchWithRetry","url","init","baseDelayMs","attempt","res","fetch","ok","status","retryAfter","Number","headers","get","Math","min","err","Error","probeReachable","timeoutMs","signal","AbortSignal","timeout"],"mappings":"AAAA,MAAMA,eAAe;AAErB,SAASC,MAAMC,EAAU;IACvB,OAAO,IAAIC,QAAQ,CAACC,UAAYC,WAAWD,SAASF;AACtD;AAEA,2FAA2F;AAC3F,+FAA+F;AAC/F,OAAO,eAAeI,eAAeC,GAAW,EAAEC,IAAiB,EAAEC,cAAc,GAAG;IACpF,IAAK,IAAIC,UAAU,GAAGA,UAAUV,cAAcU,UAAW;QACvD,IAAI;YACF,MAAMC,MAAM,MAAMC,MAAML,KAAKC;YAC7B,IAAIG,IAAIE,EAAE,IAAKF,IAAIG,MAAM,KAAK,OAAOH,IAAIG,MAAM,GAAG,OAAQJ,YAAYV,eAAe,GAAG,OAAOW;YAC/F,MAAMI,aAAaC,OAAOL,IAAIM,OAAO,CAACC,GAAG,CAAC;YAC1C,iFAAiF;YACjF,MAAMjB,MAAMc,aAAa,IAAII,KAAKC,GAAG,CAACL,aAAa,MAAM,SAAUN,cAAc,KAAKC;QACxF,EAAE,OAAOW,KAAK;YACZ,IAAIX,YAAYV,eAAe,GAAG,MAAMqB;YACxC,MAAMpB,MAAMQ,cAAc,KAAKC;QACjC;IACF;IACA,MAAM,IAAIY,MAAM;AAClB;AAEA,8FAA8F;AAC9F,2FAA2F;AAC3F,OAAO,eAAeC,eAAehB,GAAW,EAAEiB,YAAY,IAAI;IAChE,IAAI;QACF,MAAMZ,MAAML,KAAK;YAAEkB,QAAQC,YAAYC,OAAO,CAACH;QAAW;QAC1D,OAAO;IACT,EAAE,OAAM;QACN,OAAO;IACT;AACF"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/http.ts"],"sourcesContent":["import { SenseError } from '../errors.ts';\n\nconst MAX_ATTEMPTS = 3;\nconst MAX_RETRY_AFTER_MS = 10_000;\nconst DEFAULT_BASE_DELAY_MS = 200;\n\n// Initial policy bounds small API/metadata responses separately from model-file downloads.\nexport const HTTP_ATTEMPT_TIMEOUT_MS = 30_000;\nexport const MODEL_FILE_ATTEMPT_TIMEOUT_MS = 5 * 60_000;\n\nexport type FetchRequestInit = Omit<RequestInit, 'signal'> & { signal?: never };\n\nexport interface FetchRetryOptions {\n signal?: AbortSignal;\n attemptTimeoutMs?: number;\n baseDelayMs?: number;\n}\n\nexport interface BufferedResponse {\n readonly ok: boolean;\n readonly status: number;\n readonly headers: Headers;\n readonly body: ArrayBuffer;\n readonly url: string;\n}\n\ntype ResponsePhase = 'response headers' | 'response body';\n\nfunction safeUrl(url: string): string {\n try {\n const parsed = new URL(url);\n parsed.username = '';\n parsed.password = '';\n parsed.search = '';\n parsed.hash = '';\n return parsed.href;\n } catch {\n return 'the configured embedding endpoint';\n }\n}\n\nfunction attemptController(\n callerSignal: AbortSignal | undefined,\n timeoutMs: number\n): {\n controller: AbortController;\n timeoutReason: object;\n cleanup: () => void;\n} {\n callerSignal?.throwIfAborted();\n const controller = new AbortController();\n const timeoutReason = {};\n const relayAbort = () => controller.abort(callerSignal?.reason);\n callerSignal?.addEventListener('abort', relayAbort, { once: true });\n if (callerSignal?.aborted) relayAbort();\n const timer = setTimeout(() => controller.abort(timeoutReason), timeoutMs);\n return {\n controller,\n timeoutReason,\n cleanup: () => {\n clearTimeout(timer);\n callerSignal?.removeEventListener('abort', relayAbort);\n },\n };\n}\n\nfunction disposeResponse(response: Response, controller: AbortController): void {\n const cancellation = response.body && !response.bodyUsed ? response.body.cancel() : undefined;\n controller.abort();\n // The controller owns socket cleanup; observe stream cancellation without delaying the attempt.\n if (cancellation)\n void cancellation.then(\n () => undefined,\n () => undefined\n );\n}\n\nfunction sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {\n signal?.throwIfAborted();\n if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));\n return new Promise((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const cleanup = () => {\n if (timer) clearTimeout(timer);\n signal.removeEventListener('abort', onAbort);\n };\n const onAbort = () => {\n cleanup();\n reject(signal.reason);\n };\n signal.addEventListener('abort', onAbort, { once: true });\n timer = setTimeout(() => {\n cleanup();\n resolve();\n }, ms);\n if (signal.aborted) onAbort();\n });\n}\n\nfunction failedRequest(url: string, phase: ResponsePhase, attempts: number, timeoutMs: number, timedOut: boolean): SenseError {\n const endpoint = safeUrl(url);\n if (timedOut) {\n return new SenseError('EMBED_MODEL', `${endpoint} timed out after ${timeoutMs}ms while waiting for ${phase} (attempt ${attempts}/${MAX_ATTEMPTS}); check the embedding service and network`);\n }\n return new SenseError('EMBED_MODEL', `${endpoint} failed while waiting for ${phase} after ${attempts} attempts; check the embedding service and network`);\n}\n\nfunction buffered(response: Response, body: ArrayBuffer, requestUrl: string): BufferedResponse {\n return {\n ok: response.ok,\n status: response.status,\n headers: response.headers,\n body,\n url: safeUrl(response.url || requestUrl),\n };\n}\n\n// Each attempt covers response headers and the complete successful body. Retry cleanup and\n// backoff honor one caller signal without transferring ownership of that signal.\nexport async function fetchWithRetry(url: string, init: FetchRequestInit, options: FetchRetryOptions = {}): Promise<BufferedResponse> {\n const timeoutMs = options.attemptTimeoutMs ?? HTTP_ATTEMPT_TIMEOUT_MS;\n const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {\n options.signal?.throwIfAborted();\n const { controller, timeoutReason, cleanup } = attemptController(options.signal, timeoutMs);\n let response: Response | undefined;\n let phase: ResponsePhase = 'response headers';\n let retryDelayMs: number | undefined;\n try {\n response = await fetch(url, { ...init, signal: controller.signal });\n if (!response.ok) {\n const retryable = response.status === 429 || response.status >= 500;\n if (retryable && attempt < MAX_ATTEMPTS - 1) {\n const retryAfter = Number(response.headers.get('retry-after'));\n retryDelayMs = retryAfter > 0 ? Math.min(retryAfter * 1000, MAX_RETRY_AFTER_MS) : baseDelayMs * 2 ** attempt;\n disposeResponse(response, controller);\n } else {\n disposeResponse(response, controller);\n options.signal?.throwIfAborted();\n return buffered(response, new ArrayBuffer(0), url);\n }\n } else {\n phase = 'response body';\n const body = await response.arrayBuffer();\n options.signal?.throwIfAborted();\n return buffered(response, body, url);\n }\n } catch {\n try {\n options.signal?.throwIfAborted();\n } finally {\n if (response) disposeResponse(response, controller);\n }\n const timedOut = controller.signal.reason === timeoutReason;\n if (attempt === MAX_ATTEMPTS - 1) throw failedRequest(url, phase, attempt + 1, timeoutMs, timedOut);\n retryDelayMs = baseDelayMs * 2 ** attempt;\n } finally {\n cleanup();\n }\n await sleep(retryDelayMs ?? baseDelayMs * 2 ** attempt, options.signal);\n }\n throw new Error('unreachable');\n}\n\n// Any HTTP response means the endpoint is up. Abort it immediately because status does not\n// consume its body, and bound the headers wait for an air-gapped machine.\nexport async function probeReachable(url: string, timeoutMs = 1500): Promise<boolean> {\n const { controller, cleanup } = attemptController(undefined, timeoutMs);\n try {\n const response = await fetch(url, { signal: controller.signal });\n disposeResponse(response, controller);\n return true;\n } catch {\n return false;\n } finally {\n cleanup();\n }\n}\n"],"names":["SenseError","MAX_ATTEMPTS","MAX_RETRY_AFTER_MS","DEFAULT_BASE_DELAY_MS","HTTP_ATTEMPT_TIMEOUT_MS","MODEL_FILE_ATTEMPT_TIMEOUT_MS","safeUrl","url","parsed","URL","username","password","search","hash","href","attemptController","callerSignal","timeoutMs","throwIfAborted","controller","AbortController","timeoutReason","relayAbort","abort","reason","addEventListener","once","aborted","timer","setTimeout","cleanup","clearTimeout","removeEventListener","disposeResponse","response","cancellation","body","bodyUsed","cancel","undefined","then","sleep","ms","signal","Promise","resolve","reject","onAbort","failedRequest","phase","attempts","timedOut","endpoint","buffered","requestUrl","ok","status","headers","fetchWithRetry","init","options","attemptTimeoutMs","baseDelayMs","attempt","retryDelayMs","fetch","retryable","retryAfter","Number","get","Math","min","ArrayBuffer","arrayBuffer","Error","probeReachable"],"mappings":"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"}
@@ -1,17 +1,35 @@
1
1
  import { SenseError } from '../errors.js';
2
- import { fetchWithRetry } from './http.js';
2
+ import { effectiveEmbedEndpoint } from './endpoint.js';
3
+ import { fetchWithRetry, HTTP_ATTEMPT_TIMEOUT_MS } from './http.js';
3
4
  const BATCH_CAP = 64;
5
+ function isEmbedding(value) {
6
+ if (value === null || typeof value !== 'object' || !('embedding' in value) || !Array.isArray(value.embedding) || !value.embedding.every((part)=>typeof part === 'number')) return false;
7
+ return !('index' in value) || value.index === undefined || typeof value.index === 'number';
8
+ }
9
+ function parseBody(body, endpoint) {
10
+ let decoded;
11
+ try {
12
+ decoded = JSON.parse(new TextDecoder().decode(body));
13
+ } catch {
14
+ throw new SenseError('EMBED_MODEL', `${endpoint} returned invalid JSON`);
15
+ }
16
+ if (decoded === null || typeof decoded !== 'object' || !('data' in decoded) || !Array.isArray(decoded.data) || !decoded.data.every(isEmbedding)) {
17
+ throw new SenseError('EMBED_MODEL', `${endpoint} returned an invalid embedding response`);
18
+ }
19
+ return decoded.data;
20
+ }
4
21
  // One POST against any OpenAI-compatible /embeddings endpoint: Ollama, LM Studio, Cloudflare
5
22
  // Workers AI, Jina, Voyage, and Gemini's compat base URL all serve this shape.
6
23
  export async function openaiProvider(model, url, keyEnv) {
7
- if (!url) throw new SenseError('EMBED_MODEL', 'embed.provider "openai" requires a url');
8
- const base = url.replace(/\/+$/, '');
24
+ const base = effectiveEmbedEndpoint('openai', url);
25
+ if (!base) throw new SenseError('EMBED_MODEL', 'embed.provider "openai" requires a url');
9
26
  const headers = {
10
27
  'content-type': 'application/json'
11
28
  };
12
29
  const key = keyEnv ? process.env[keyEnv] : undefined;
13
30
  if (key) headers.authorization = `Bearer ${key}`;
14
- async function post(texts) {
31
+ async function post(texts, options) {
32
+ var _options_signal;
15
33
  const res = await fetchWithRetry(`${base}/embeddings`, {
16
34
  method: 'POST',
17
35
  headers,
@@ -19,17 +37,21 @@ export async function openaiProvider(model, url, keyEnv) {
19
37
  model,
20
38
  input: texts
21
39
  })
40
+ }, {
41
+ signal: options === null || options === void 0 ? void 0 : options.signal,
42
+ attemptTimeoutMs: HTTP_ATTEMPT_TIMEOUT_MS
22
43
  });
23
- if (!res.ok) throw new SenseError('EMBED_MODEL', `${base}/embeddings -> HTTP ${res.status}`);
24
- const body = await res.json();
44
+ options === null || options === void 0 ? void 0 : (_options_signal = options.signal) === null || _options_signal === void 0 ? void 0 : _options_signal.throwIfAborted();
45
+ if (!res.ok) throw new SenseError('EMBED_MODEL', `${res.url} -> HTTP ${res.status}`);
46
+ const body = parseBody(res.body, res.url);
25
47
  // The schema carries `index` because response order is not promised, and the caller maps
26
48
  // vectors back to chunks positionally: sort by it whenever every element has one.
27
- const data = body.data.every((d)=>typeof d.index === 'number') ? [
28
- ...body.data
29
- ].sort((a, b)=>a.index - b.index) : body.data;
49
+ const data = body.every((d)=>typeof d.index === 'number') ? [
50
+ ...body
51
+ ].sort((a, b)=>a.index - b.index) : body;
30
52
  // A server that drops an input (empty or over-length strings are the usual cause) would
31
53
  // otherwise shift every later vector onto the wrong chunk, silently.
32
- if (data.length !== texts.length) throw new SenseError('EMBED_MODEL', `${base}/embeddings returned ${data.length} embeddings for ${texts.length} inputs`);
54
+ if (data.length !== texts.length) throw new SenseError('EMBED_MODEL', `${res.url} returned ${data.length} embeddings for ${texts.length} inputs`);
33
55
  return data.map((d)=>Float32Array.from(d.embedding));
34
56
  }
35
57
  const dims = (await post([
@@ -42,8 +64,8 @@ export async function openaiProvider(model, url, keyEnv) {
42
64
  dims,
43
65
  batchCap: BATCH_CAP,
44
66
  embedDocuments: post,
45
- embedQuery: async (text)=>(await post([
67
+ embedQuery: async (text, options)=>(await post([
46
68
  text
47
- ]))[0]
69
+ ], options))[0]
48
70
  };
49
71
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/openai.ts"],"sourcesContent":["import { SenseError } from '../errors.ts';\nimport { fetchWithRetry } from './http.ts';\nimport type { EmbedProvider } from './types.ts';\n\nconst BATCH_CAP = 64;\n\n// One POST against any OpenAI-compatible /embeddings endpoint: Ollama, LM Studio, Cloudflare\n// Workers AI, Jina, Voyage, and Gemini's compat base URL all serve this shape.\nexport async function openaiProvider(model: string, url: string | undefined, keyEnv: string | undefined): Promise<EmbedProvider> {\n if (!url) throw new SenseError('EMBED_MODEL', 'embed.provider \"openai\" requires a url');\n const base = url.replace(/\\/+$/, '');\n const headers: Record<string, string> = { 'content-type': 'application/json' };\n const key = keyEnv ? process.env[keyEnv] : undefined;\n if (key) headers.authorization = `Bearer ${key}`;\n\n async function post(texts: string[]): Promise<Float32Array[]> {\n const res = await fetchWithRetry(`${base}/embeddings`, { method: 'POST', headers, body: JSON.stringify({ model, input: texts }) });\n if (!res.ok) throw new SenseError('EMBED_MODEL', `${base}/embeddings -> HTTP ${res.status}`);\n const body = (await res.json()) as { data: Array<{ embedding: number[]; index?: number }> };\n // The schema carries `index` because response order is not promised, and the caller maps\n // vectors back to chunks positionally: sort by it whenever every element has one.\n const data = body.data.every((d) => typeof d.index === 'number') ? [...body.data].sort((a, b) => (a.index as number) - (b.index as number)) : body.data;\n // A server that drops an input (empty or over-length strings are the usual cause) would\n // otherwise shift every later vector onto the wrong chunk, silently.\n if (data.length !== texts.length) throw new SenseError('EMBED_MODEL', `${base}/embeddings returned ${data.length} embeddings for ${texts.length} inputs`);\n return data.map((d) => Float32Array.from(d.embedding));\n }\n\n const dims = (await post(['dimension probe']))[0].length;\n // embedQuery and embedDocuments are symmetric here; per-model prefixes like nomic's\n // search_document/search_query are not applied.\n return { id: `openai:${base}:${model}`, dims, batchCap: BATCH_CAP, embedDocuments: post, embedQuery: async (text) => (await post([text]))[0] };\n}\n"],"names":["SenseError","fetchWithRetry","BATCH_CAP","openaiProvider","model","url","keyEnv","base","replace","headers","key","process","env","undefined","authorization","post","texts","res","method","body","JSON","stringify","input","ok","status","json","data","every","d","index","sort","a","b","length","map","Float32Array","from","embedding","dims","id","batchCap","embedDocuments","embedQuery","text"],"mappings":"AAAA,SAASA,UAAU,QAAQ,eAAe;AAC1C,SAASC,cAAc,QAAQ,YAAY;AAG3C,MAAMC,YAAY;AAElB,6FAA6F;AAC7F,+EAA+E;AAC/E,OAAO,eAAeC,eAAeC,KAAa,EAAEC,GAAuB,EAAEC,MAA0B;IACrG,IAAI,CAACD,KAAK,MAAM,IAAIL,WAAW,eAAe;IAC9C,MAAMO,OAAOF,IAAIG,OAAO,CAAC,QAAQ;IACjC,MAAMC,UAAkC;QAAE,gBAAgB;IAAmB;IAC7E,MAAMC,MAAMJ,SAASK,QAAQC,GAAG,CAACN,OAAO,GAAGO;IAC3C,IAAIH,KAAKD,QAAQK,aAAa,GAAG,CAAC,OAAO,EAAEJ,KAAK;IAEhD,eAAeK,KAAKC,KAAe;QACjC,MAAMC,MAAM,MAAMhB,eAAe,GAAGM,KAAK,WAAW,CAAC,EAAE;YAAEW,QAAQ;YAAQT;YAASU,MAAMC,KAAKC,SAAS,CAAC;gBAAEjB;gBAAOkB,OAAON;YAAM;QAAG;QAChI,IAAI,CAACC,IAAIM,EAAE,EAAE,MAAM,IAAIvB,WAAW,eAAe,GAAGO,KAAK,oBAAoB,EAAEU,IAAIO,MAAM,EAAE;QAC3F,MAAML,OAAQ,MAAMF,IAAIQ,IAAI;QAC5B,yFAAyF;QACzF,kFAAkF;QAClF,MAAMC,OAAOP,KAAKO,IAAI,CAACC,KAAK,CAAC,CAACC,IAAM,OAAOA,EAAEC,KAAK,KAAK,YAAY;eAAIV,KAAKO,IAAI;SAAC,CAACI,IAAI,CAAC,CAACC,GAAGC,IAAM,AAACD,EAAEF,KAAK,GAAeG,EAAEH,KAAK,IAAeV,KAAKO,IAAI;QACvJ,wFAAwF;QACxF,qEAAqE;QACrE,IAAIA,KAAKO,MAAM,KAAKjB,MAAMiB,MAAM,EAAE,MAAM,IAAIjC,WAAW,eAAe,GAAGO,KAAK,qBAAqB,EAAEmB,KAAKO,MAAM,CAAC,gBAAgB,EAAEjB,MAAMiB,MAAM,CAAC,OAAO,CAAC;QACxJ,OAAOP,KAAKQ,GAAG,CAAC,CAACN,IAAMO,aAAaC,IAAI,CAACR,EAAES,SAAS;IACtD;IAEA,MAAMC,OAAO,AAAC,CAAA,MAAMvB,KAAK;QAAC;KAAkB,CAAA,CAAE,CAAC,EAAE,CAACkB,MAAM;IACxD,oFAAoF;IACpF,gDAAgD;IAChD,OAAO;QAAEM,IAAI,CAAC,OAAO,EAAEhC,KAAK,CAAC,EAAEH,OAAO;QAAEkC;QAAME,UAAUtC;QAAWuC,gBAAgB1B;QAAM2B,YAAY,OAAOC,OAAS,AAAC,CAAA,MAAM5B,KAAK;gBAAC4B;aAAK,CAAA,CAAE,CAAC,EAAE;IAAC;AAC/I"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/openai.ts"],"sourcesContent":["import { SenseError } from '../errors.ts';\nimport { effectiveEmbedEndpoint } from './endpoint.ts';\nimport { fetchWithRetry, HTTP_ATTEMPT_TIMEOUT_MS } from './http.ts';\nimport type { EmbedCallOptions, EmbedProvider } from './types.ts';\n\nconst BATCH_CAP = 64;\n\ninterface OpenaiEmbedding {\n embedding: number[];\n index?: number;\n}\n\nfunction isEmbedding(value: unknown): value is OpenaiEmbedding {\n if (value === null || typeof value !== 'object' || !('embedding' in value) || !Array.isArray(value.embedding) || !value.embedding.every((part) => typeof part === 'number')) return false;\n return !('index' in value) || value.index === undefined || typeof value.index === 'number';\n}\n\nfunction parseBody(body: ArrayBuffer, endpoint: string): OpenaiEmbedding[] {\n let decoded: unknown;\n try {\n decoded = JSON.parse(new TextDecoder().decode(body));\n } catch {\n throw new SenseError('EMBED_MODEL', `${endpoint} returned invalid JSON`);\n }\n if (decoded === null || typeof decoded !== 'object' || !('data' in decoded) || !Array.isArray(decoded.data) || !decoded.data.every(isEmbedding)) {\n throw new SenseError('EMBED_MODEL', `${endpoint} returned an invalid embedding response`);\n }\n return decoded.data;\n}\n\n// One POST against any OpenAI-compatible /embeddings endpoint: Ollama, LM Studio, Cloudflare\n// Workers AI, Jina, Voyage, and Gemini's compat base URL all serve this shape.\nexport async function openaiProvider(model: string, url: string | undefined, keyEnv: string | undefined): Promise<EmbedProvider> {\n const base = effectiveEmbedEndpoint('openai', url);\n if (!base) throw new SenseError('EMBED_MODEL', 'embed.provider \"openai\" requires a url');\n const headers: Record<string, string> = { 'content-type': 'application/json' };\n const key = keyEnv ? process.env[keyEnv] : undefined;\n if (key) headers.authorization = `Bearer ${key}`;\n\n async function post(texts: string[], options?: EmbedCallOptions): Promise<Float32Array[]> {\n const res = await fetchWithRetry(`${base}/embeddings`, { method: 'POST', headers, body: JSON.stringify({ model, input: texts }) }, { signal: options?.signal, attemptTimeoutMs: HTTP_ATTEMPT_TIMEOUT_MS });\n options?.signal?.throwIfAborted();\n if (!res.ok) throw new SenseError('EMBED_MODEL', `${res.url} -> HTTP ${res.status}`);\n const body = parseBody(res.body, res.url);\n // The schema carries `index` because response order is not promised, and the caller maps\n // vectors back to chunks positionally: sort by it whenever every element has one.\n const data = body.every((d): d is OpenaiEmbedding & { index: number } => typeof d.index === 'number') ? [...body].sort((a, b) => a.index - b.index) : body;\n // A server that drops an input (empty or over-length strings are the usual cause) would\n // otherwise shift every later vector onto the wrong chunk, silently.\n if (data.length !== texts.length) throw new SenseError('EMBED_MODEL', `${res.url} returned ${data.length} embeddings for ${texts.length} inputs`);\n return data.map((d) => Float32Array.from(d.embedding));\n }\n\n const dims = (await post(['dimension probe']))[0].length;\n // embedQuery and embedDocuments are symmetric here; per-model prefixes like nomic's\n // search_document/search_query are not applied.\n return { id: `openai:${base}:${model}`, dims, batchCap: BATCH_CAP, embedDocuments: post, embedQuery: async (text, options) => (await post([text], options))[0] };\n}\n"],"names":["SenseError","effectiveEmbedEndpoint","fetchWithRetry","HTTP_ATTEMPT_TIMEOUT_MS","BATCH_CAP","isEmbedding","value","Array","isArray","embedding","every","part","index","undefined","parseBody","body","endpoint","decoded","JSON","parse","TextDecoder","decode","data","openaiProvider","model","url","keyEnv","base","headers","key","process","env","authorization","post","texts","options","res","method","stringify","input","signal","attemptTimeoutMs","throwIfAborted","ok","status","d","sort","a","b","length","map","Float32Array","from","dims","id","batchCap","embedDocuments","embedQuery","text"],"mappings":"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"}
@@ -1,12 +1,18 @@
1
- import type { Config } from '../config/index.js';
1
+ import { type Config } from '../config/index.js';
2
2
  import type { Store, VectorCandidate, VectorSimilar } from '../store/types.js';
3
+ import { type EmbedCallOptions } from './types.js';
3
4
  export declare function toStore(full: Float32Array, dims: number, int8: boolean): {
4
5
  v: Float32Array;
5
6
  scale: number;
6
7
  };
7
- export declare function embedPending(store: Store, cfg: Config, allowed?: ReadonlySet<string>): Promise<void>;
8
+ export declare function embedPending(store: Store, cfg: Config, allowed?: ReadonlySet<string>, expectedSignature?: string | null, options?: EmbedCallOptions): Promise<void>;
8
9
  export declare function ensureDocumentEmbeddings(store: Store, _cfg: Config, allowed: ReadonlySet<string>): Promise<void>;
9
- export declare function semanticCandidates(store: Store, cfg: Config, terms: string, fetch: number, allowed?: Set<string>): Promise<VectorCandidate[]>;
10
+ export interface PreparedSemanticQuery {
11
+ readonly vector: Float32Array;
12
+ readonly storeDims: number;
13
+ }
14
+ export declare function prepareSemanticQuery(cfg: Config, terms: string, options?: EmbedCallOptions): Promise<PreparedSemanticQuery>;
15
+ export declare function semanticCandidates(store: Store, prepared: PreparedSemanticQuery, fetch: number, allowed?: Set<string>): Promise<VectorCandidate[]>;
10
16
  export declare function hasEmbedding(store: Store, path: string): Promise<boolean>;
11
17
  export declare function similarNotes(store: Store, _cfg: Config, path: string, opts: {
12
18
  exclude: Set<string>;
@@ -1,8 +1,13 @@
1
+ import { featureSignature } from '../config/index.js';
1
2
  import { SenseError } from '../errors.js';
2
3
  import { embed } from '../features/embed.js';
4
+ import { FEATURES } from '../features/index.js';
3
5
  import { progress } from '../output/progress.js';
4
6
  import { parseSource } from '../scan/index.js';
5
- import { takeChunkText } from './handoff.js';
7
+ import { FEATURE_SIGNATURE_META_KEY } from '../store/readiness.js';
8
+ import { getMeta } from '../store/shared.js';
9
+ import { embeddingSignatureCompatible } from '../store/signature.js';
10
+ import { identifyChunkText, takeChunkText } from './handoff.js';
6
11
  import { checkLanguageFit } from './langfit.js';
7
12
  import { getProvider } from './registry.js';
8
13
  import { STORE_DIMS } from './types.js';
@@ -28,38 +33,64 @@ export function toStore(full, dims, int8) {
28
33
  // Embed rows whose vector is NULL. Reconcile hands its chunk text over in memory when it ran in
29
34
  // this process (handoff.ts); a row left pending by an earlier command re-derives from the exact
30
35
  // source string persisted with that indexed generation.
31
- export async function embedPending(store, cfg, allowed) {
36
+ function staleEmbeddingWork(reason) {
37
+ return new SenseError('INDEX_NOT_READY', `${reason}; run \`sense build\` (or library build(config)) again to prepare the current index generation`);
38
+ }
39
+ // expectedSignature is captured by open() before provider work. Direct internal callers omit it
40
+ // and capture the durable value here, before selecting chunks or constructing a provider.
41
+ export async function embedPending(store, cfg, allowed, expectedSignature, options = {}) {
32
42
  var _takeChunkText;
33
- const provider = await getProvider(cfg); // throws EMBED_DISABLED before touching the table
34
- const dirty = (await store.vectors.pending()).filter((row)=>{
43
+ var _options_signal, _options_signal1, _options_signal2, _options_signal3, // Over the exact texts about to be embedded, before any of them are: a mismatch fails
44
+ // this run loudly instead of quietly storing vectors from the wrong model.
45
+ _options_signal4, _options_signal5;
46
+ (_options_signal = options.signal) === null || _options_signal === void 0 ? void 0 : _options_signal.throwIfAborted();
47
+ const expectedDurableSignature = expectedSignature === undefined ? await getMeta(store, FEATURE_SIGNATURE_META_KEY) : expectedSignature;
48
+ (_options_signal1 = options.signal) === null || _options_signal1 === void 0 ? void 0 : _options_signal1.throwIfAborted();
49
+ if (!embeddingSignatureCompatible(expectedDurableSignature, featureSignature(cfg, FEATURES))) {
50
+ throw staleEmbeddingWork('the index configuration changed before vectors could be prepared');
51
+ }
52
+ const pendingStmt = await store.prepare('SELECT "path", chunk, content_identity FROM embeddings WHERE vector IS NULL ORDER BY "path", chunk');
53
+ const dirty = (await pendingStmt.all()).filter((row)=>{
35
54
  var _ref;
36
55
  return (_ref = allowed === null || allowed === void 0 ? void 0 : allowed.has(row.path)) !== null && _ref !== void 0 ? _ref : true;
37
56
  });
57
+ (_options_signal2 = options.signal) === null || _options_signal2 === void 0 ? void 0 : _options_signal2.throwIfAborted();
38
58
  if (dirty.length === 0) return;
59
+ const provider = await getProvider(cfg, options);
60
+ (_options_signal3 = options.signal) === null || _options_signal3 === void 0 ? void 0 : _options_signal3.throwIfAborted();
61
+ const submittedSignature = featureSignature(cfg, FEATURES);
62
+ if (!embeddingSignatureCompatible(expectedDurableSignature, submittedSignature)) {
63
+ throw staleEmbeddingWork('the index configuration changed while the embedding provider was being prepared');
64
+ }
39
65
  const storeDims = Math.min(STORE_DIMS, provider.dims);
40
66
  const byPath = new Map();
41
67
  for (const row of dirty){
42
68
  var _byPath_get;
43
69
  const list = (_byPath_get = byPath.get(row.path)) !== null && _byPath_get !== void 0 ? _byPath_get : [];
44
- list.push(row.chunk);
70
+ list.push(row);
45
71
  byPath.set(row.path, list);
46
72
  }
47
73
  const textByPath = (_takeChunkText = takeChunkText(store)) !== null && _takeChunkText !== void 0 ? _takeChunkText : new Map();
48
74
  const sourceStmt = await store.prepare('SELECT text FROM indexed_sources WHERE "path" = ?');
49
75
  const jobs = [];
50
- for (const [path, chunkIdxs] of byPath){
76
+ for (const [path, pending] of byPath){
77
+ var _options_signal6;
51
78
  const texts = textByPath.get(path);
52
- if (texts && chunkIdxs.every((idx)=>idx < texts.length)) {
53
- for (const idx of chunkIdxs)jobs.push({
79
+ if (texts && pending.every((row)=>{
80
+ var _texts_row_chunk;
81
+ return ((_texts_row_chunk = texts[row.chunk]) === null || _texts_row_chunk === void 0 ? void 0 : _texts_row_chunk.identity) === row.content_identity;
82
+ })) {
83
+ for (const row of pending)jobs.push({
54
84
  path,
55
- chunk: idx,
56
- text: texts[idx]
85
+ chunk: row.chunk,
86
+ ...texts[row.chunk]
57
87
  });
58
88
  continue;
59
89
  }
60
90
  // A prior process may have reconciled the row, so its in-memory handoff is gone. Re-derive
61
91
  // from that indexed generation's stored source, never from a newer live file.
62
92
  const source = await sourceStmt.get(path);
93
+ (_options_signal6 = options.signal) === null || _options_signal6 === void 0 ? void 0 : _options_signal6.throwIfAborted();
63
94
  if (!source) throw new SenseError('INDEX_NOT_READY', `indexed source text is missing for ${path}; run \`sense build --force\` (or library build(config, { force: true })) before preparing vectors`);
64
95
  const chunks = parseSource({
65
96
  relPath: path,
@@ -72,26 +103,33 @@ export async function embedPending(store, cfg, allowed) {
72
103
  }, source.text, [
73
104
  embed
74
105
  ], cfg).doc.extracted.embed;
75
- for (const idx of chunkIdxs){
76
- const chunk = chunks[idx];
106
+ for (const row of pending){
107
+ const chunk = chunks[row.chunk];
77
108
  if (!chunk) throw new SenseError('INDEX_NOT_READY', `indexed vector metadata for ${path} does not match its indexed source; run \`sense build --force\` (or library build(config, { force: true }))`);
109
+ const identified = identifyChunkText(chunk.text);
110
+ if (identified.identity !== row.content_identity) throw staleEmbeddingWork(`indexed chunk ${path}#${row.chunk} changed before vectors could be prepared`);
78
111
  jobs.push({
79
112
  path,
80
- chunk: idx,
81
- text: chunk.text
113
+ chunk: row.chunk,
114
+ ...identified
82
115
  });
83
116
  }
84
117
  }
85
- // Over the exact texts about to be embedded, before any of them are: a mismatch fails
86
- // this run loudly instead of quietly storing vectors from the wrong model.
118
+ (_options_signal4 = options.signal) === null || _options_signal4 === void 0 ? void 0 : _options_signal4.throwIfAborted();
87
119
  await checkLanguageFit(store, provider, jobs.map((j)=>j.text));
120
+ (_options_signal5 = options.signal) === null || _options_signal5 === void 0 ? void 0 : _options_signal5.throwIfAborted();
88
121
  // Preparing a large configured scope can take long enough to look stalled, so report each
89
122
  // completed provider batch.
90
123
  const report = progress('embedding chunks', jobs.length);
91
124
  for(let i = 0; i < jobs.length; i += provider.batchCap){
125
+ var _options_signal7, _options_signal8, _options_signal9;
92
126
  const batch = jobs.slice(i, i + provider.batchCap);
93
- const vectors = await provider.embedDocuments(batch.map((j)=>j.text));
94
- await store.vectors.writeVectors(batch.map((job, j)=>{
127
+ (_options_signal7 = options.signal) === null || _options_signal7 === void 0 ? void 0 : _options_signal7.throwIfAborted();
128
+ if (featureSignature(cfg, FEATURES) !== submittedSignature) throw staleEmbeddingWork('the embedding configuration changed before a provider batch was submitted');
129
+ const vectors = await provider.embedDocuments(batch.map((j)=>j.text), options);
130
+ (_options_signal8 = options.signal) === null || _options_signal8 === void 0 ? void 0 : _options_signal8.throwIfAborted();
131
+ if (featureSignature(cfg, FEATURES) !== submittedSignature) throw staleEmbeddingWork('the embedding configuration changed while a provider batch was running');
132
+ const writes = batch.map((job, j)=>{
95
133
  const { v, scale } = toStore(vectors[j], storeDims, true);
96
134
  const q = new Int8Array(storeDims);
97
135
  for(let d = 0; d < storeDims; d++)q[d] = Math.round(v[d] / scale);
@@ -101,7 +139,28 @@ export async function embedPending(store, cfg, allowed) {
101
139
  scale,
102
140
  vector: Buffer.from(q.buffer)
103
141
  };
104
- }));
142
+ });
143
+ (_options_signal9 = options.signal) === null || _options_signal9 === void 0 ? void 0 : _options_signal9.throwIfAborted();
144
+ await store.transaction(async ()=>{
145
+ var // Write first so SQLite owns the write lock before the fence reads. Any mismatch throws
146
+ // inside this transaction and rolls every row in the provider batch back together.
147
+ _options_signal, _options_signal1, _options_signal2;
148
+ (_options_signal = options.signal) === null || _options_signal === void 0 ? void 0 : _options_signal.throwIfAborted();
149
+ await store.vectors.writeVectors(writes);
150
+ (_options_signal1 = options.signal) === null || _options_signal1 === void 0 ? void 0 : _options_signal1.throwIfAborted();
151
+ if (await getMeta(store, FEATURE_SIGNATURE_META_KEY) !== expectedDurableSignature) throw staleEmbeddingWork('the durable embedding configuration changed while a provider batch was running');
152
+ const currentStmt = await store.prepare(`SELECT "path", chunk, content_identity FROM embeddings WHERE ${batch.map(()=>'("path" = ? AND chunk = ?)').join(' OR ')}`);
153
+ const current = await currentStmt.all(...batch.flatMap((job)=>[
154
+ job.path,
155
+ job.chunk
156
+ ]));
157
+ const identities = new Map(current.map((row)=>[
158
+ `${row.path}\0${Number(row.chunk)}`,
159
+ row.content_identity
160
+ ]));
161
+ if (batch.some((job)=>identities.get(`${job.path}\0${job.chunk}`) !== job.identity)) throw staleEmbeddingWork('indexed chunks changed while a provider batch was running');
162
+ (_options_signal2 = options.signal) === null || _options_signal2 === void 0 ? void 0 : _options_signal2.throwIfAborted();
163
+ });
105
164
  report.tick(Math.min(i + provider.batchCap, jobs.length));
106
165
  }
107
166
  report.finish();
@@ -112,22 +171,35 @@ export async function ensureDocumentEmbeddings(store, _cfg, allowed) {
112
171
  throw new SenseError('INDEX_NOT_READY', `${pending.length} indexed chunk(s) in the requested scope still need vectors; run \`sense build\` (or library build(config)) before querying with build disabled`);
113
172
  }
114
173
  }
115
- // Best chunk per file by cosine, its line range riding along; FTS5 operators are stripped as
116
- // lexical syntax. Similarity comes back because the fused score cannot express match quality.
117
- export async function semanticCandidates(store, cfg, terms, fetch, allowed) {
174
+ // Provider/model work is independent of the index snapshot. FTS5 operators are stripped as
175
+ // lexical syntax before the query is embedded, matching the prior semantic query behavior.
176
+ export async function prepareSemanticQuery(cfg, terms, options = {}) {
118
177
  var _cfg_rootDir, _terms_match;
178
+ var _options_signal, _options_signal1, _options_signal2;
119
179
  const rootDir = (_cfg_rootDir = cfg.rootDir) !== null && _cfg_rootDir !== void 0 ? _cfg_rootDir : cfg.baseDir;
120
180
  if (!rootDir) throw new SenseError('EMBED_MODEL', 'semantic expansion needs a resolved config (use loadConfig/open)');
121
- const provider = await getProvider(cfg);
122
- const storeDims = Math.min(STORE_DIMS, provider.dims);
123
181
  const text = ((_terms_match = terms.match(/[\p{L}\p{N}]+/gu)) !== null && _terms_match !== void 0 ? _terms_match : []).filter((t)=>![
124
182
  'AND',
125
183
  'OR',
126
184
  'NOT',
127
185
  'NEAR'
128
186
  ].includes(t)).join(' ');
129
- const { v: qv } = toStore(await provider.embedQuery(text), storeDims, false);
130
- return store.vectors.candidates(qv, storeDims, fetch, allowed);
187
+ (_options_signal = options.signal) === null || _options_signal === void 0 ? void 0 : _options_signal.throwIfAborted();
188
+ const provider = await getProvider(cfg, options);
189
+ (_options_signal1 = options.signal) === null || _options_signal1 === void 0 ? void 0 : _options_signal1.throwIfAborted();
190
+ const storeDims = Math.min(STORE_DIMS, provider.dims);
191
+ const queryVector = await provider.embedQuery(text, options);
192
+ (_options_signal2 = options.signal) === null || _options_signal2 === void 0 ? void 0 : _options_signal2.throwIfAborted();
193
+ const { v: qv } = toStore(queryVector, storeDims, false);
194
+ return {
195
+ vector: qv,
196
+ storeDims
197
+ };
198
+ }
199
+ // Best chunk per file by cosine, its line range riding along. The caller owns the read snapshot
200
+ // and the prepared vector, so this performs no provider work and only delegates to the store.
201
+ export function semanticCandidates(store, prepared, fetch, allowed) {
202
+ return store.vectors.candidates(prepared.vector, prepared.storeDims, fetch, allowed);
131
203
  }
132
204
  // Whether a note has any chunk with a vector. Distinguishes "nothing is near this note" from
133
205
  // "this note has no text", which look the same in an empty result.