brane 0.1.1 → 0.3.1

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 (352) hide show
  1. package/AGENTS.md +367 -0
  2. package/CHANGELOG.md +66 -0
  3. package/CONTRIBUTING.md +101 -0
  4. package/DESIGN.md +405 -0
  5. package/Dockerfile +61 -0
  6. package/LICENSE +656 -17
  7. package/README.md +174 -2
  8. package/SECURITY.md +213 -0
  9. package/dist/absorb/brane-id.js +11 -0
  10. package/dist/absorb/chatgpt.js +161 -0
  11. package/dist/absorb/claude-code.js +504 -0
  12. package/dist/absorb/index.js +543 -0
  13. package/dist/absorb/organelle.js +345 -0
  14. package/dist/absorb/run.js +520 -0
  15. package/dist/absorb/sources.js +168 -0
  16. package/dist/absorb/types.js +3 -0
  17. package/dist/agent-env.js +45 -0
  18. package/dist/agents.js +1168 -0
  19. package/dist/claude-live-session.js +124 -0
  20. package/dist/cli.js +50 -0
  21. package/dist/codex-tool-log.js +144 -0
  22. package/dist/commands/absorb.js +291 -0
  23. package/dist/commands/attach.js +31 -0
  24. package/dist/commands/certify.js +62 -0
  25. package/dist/commands/config.js +133 -0
  26. package/dist/commands/cost.js +126 -0
  27. package/dist/commands/daemon.js +350 -0
  28. package/dist/commands/doctor.js +262 -0
  29. package/dist/commands/fold.js +95 -0
  30. package/dist/commands/list.js +89 -0
  31. package/dist/commands/mcp.js +84 -0
  32. package/dist/commands/meld.js +360 -0
  33. package/dist/commands/memory.js +215 -0
  34. package/dist/commands/revive.js +74 -0
  35. package/dist/commands/run.js +85 -0
  36. package/dist/commands/spawn.js +1101 -0
  37. package/dist/commands/token.js +154 -0
  38. package/dist/copilot-tool-log.js +145 -0
  39. package/dist/cost/pricing.js +116 -0
  40. package/dist/cost/reprice.js +136 -0
  41. package/dist/crush-tool-log.js +293 -0
  42. package/dist/daemon/agent-scope.js +116 -0
  43. package/dist/daemon/awaiting.js +54 -0
  44. package/dist/daemon/cert-manager.js +349 -0
  45. package/dist/daemon/client.js +262 -0
  46. package/dist/daemon/gated-routes.js +112 -0
  47. package/dist/daemon/keep-awake.js +103 -0
  48. package/dist/daemon/local-socket.js +145 -0
  49. package/dist/daemon/presence-gate.js +206 -0
  50. package/dist/daemon/server.js +6353 -0
  51. package/dist/daemon/signal-source.js +35 -0
  52. package/dist/daemon/tailscale-rebind.js +58 -0
  53. package/dist/daemon/token-ring.js +263 -0
  54. package/dist/daemon/token-routes.js +190 -0
  55. package/dist/db/events.js +152 -0
  56. package/dist/db/mentions.js +179 -0
  57. package/dist/db/schema.js +643 -0
  58. package/dist/db/tokens.js +624 -0
  59. package/dist/db/tool-uses.js +647 -0
  60. package/dist/diagram/index.js +85 -0
  61. package/dist/diagram/label.js +455 -0
  62. package/dist/diagram/layout.js +476 -0
  63. package/dist/diagram/links.js +50 -0
  64. package/dist/diagram/metrics.js +152 -0
  65. package/dist/diagram/parse.js +487 -0
  66. package/dist/diagram/svg.js +389 -0
  67. package/dist/diagram/types.js +9 -0
  68. package/dist/export.js +629 -0
  69. package/dist/files/archive-types.js +2 -0
  70. package/dist/files/archive.js +182 -0
  71. package/dist/files/browse.js +327 -0
  72. package/dist/files/bytes.js +162 -0
  73. package/dist/files/codecs.js +33 -0
  74. package/dist/files/find.js +212 -0
  75. package/dist/files/tar.js +223 -0
  76. package/dist/files/zip.js +209 -0
  77. package/dist/folds.js +72 -0
  78. package/dist/grammars/ebnf.js +92 -0
  79. package/dist/highlight.js +250 -0
  80. package/dist/ids.js +24 -0
  81. package/dist/markdown-diagnostics.js +233 -0
  82. package/dist/math.js +168 -0
  83. package/dist/mcp/server.js +886 -0
  84. package/dist/memory/embedder.js +145 -0
  85. package/dist/memory/embedding-worker.js +71 -0
  86. package/dist/memory/embeddings.js +91 -0
  87. package/dist/memory/index.js +232 -0
  88. package/dist/memory/storage.js +587 -0
  89. package/dist/memory/wiki.js +688 -0
  90. package/dist/memory/wordpiece.js +247 -0
  91. package/dist/mentions-scan.js +40 -0
  92. package/dist/presence.js +259 -0
  93. package/dist/session-affinity.js +185 -0
  94. package/dist/shared/api-request.js +124 -0
  95. package/dist/shared/app-origin.js +26 -0
  96. package/dist/shared/block-glyphs.js +114 -0
  97. package/dist/shared/diagram-css.js +17 -0
  98. package/dist/shared/export-selection.js +101 -0
  99. package/dist/shared/find-in-text.js +110 -0
  100. package/dist/shared/glow-hues.js +225 -0
  101. package/dist/shared/hex-dump.js +62 -0
  102. package/dist/shared/hex-pages.js +52 -0
  103. package/dist/shared/hit-drum.js +79 -0
  104. package/dist/shared/known-nids.js +58 -0
  105. package/dist/shared/list-scroll-keys.js +56 -0
  106. package/dist/shared/markdown-lists.js +72 -0
  107. package/dist/shared/markdown-math.js +485 -0
  108. package/dist/shared/markdown-mentions.js +79 -0
  109. package/dist/shared/markdown-nids.js +41 -0
  110. package/dist/shared/markdown-protocols.js +53 -0
  111. package/dist/shared/markdown-refs.js +123 -0
  112. package/dist/shared/membrane-invasion.js +342 -0
  113. package/dist/shared/membrane-motion.js +83 -0
  114. package/dist/shared/membrane-palette.js +253 -0
  115. package/dist/shared/membrane-terrain.js +212 -0
  116. package/dist/shared/message-selection.js +123 -0
  117. package/dist/shared/mo-glyph.js +57 -0
  118. package/dist/shared/narrative-format.js +237 -0
  119. package/dist/shared/narrative-menu.js +84 -0
  120. package/dist/shared/narrative-sort.js +47 -0
  121. package/dist/shared/narrative-status.js +52 -0
  122. package/dist/shared/no-pan.js +85 -0
  123. package/dist/shared/pane-layout.js +50 -0
  124. package/dist/shared/payload-inline.js +83 -0
  125. package/dist/shared/speaker-color.js +61 -0
  126. package/dist/shared/token-rotation.js +14 -0
  127. package/dist/shared/view-url.js +216 -0
  128. package/dist/shared/wiki-links.js +70 -0
  129. package/dist/shared/wiki-window.js +4 -0
  130. package/dist/tailscale.js +55 -0
  131. package/dist/test/absorb-claude.test.js +892 -0
  132. package/dist/test/agent-scope.test.js +193 -0
  133. package/dist/test/agents.test.js +55 -0
  134. package/dist/test/api-request.test.js +122 -0
  135. package/dist/test/app-host-literal.test.js +53 -0
  136. package/dist/test/awaiting-glow.test.js +54 -0
  137. package/dist/test/awaiting.test.js +122 -0
  138. package/dist/test/bind-token.test.js +88 -0
  139. package/dist/test/block-glyphs.test.js +68 -0
  140. package/dist/test/cert-manager.test.js +262 -0
  141. package/dist/test/claude-live-session.test.js +110 -0
  142. package/dist/test/codex-tool-log.test.js +568 -0
  143. package/dist/test/config.test.js +71 -0
  144. package/dist/test/copilot-tool-log.test.js +93 -0
  145. package/dist/test/cors-preflight.test.js +56 -0
  146. package/dist/test/cost-linger.test.js +31 -0
  147. package/dist/test/cost-pricing.test.js +72 -0
  148. package/dist/test/cost-reprice.test.js +139 -0
  149. package/dist/test/cost-sessions.test.js +301 -0
  150. package/dist/test/crush-tool-log.test.js +289 -0
  151. package/dist/test/daemon-client.test.js +109 -0
  152. package/dist/test/daemon-socket.test.js +293 -0
  153. package/dist/test/diagram-arrows.test.js +205 -0
  154. package/dist/test/diagram-export.test.js +210 -0
  155. package/dist/test/diagram-labels.test.js +313 -0
  156. package/dist/test/diagram-parse.test.js +191 -0
  157. package/dist/test/diagram-render.test.js +175 -0
  158. package/dist/test/diagram-review.test.js +288 -0
  159. package/dist/test/diagram-self-loop-labels.test.js +303 -0
  160. package/dist/test/embedder.test.js +84 -0
  161. package/dist/test/events.test.js +123 -0
  162. package/dist/test/export-selection.test.js +102 -0
  163. package/dist/test/export.test.js +505 -0
  164. package/dist/test/files-browse.test.js +393 -0
  165. package/dist/test/files-find.test.js +272 -0
  166. package/dist/test/find-in-text.test.js +86 -0
  167. package/dist/test/first-paint-viewport.test.js +59 -0
  168. package/dist/test/fixtures/embedding-golden.js +23 -0
  169. package/dist/test/fixtures/wordpiece-golden.js +167 -0
  170. package/dist/test/glow-hues.test.js +202 -0
  171. package/dist/test/harness-cost.test.js +423 -0
  172. package/dist/test/helpers.js +21 -0
  173. package/dist/test/hex-pages.test.js +68 -0
  174. package/dist/test/highlight.test.js +69 -0
  175. package/dist/test/hit-drum.test.js +71 -0
  176. package/dist/test/hook-config.test.js +299 -0
  177. package/dist/test/json-gzip.test.js +70 -0
  178. package/dist/test/key-routing.test.js +58 -0
  179. package/dist/test/known-nids.test.js +106 -0
  180. package/dist/test/link-preview.test.js +67 -0
  181. package/dist/test/list-scroll-keys.test.js +66 -0
  182. package/dist/test/listen-own-words.test.js +147 -0
  183. package/dist/test/listen-timeout.test.js +33 -0
  184. package/dist/test/markdown-diagnostics.test.js +121 -0
  185. package/dist/test/markdown-lists.test.js +182 -0
  186. package/dist/test/markdown-math.test.js +256 -0
  187. package/dist/test/markdown-nids.test.js +146 -0
  188. package/dist/test/markdown-protocols.test.js +139 -0
  189. package/dist/test/mcp-bridge-cancel.test.js +98 -0
  190. package/dist/test/mcp-bridge-reconnect.test.js +121 -0
  191. package/dist/test/mcp-narrative.test.js +193 -0
  192. package/dist/test/mcp.test.js +548 -0
  193. package/dist/test/meld.test.js +143 -0
  194. package/dist/test/membrane-invasion.test.js +482 -0
  195. package/dist/test/membrane-motion.test.js +101 -0
  196. package/dist/test/membrane-palette.test.js +166 -0
  197. package/dist/test/membrane-terrain.test.js +170 -0
  198. package/dist/test/memory-log.test.js +202 -0
  199. package/dist/test/mentions-e2e.test.js +261 -0
  200. package/dist/test/mentions-ledger.test.js +265 -0
  201. package/dist/test/mentions-listen.test.js +265 -0
  202. package/dist/test/mentions.test.js +238 -0
  203. package/dist/test/message-selection.test.js +114 -0
  204. package/dist/test/mo-glyph.test.js +22 -0
  205. package/dist/test/model-resolution.test.js +166 -0
  206. package/dist/test/narrate-validate.test.js +77 -0
  207. package/dist/test/narrate-wait.test.js +160 -0
  208. package/dist/test/narrative-format.test.js +219 -0
  209. package/dist/test/narrative-menu.test.js +150 -0
  210. package/dist/test/narrative-sort.test.js +75 -0
  211. package/dist/test/no-pan.test.js +60 -0
  212. package/dist/test/pane-layout.test.js +153 -0
  213. package/dist/test/paths.test.js +37 -0
  214. package/dist/test/payload-inline.test.js +91 -0
  215. package/dist/test/precache-fonts.test.js +72 -0
  216. package/dist/test/presence-gate.test.js +440 -0
  217. package/dist/test/presence.test.js +160 -0
  218. package/dist/test/read-events.test.js +220 -0
  219. package/dist/test/remote-meld.test.js +128 -0
  220. package/dist/test/revive-cli.test.js +93 -0
  221. package/dist/test/search-floor.test.js +206 -0
  222. package/dist/test/search-fusion.test.js +168 -0
  223. package/dist/test/search.test.js +205 -0
  224. package/dist/test/security.test.js +48 -0
  225. package/dist/test/selection-wiring.test.js +61 -0
  226. package/dist/test/session-affinity.test.js +295 -0
  227. package/dist/test/signal-source.test.js +32 -0
  228. package/dist/test/socket-account.test.js +268 -0
  229. package/dist/test/spawn-toml.test.js +72 -0
  230. package/dist/test/speaker-color.test.js +90 -0
  231. package/dist/test/tailscale-rebind.test.js +79 -0
  232. package/dist/test/terminal-cursor.test.js +61 -0
  233. package/dist/test/terminal-resync.test.js +23 -0
  234. package/dist/test/terminal-size.test.js +36 -0
  235. package/dist/test/thin-cli.test.js +382 -0
  236. package/dist/test/token-bootstrap.test.js +272 -0
  237. package/dist/test/token-devices.test.js +646 -0
  238. package/dist/test/token-lapse.test.js +94 -0
  239. package/dist/test/token-ring.test.js +188 -0
  240. package/dist/test/token-rotate-cli.test.js +58 -0
  241. package/dist/test/token-routes.test.js +95 -0
  242. package/dist/test/tool-uses.test.js +1006 -0
  243. package/dist/test/usage-ledger.test.js +126 -0
  244. package/dist/test/view-url.test.js +627 -0
  245. package/dist/test/wiki-links.test.js +57 -0
  246. package/dist/test/wiki-sources.test.js +85 -0
  247. package/dist/test/wiki-window.test.js +31 -0
  248. package/dist/test/wiki.test.js +399 -0
  249. package/dist/test/wordpiece.test.js +68 -0
  250. package/dist/test/wrangler-route.test.js +27 -0
  251. package/dist/tls.js +148 -0
  252. package/dist/tmux.js +213 -0
  253. package/dist/user-agent.js +46 -0
  254. package/docker-entrypoint.sh +154 -0
  255. package/docs/README.md +3 -0
  256. package/docs/absorb.md +231 -0
  257. package/docs/cost-ledger.md +125 -0
  258. package/docs/daemon-api.md +189 -0
  259. package/docs/diagrams.md +163 -0
  260. package/docs/doctor.md +36 -0
  261. package/docs/export-scope.md +69 -0
  262. package/docs/files-view.md +133 -0
  263. package/docs/link-previews.md +38 -0
  264. package/docs/math.md +32 -0
  265. package/docs/mcp.md +69 -0
  266. package/docs/memory-search.md +75 -0
  267. package/docs/mentions.md +101 -0
  268. package/docs/multi-brane.md +14 -0
  269. package/docs/passkeys.md +165 -0
  270. package/docs/presence.md +202 -0
  271. package/docs/releasing.md +104 -0
  272. package/docs/rendering-warnings.md +22 -0
  273. package/docs/request-layer.md +72 -0
  274. package/docs/selection-store.md +50 -0
  275. package/docs/session-affinity.md +61 -0
  276. package/docs/terminal-mirroring.md +243 -0
  277. package/docs/token-rotation.md +431 -0
  278. package/docs/view-urls.md +134 -0
  279. package/docs/web-app.md +23 -0
  280. package/docs/wiki.md +43 -0
  281. package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -0
  282. package/expo/dist/apple-touch-icon.png +0 -0
  283. package/expo/dist/favicon.svg +3 -0
  284. package/expo/dist/index.html +177 -0
  285. package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  286. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  287. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  288. package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  289. package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  290. package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  291. package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  292. package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  293. package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  294. package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  295. package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  296. package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  297. package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  298. package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  299. package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  300. package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  301. package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  302. package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  303. package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  304. package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  305. package/expo/dist/katex/katex.min.css +1 -0
  306. package/expo/dist/manifest.json +16 -0
  307. package/expo/dist/metadata.json +1 -0
  308. package/expo/dist/og/membrane.v1.png +0 -0
  309. package/expo/dist/pdf.min.mjs +28 -0
  310. package/expo/dist/pdf.worker.min.mjs +28 -0
  311. package/expo/dist/sw.js +154 -0
  312. package/package.json +79 -27
  313. package/protocols/LIVE_loop.md +407 -0
  314. package/protocols/bebrief.md +41 -0
  315. package/protocols/digest.md +231 -0
  316. package/protocols/fly.md +266 -0
  317. package/protocols/idle.md +81 -0
  318. package/protocols/keeper.md +829 -0
  319. package/protocols/markdown.md +80 -0
  320. package/protocols/memory.md +419 -0
  321. package/protocols/mentions.md +93 -0
  322. package/protocols/payloads.md +53 -0
  323. package/protocols/prelude.md +91 -0
  324. package/protocols/ramify.md +105 -0
  325. package/protocols/review.md +356 -0
  326. package/protocols/rrr.md +232 -0
  327. package/protocols/scribe.md +429 -0
  328. package/protocols/summarize.md +203 -0
  329. package/protocols/twin.md +174 -0
  330. package/protocols/wiki.md +438 -0
  331. package/protocols/worker.md +38 -0
  332. package/wiki/shared/append-only-event-log.md +148 -0
  333. package/wiki/shared/index.md +53 -0
  334. package/wiki/shared/owning-your-narratives.md +161 -0
  335. package/wiki/shared/premature-ontologies.md +435 -0
  336. package/wiki/shared/sedimentary-superintelligence.md +128 -0
  337. package/wiki/shared/sequestering-the-narrative.md +143 -0
  338. package/wiki/shared/why-wikis-work.md +216 -0
  339. package/.travis.yml +0 -7
  340. package/lib/brane.cjs.js +0 -12
  341. package/lib/brane.cjs.js.map +0 -1
  342. package/lib/brane.d.ts +0 -11
  343. package/lib/brane.js +0 -6
  344. package/lib/brane.js.map +0 -1
  345. package/lib/tests.d.ts +0 -1
  346. package/lib/tests.js +0 -8
  347. package/lib/tests.js.map +0 -1
  348. package/rollup.config.js +0 -21
  349. package/scripts/test.sh +0 -8
  350. package/src/brane.ts +0 -21
  351. package/src/tests.ts +0 -8
  352. package/tsconfig.json +0 -16
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Feature extraction on `onnxruntime-node` directly — the slice of
3
+ * `@huggingface/transformers` brane actually used.
4
+ *
5
+ * The omnibus package brought 138 MB of code for four steps: tokenize, run one
6
+ * ONNX session, mean-pool over the attention mask, L2-normalize. This file is
7
+ * those four steps. It is written to produce byte-identical vectors to the
8
+ * pipeline it replaces, because `memory_vec` is already full of the old ones —
9
+ * see `src/test/embedder.test.ts`, which pins that against real chunks.
10
+ *
11
+ * The on-disk cache layout is deliberately the one transformers.js used
12
+ * (`<cacheDir>/<repo>/<file>`), so an existing `~/.brane/models` keeps working
13
+ * and nobody re-downloads 133 MB on upgrade.
14
+ */
15
+ import { createWriteStream, existsSync, mkdirSync, renameSync } from "node:fs";
16
+ import { availableParallelism, homedir } from "node:os";
17
+ import { dirname, join } from "node:path";
18
+ import { Readable } from "node:stream";
19
+ import { pipeline as streamPipeline } from "node:stream/promises";
20
+ import * as ort from "onnxruntime-node";
21
+ import { WordPieceTokenizer } from "./wordpiece.js";
22
+ /** The model brane embeds with. */
23
+ export const MODEL_REPO = "Xenova/bge-small-en-v1.5";
24
+ /** Its hidden size — the width of every vector in `memory_vec`. */
25
+ export const VECTOR_DIM = 384;
26
+ const HF_ENDPOINT = process.env.HF_ENDPOINT || "https://huggingface.co";
27
+ function cacheDir() {
28
+ return process.env.BRANE_MODEL_CACHE || join(homedir(), ".brane", "models");
29
+ }
30
+ /**
31
+ * Download one file to the cache if it isn't already there.
32
+ *
33
+ * Writes to a temporary name and renames into place: a model interrupted
34
+ * halfway is 133 MB of plausible-looking ONNX that fails at session load with
35
+ * nothing to suggest a truncated download. The rename makes presence mean
36
+ * complete. (Never removes the partial file — the golden rule applies to
37
+ * brane's own caches too; a stale `.partial` costs disk, and disk is cheap.)
38
+ */
39
+ export async function ensureFile(repo, file) {
40
+ const target = join(cacheDir(), repo, file);
41
+ if (existsSync(target))
42
+ return target;
43
+ mkdirSync(dirname(target), { recursive: true });
44
+ const url = `${HF_ENDPOINT}/${repo}/resolve/main/${file}`;
45
+ const res = await fetch(url);
46
+ if (!res.ok || !res.body) {
47
+ throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
48
+ }
49
+ const partial = `${target}.partial-${process.pid}`;
50
+ await streamPipeline(Readable.fromWeb(res.body), createWriteStream(partial));
51
+ renameSync(partial, target);
52
+ return target;
53
+ }
54
+ /**
55
+ * How many cores one embedding session may use.
56
+ *
57
+ * This number was measured, not chosen. Capping ORT at a single intra-op
58
+ * thread — to be a good neighbour to the daemon's event loop and the 10 fps
59
+ * tmux capture — made embedding **6.5x slower** than the transformers.js
60
+ * pipeline it replaces (308 ms/text against 47), which would have turned a
61
+ * dependency cleanup into a visible regression in indexing. Scaling on a
62
+ * 16-core machine: 1→308, 2→152, 4→80, 8→47.6, unbounded→49.7 ms/text. It
63
+ * saturates at 8; letting ORT take everything is marginally *worse* than
64
+ * taking eight.
65
+ *
66
+ * So: leave two cores for everything else, and never exceed the point where
67
+ * more threads stop buying anything.
68
+ */
69
+ const MAX_INTRA_OP_THREADS = 8;
70
+ function intraOpThreads() {
71
+ return Math.max(1, Math.min(MAX_INTRA_OP_THREADS, availableParallelism() - 2));
72
+ }
73
+ /** Load the model (downloading on a cold cache) and return an embedder. */
74
+ export async function createEmbedder() {
75
+ const tokenizerPath = await ensureFile(MODEL_REPO, "tokenizer.json");
76
+ const modelPath = await ensureFile(MODEL_REPO, "onnx/model.onnx");
77
+ const tokenizer = WordPieceTokenizer.fromFile(tokenizerPath);
78
+ const session = await ort.InferenceSession.create(modelPath, {
79
+ executionProviders: ["cpu"],
80
+ graphOptimizationLevel: "all",
81
+ intraOpNumThreads: intraOpThreads(),
82
+ });
83
+ return {
84
+ async embed(texts) {
85
+ if (texts.length === 0)
86
+ return [];
87
+ const { input_ids, attention_mask, token_type_ids } = tokenizer.encodeBatch(texts);
88
+ const batch = input_ids.length;
89
+ const seqLen = input_ids[0].length;
90
+ const dims = [batch, seqLen];
91
+ const toTensor = (rows) => new ort.Tensor("int64", BigInt64Array.from(rows.flat().map(BigInt)), dims);
92
+ const output = await session.run({
93
+ input_ids: toTensor(input_ids),
94
+ attention_mask: toTensor(attention_mask),
95
+ token_type_ids: toTensor(token_type_ids),
96
+ });
97
+ const hidden = output.last_hidden_state;
98
+ const data = hidden.data;
99
+ const embedDim = hidden.dims[2];
100
+ const vectors = [];
101
+ for (let b = 0; b < batch; b++) {
102
+ const vec = meanPool(data, b * seqLen * embedDim, seqLen, embedDim, attention_mask[b]);
103
+ l2Normalize(vec);
104
+ vectors.push(vec);
105
+ }
106
+ return vectors;
107
+ },
108
+ async dispose() {
109
+ await session.release();
110
+ },
111
+ };
112
+ }
113
+ /**
114
+ * Masked mean pool one sequence out of a `[batch, seq, dim]` hidden state.
115
+ *
116
+ * Padding positions carry mask 0 and so contribute nothing to either the sum
117
+ * or the divisor — which is what lets a text embed identically whether it was
118
+ * batched with longer neighbours or sent alone. Divides by the token count,
119
+ * with no epsilon, matching transformers.js's `mean_pooling` exactly; a
120
+ * sequence always contains at least [CLS] and [SEP], so the count is never 0.
121
+ */
122
+ export function meanPool(hidden, base, seqLen, embedDim, mask) {
123
+ const vec = new Float32Array(embedDim);
124
+ let count = 0;
125
+ for (let j = 0; j < seqLen; j++)
126
+ count += mask[j];
127
+ for (let k = 0; k < embedDim; k++) {
128
+ let sum = 0;
129
+ for (let j = 0; j < seqLen; j++) {
130
+ sum += hidden[base + j * embedDim + k] * mask[j];
131
+ }
132
+ vec[k] = sum / count;
133
+ }
134
+ return vec;
135
+ }
136
+ /** L2-normalize in place, so cosine distance in `memory_vec` is a dot product. */
137
+ export function l2Normalize(vec) {
138
+ let norm = 0;
139
+ for (let k = 0; k < vec.length; k++)
140
+ norm += vec[k] * vec[k];
141
+ norm = Math.sqrt(norm);
142
+ for (let k = 0; k < vec.length; k++)
143
+ vec[k] /= norm;
144
+ }
145
+ //# sourceMappingURL=embedder.js.map
@@ -0,0 +1,71 @@
1
+ import { parentPort } from "node:worker_threads";
2
+ import { createEmbedder, VECTOR_DIM } from "./embedder.js";
3
+ if (!parentPort) {
4
+ throw new Error("embedding-worker.ts must be loaded as a worker thread");
5
+ }
6
+ let embedderPromise = null;
7
+ async function loadExtractor() {
8
+ console.log("Loading embedding model...");
9
+ // The first load may fetch the model from HuggingFace, which can be
10
+ // transiently rate-limited (HTTP 429) or otherwise flaky. Retry with
11
+ // exponential backoff so a cold cache survives a hiccup instead of failing
12
+ // the whole process (notably CI). Once the model is on disk this loop
13
+ // succeeds on the first attempt and never touches the network.
14
+ const MAX_ATTEMPTS = 4;
15
+ let lastErr;
16
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
17
+ try {
18
+ const embedder = await createEmbedder();
19
+ console.log("Embedding model ready.");
20
+ return embedder;
21
+ }
22
+ catch (err) {
23
+ lastErr = err;
24
+ if (attempt === MAX_ATTEMPTS)
25
+ break;
26
+ const delayMs = 1000 * 2 ** (attempt - 1); // 1s, 2s, 4s
27
+ console.warn(`Embedding model load failed (attempt ${attempt}/${MAX_ATTEMPTS}), ` +
28
+ `retrying in ${delayMs}ms:`, err instanceof Error ? err.message : err);
29
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
30
+ }
31
+ }
32
+ throw lastErr;
33
+ }
34
+ function getExtractor() {
35
+ if (!embedderPromise) {
36
+ embedderPromise = loadExtractor().catch((err) => {
37
+ embedderPromise = null;
38
+ throw err;
39
+ });
40
+ }
41
+ return embedderPromise;
42
+ }
43
+ parentPort.on("message", async (msg) => {
44
+ if (msg.kind === "preload") {
45
+ getExtractor().catch((err) => {
46
+ console.error("Failed to preload embedding model:", err);
47
+ });
48
+ return;
49
+ }
50
+ if (msg.kind === "embed") {
51
+ try {
52
+ const embedder = await getExtractor();
53
+ const vectors = await embedder.embed(msg.texts);
54
+ // Concatenate into one buffer so the whole batch crosses the worker
55
+ // boundary as a single transfer rather than N copies; `embedBatch`
56
+ // slices it back apart on the other side.
57
+ const flat = new Float32Array(vectors.length * VECTOR_DIM);
58
+ for (let i = 0; i < vectors.length; i++)
59
+ flat.set(vectors[i], i * VECTOR_DIM);
60
+ parentPort.postMessage({ kind: "embed-result", id: msg.id, flat }, [flat.buffer]);
61
+ }
62
+ catch (err) {
63
+ parentPort.postMessage({
64
+ kind: "embed-error",
65
+ id: msg.id,
66
+ message: err instanceof Error ? err.message : String(err),
67
+ });
68
+ }
69
+ }
70
+ });
71
+ //# sourceMappingURL=embedding-worker.js.map
@@ -0,0 +1,91 @@
1
+ import { Worker } from "node:worker_threads";
2
+ // Re-exported, not redeclared: the dimensionality belongs to the model, and
3
+ // `storage.ts` bakes it into the vec0 schema. Two copies could disagree.
4
+ export { VECTOR_DIM } from "./embedder.js";
5
+ import { VECTOR_DIM } from "./embedder.js";
6
+ let worker = null;
7
+ let nextId = 0;
8
+ const pending = new Map();
9
+ function failAllPending(err) {
10
+ for (const p of pending.values())
11
+ p.reject(err);
12
+ pending.clear();
13
+ }
14
+ function getWorker() {
15
+ if (worker)
16
+ return worker;
17
+ const w = new Worker(new URL("./embedding-worker.js", import.meta.url));
18
+ w.on("message", (msg) => {
19
+ const p = pending.get(msg.id);
20
+ if (!p)
21
+ return;
22
+ pending.delete(msg.id);
23
+ if (msg.kind === "embed-result")
24
+ p.resolve(msg.flat);
25
+ else
26
+ p.reject(new Error(msg.message));
27
+ });
28
+ w.on("error", (err) => {
29
+ failAllPending(err);
30
+ worker = null;
31
+ });
32
+ w.on("exit", () => {
33
+ failAllPending(new Error("embedding worker exited"));
34
+ worker = null;
35
+ });
36
+ worker = w;
37
+ return w;
38
+ }
39
+ /** Preload the embedding model in the background. Safe to call multiple times. */
40
+ export function preloadEmbeddingModel() {
41
+ try {
42
+ getWorker().postMessage({ kind: "preload" });
43
+ }
44
+ catch (err) {
45
+ console.error("Failed to preload embedding model:", err);
46
+ }
47
+ }
48
+ /**
49
+ * Tear down the embedding worker. Calling `worker.terminate()` kills the
50
+ * worker thread and releases its ONNX session — crucially without invoking
51
+ * the main process's `__cxa_finalize`, which is where the
52
+ * `pthread_mutex_lock: Invalid argument` abort used to come from.
53
+ */
54
+ export async function disposeEmbeddingModel() {
55
+ if (!worker)
56
+ return;
57
+ const w = worker;
58
+ worker = null;
59
+ try {
60
+ await w.terminate();
61
+ }
62
+ catch {
63
+ // Worker already gone; nothing to do.
64
+ }
65
+ failAllPending(new Error("embedding worker terminated"));
66
+ }
67
+ function sendEmbed(texts) {
68
+ const w = getWorker();
69
+ const id = nextId++;
70
+ return new Promise((resolve, reject) => {
71
+ pending.set(id, { resolve, reject });
72
+ w.postMessage({ kind: "embed", id, texts });
73
+ });
74
+ }
75
+ /** Embed a single text string, returns normalized 384-dim vector. */
76
+ export async function embedText(text) {
77
+ return sendEmbed([text]);
78
+ }
79
+ /** Embed multiple texts in one call. Returns array of 384-dim vectors. */
80
+ export async function embedBatch(texts) {
81
+ if (texts.length === 0)
82
+ return [];
83
+ const flat = await sendEmbed(texts);
84
+ const vectors = [];
85
+ for (let i = 0; i < texts.length; i++) {
86
+ const start = i * VECTOR_DIM;
87
+ vectors.push(flat.slice(start, start + VECTOR_DIM));
88
+ }
89
+ return vectors;
90
+ }
91
+ //# sourceMappingURL=embeddings.js.map
@@ -0,0 +1,232 @@
1
+ import { embedBatch } from "./embeddings.js";
2
+ import { initMemorySchema, getLastIndexedSeq, setLastIndexedSeq, upsertChunks, } from "./storage.js";
3
+ import { getAgentAdapter, TRACE_RESOLVABLE_AGENTS } from "../agents.js";
4
+ import { fromPortablePath } from "../db/schema.js";
5
+ /** Max events to embed per batch (memory bound). */
6
+ const BATCH_SIZE = 64;
7
+ /**
8
+ * Cooperative-scheduling bounds for `resolvePendingTraces`. The resolver runs
9
+ * in-process on the daemon's main thread and each resolution does a blocking
10
+ * transcript read, so a large backfill could otherwise starve narrate/listen/
11
+ * terminal. We yield to the event loop between reads and cap reads per pass;
12
+ * resolvable events beyond the cap roll to the next index tick (resolved rows
13
+ * drop out of the gate, so progress stays monotonic).
14
+ */
15
+ const RESOLVE_YIELD_EVERY_READS = 16;
16
+ const RESOLVE_YIELD_EVERY_ROWS = 2048;
17
+ const MAX_RESOLVES_PER_PASS = 2000;
18
+ /** Yield control to the event loop so the daemon can service requests. */
19
+ const yieldToEventLoop = () => new Promise((r) => setImmediate(r));
20
+ /**
21
+ * Resolve per-message provenance (meta.session.offset + meta.model) for
22
+ * narration/agent_signal events that don't yet have it. Runs *before*
23
+ * embedding and is gated purely on the *absence* of meta.model — never on
24
+ * the embedding cursor — so it both maintains new events and self-heals
25
+ * (backfills) any historical event still missing the attribution, with no
26
+ * events stranded behind a cursor. Idempotent: a resolved event is skipped
27
+ * forever, and agent session logs are immutable so a value never goes stale.
28
+ *
29
+ * meta.session is *author-relative*: each event's author is its `source`
30
+ * narrative, so we resolve against the author's agent/fold/session — which
31
+ * makes foreign narrations attribute to the authoring agent, not the host.
32
+ */
33
+ export async function resolvePendingTraces(db, signal, opts = {}) {
34
+ // No trace-capable agent → nothing is ever resolvable; skip the scan.
35
+ if (TRACE_RESOLVABLE_AGENTS.length === 0)
36
+ return 0;
37
+ // Push every precondition into SQL so we only materialize rows the resolver
38
+ // could actually act on — instead of pulling the whole unresolved backlog
39
+ // into JS to skip most of it. A row qualifies only when:
40
+ // - its author agent (events.source → narratives) has a resolveTrace
41
+ // adapter (foreign narrations attribute to the *author*, not the host),
42
+ // - meta.model is still absent (gate is absence-of-model, never a cursor,
43
+ // so the resolver self-heals), and
44
+ // - a session id is available — either the live-stamped meta.session.id or
45
+ // the author narrative's agent_session_id fallback. Without one there is
46
+ // no transcript to read, so such rows are pure waste to materialize.
47
+ // The CASE is ordered so json_extract only runs on valid JSON: it *throws*
48
+ // "malformed JSON" on a corrupt blob, which would otherwise abort the pass.
49
+ // appendEvent only writes valid JSON or NULL, so corrupt rows are reachable
50
+ // only via a direct DB edit — they're skipped, not allowed to take down the
51
+ // whole pass.
52
+ const agentPlaceholders = TRACE_RESOLVABLE_AGENTS.map(() => "?").join(", ");
53
+ const rows = db
54
+ .prepare(`SELECT e.narrative_id AS narrative_id, e.seq AS seq, e.content AS content,
55
+ e.source AS source, e.meta AS meta,
56
+ n.agent_type AS agent_type, n.fold_path AS fold_path,
57
+ n.agent_session_id AS agent_session_id
58
+ FROM events e
59
+ JOIN narratives n ON n.id = e.source
60
+ WHERE e.type IN ('narration', 'agent_signal')
61
+ AND e.content IS NOT NULL AND e.content != ''
62
+ AND n.agent_type IN (${agentPlaceholders})
63
+ AND n.fold_path IS NOT NULL
64
+ AND CASE
65
+ WHEN e.meta IS NOT NULL AND json_valid(e.meta) = 0 THEN 0
66
+ WHEN e.meta IS NOT NULL AND json_extract(e.meta, '$.model') IS NOT NULL THEN 0
67
+ WHEN e.meta IS NOT NULL AND json_extract(e.meta, '$.session.id') IS NOT NULL THEN 1
68
+ WHEN n.agent_session_id IS NOT NULL THEN 1
69
+ ELSE 0
70
+ END = 1`)
71
+ .all(...TRACE_RESOLVABLE_AGENTS);
72
+ // Memoize each author's whole-session trace index so a transcript is read and
73
+ // parsed at most once per pass, however many of its events are pending. A
74
+ // `null` (missing/unreadable session file) is cached too, so a dead session
75
+ // isn't re-read for every one of its events.
76
+ const indexCache = new Map();
77
+ // Phase 1: compute all updates with NO DB write lock held (building a trace
78
+ // index does file IO of the agent's own session log, which can be slow).
79
+ // Yields to the event loop around transcript parses and caps parses per pass
80
+ // so this pass can't starve the daemon's main thread (see bounds above).
81
+ const updates = [];
82
+ let reads = 0;
83
+ for (let i = 0; i < rows.length; i++) {
84
+ if (signal?.aborted)
85
+ break;
86
+ // Coarse yield so even a long run of cheap (cache-hit) rows can't block.
87
+ if (i > 0 && i % RESOLVE_YIELD_EVERY_ROWS === 0)
88
+ await yieldToEventLoop();
89
+ const row = rows[i];
90
+ if (!row.agent_type || !row.fold_path)
91
+ continue; // SQL-guarded; defensive
92
+ const adapter = getAgentAdapter(row.agent_type);
93
+ if (!adapter.buildTraceIndex)
94
+ continue; // unsupported agent — leave unset
95
+ const meta = row.meta ? JSON.parse(row.meta) : {};
96
+ // Prefer the session id stamped live at write time; fall back to the
97
+ // author narrative's current session (best-effort for pre-capture events).
98
+ const sessionId = meta.session?.id ?? row.agent_session_id ?? undefined;
99
+ if (!sessionId)
100
+ continue; // SQL-guarded; defensive
101
+ const foldPath = fromPortablePath(row.fold_path);
102
+ // JSON-stringify the pair for an unambiguous key - no separator can collide.
103
+ const cacheKey = JSON.stringify([sessionId, foldPath]);
104
+ let index = indexCache.get(cacheKey);
105
+ if (index === undefined) {
106
+ // Cache miss → parse this transcript once (the expensive op). Bound the
107
+ // parses per pass and yield around them; the rest roll to the next tick.
108
+ // Cheap cache-hit lookups below never consume this budget.
109
+ if (reads >= MAX_RESOLVES_PER_PASS)
110
+ break;
111
+ index = adapter.buildTraceIndex({ sessionId, foldPath });
112
+ indexCache.set(cacheKey, index);
113
+ reads++;
114
+ if (reads % RESOLVE_YIELD_EVERY_READS === 0)
115
+ await yieldToEventLoop();
116
+ }
117
+ if (!index)
118
+ continue; // session file gone/unreadable
119
+ const trace = index.get(row.content);
120
+ if (!trace || (!trace.model && !trace.offset))
121
+ continue;
122
+ const offset = trace.offset ?? meta.session?.offset;
123
+ meta.session = offset ? { id: sessionId, offset } : { id: sessionId };
124
+ if (trace.model)
125
+ meta.model = trace.model;
126
+ updates.push({ narrative_id: row.narrative_id, seq: row.seq, meta: JSON.stringify(meta) });
127
+ }
128
+ if (opts.dryRun || updates.length === 0)
129
+ return updates.length;
130
+ // Phase 2: apply every update in a single transaction (atomic, minimal WAL
131
+ // churn, short lock window). Purely additive — never touches content.
132
+ const stmt = db.prepare("UPDATE events SET meta = ? WHERE narrative_id = ? AND seq = ?");
133
+ const apply = db.transaction((items) => {
134
+ for (const u of items)
135
+ stmt.run(u.meta, u.narrative_id, u.seq);
136
+ });
137
+ apply(updates);
138
+ return updates.length;
139
+ }
140
+ /**
141
+ * Run incremental indexing across all narratives in brane.db.
142
+ *
143
+ * For each narrative, compares max(seq) against last_indexed_seq cursor.
144
+ * Only embeds events with seq > cursor. Idempotent — safe to run repeatedly.
145
+ */
146
+ export async function runIndex(db, rebuild, signal) {
147
+ initMemorySchema(db);
148
+ // Resolve per-message model/session provenance before embedding. Gated on
149
+ // absence of meta.model, independent of the embedding cursor (see fn docs).
150
+ const tracesResolved = await resolvePendingTraces(db, signal);
151
+ if (rebuild) {
152
+ db.exec(`
153
+ DELETE FROM memory_chunks;
154
+ DELETE FROM memory_vec;
155
+ DELETE FROM memory_content_df;
156
+ DELETE FROM memory_stats;
157
+ DELETE FROM memory_cursors;
158
+ INSERT INTO memory_fts(memory_fts) VALUES('rebuild');
159
+ `);
160
+ }
161
+ // Find all narratives with events
162
+ const narratives = db
163
+ .prepare(`SELECT narrative_id, MAX(seq) as max_seq, COUNT(*) as event_count
164
+ FROM events
165
+ GROUP BY narrative_id`)
166
+ .all();
167
+ let narrativesIndexed = 0;
168
+ let totalChunks = 0;
169
+ for (const nar of narratives) {
170
+ if (signal?.aborted)
171
+ break;
172
+ const lastSeq = getLastIndexedSeq(db, nar.narrative_id);
173
+ if (nar.max_seq <= lastSeq)
174
+ continue;
175
+ // Fetch unindexed events with textual content
176
+ const events = db
177
+ .prepare(`SELECT narrative_id, seq, time, type, content
178
+ FROM events
179
+ WHERE narrative_id = ? AND seq > ? AND content IS NOT NULL AND content != ''
180
+ ORDER BY seq ASC`)
181
+ .all(nar.narrative_id, lastSeq);
182
+ if (events.length === 0) {
183
+ // No textual events, but advance cursor to avoid re-scanning
184
+ setLastIndexedSeq(db, nar.narrative_id, nar.max_seq);
185
+ continue;
186
+ }
187
+ console.log(`[${narrativesIndexed + 1}] Embedding ${nar.narrative_id} (${events.length} events)...`);
188
+ // Process in batches
189
+ for (let i = 0; i < events.length; i += BATCH_SIZE) {
190
+ if (signal?.aborted)
191
+ break;
192
+ const batch = events.slice(i, i + BATCH_SIZE);
193
+ const texts = batch.map((e) => e.content);
194
+ const vectors = await embedBatch(texts);
195
+ const chunks = batch.map((e, j) => ({
196
+ id: `${e.narrative_id}-${e.seq}`,
197
+ narrative_id: e.narrative_id,
198
+ seq: e.seq,
199
+ content: e.content,
200
+ time: e.time,
201
+ vector: vectors[j],
202
+ }));
203
+ upsertChunks(db, chunks);
204
+ totalChunks += chunks.length;
205
+ }
206
+ setLastIndexedSeq(db, nar.narrative_id, nar.max_seq);
207
+ narrativesIndexed++;
208
+ }
209
+ return {
210
+ narratives_scanned: narratives.length,
211
+ narratives_indexed: narrativesIndexed,
212
+ chunks_created: totalChunks,
213
+ traces_resolved: tracesResolved,
214
+ };
215
+ }
216
+ /**
217
+ * Check if any narrative has unindexed events.
218
+ * Cheap check — no embedding, just seq comparisons.
219
+ */
220
+ export function hasUnindexedEvents(db) {
221
+ initMemorySchema(db);
222
+ const rows = db
223
+ .prepare(`SELECT e.narrative_id, MAX(e.seq) as max_seq
224
+ FROM events e
225
+ LEFT JOIN memory_cursors mc ON mc.narrative_id = e.narrative_id
226
+ WHERE e.seq > COALESCE(mc.last_seq, 0)
227
+ GROUP BY e.narrative_id
228
+ LIMIT 1`)
229
+ .all();
230
+ return rows.length > 0;
231
+ }
232
+ //# sourceMappingURL=index.js.map