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.
- package/AGENTS.md +367 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +101 -0
- package/DESIGN.md +405 -0
- package/Dockerfile +61 -0
- package/LICENSE +656 -17
- package/README.md +174 -2
- package/SECURITY.md +213 -0
- package/dist/absorb/brane-id.js +11 -0
- package/dist/absorb/chatgpt.js +161 -0
- package/dist/absorb/claude-code.js +504 -0
- package/dist/absorb/index.js +543 -0
- package/dist/absorb/organelle.js +345 -0
- package/dist/absorb/run.js +520 -0
- package/dist/absorb/sources.js +168 -0
- package/dist/absorb/types.js +3 -0
- package/dist/agent-env.js +45 -0
- package/dist/agents.js +1168 -0
- package/dist/claude-live-session.js +124 -0
- package/dist/cli.js +50 -0
- package/dist/codex-tool-log.js +144 -0
- package/dist/commands/absorb.js +291 -0
- package/dist/commands/attach.js +31 -0
- package/dist/commands/certify.js +62 -0
- package/dist/commands/config.js +133 -0
- package/dist/commands/cost.js +126 -0
- package/dist/commands/daemon.js +350 -0
- package/dist/commands/doctor.js +262 -0
- package/dist/commands/fold.js +95 -0
- package/dist/commands/list.js +89 -0
- package/dist/commands/mcp.js +84 -0
- package/dist/commands/meld.js +360 -0
- package/dist/commands/memory.js +215 -0
- package/dist/commands/revive.js +74 -0
- package/dist/commands/run.js +85 -0
- package/dist/commands/spawn.js +1101 -0
- package/dist/commands/token.js +154 -0
- package/dist/copilot-tool-log.js +145 -0
- package/dist/cost/pricing.js +116 -0
- package/dist/cost/reprice.js +136 -0
- package/dist/crush-tool-log.js +293 -0
- package/dist/daemon/agent-scope.js +116 -0
- package/dist/daemon/awaiting.js +54 -0
- package/dist/daemon/cert-manager.js +349 -0
- package/dist/daemon/client.js +262 -0
- package/dist/daemon/gated-routes.js +112 -0
- package/dist/daemon/keep-awake.js +103 -0
- package/dist/daemon/local-socket.js +145 -0
- package/dist/daemon/presence-gate.js +206 -0
- package/dist/daemon/server.js +6353 -0
- package/dist/daemon/signal-source.js +35 -0
- package/dist/daemon/tailscale-rebind.js +58 -0
- package/dist/daemon/token-ring.js +263 -0
- package/dist/daemon/token-routes.js +190 -0
- package/dist/db/events.js +152 -0
- package/dist/db/mentions.js +179 -0
- package/dist/db/schema.js +643 -0
- package/dist/db/tokens.js +624 -0
- package/dist/db/tool-uses.js +647 -0
- package/dist/diagram/index.js +85 -0
- package/dist/diagram/label.js +455 -0
- package/dist/diagram/layout.js +476 -0
- package/dist/diagram/links.js +50 -0
- package/dist/diagram/metrics.js +152 -0
- package/dist/diagram/parse.js +487 -0
- package/dist/diagram/svg.js +389 -0
- package/dist/diagram/types.js +9 -0
- package/dist/export.js +629 -0
- package/dist/files/archive-types.js +2 -0
- package/dist/files/archive.js +182 -0
- package/dist/files/browse.js +327 -0
- package/dist/files/bytes.js +162 -0
- package/dist/files/codecs.js +33 -0
- package/dist/files/find.js +212 -0
- package/dist/files/tar.js +223 -0
- package/dist/files/zip.js +209 -0
- package/dist/folds.js +72 -0
- package/dist/grammars/ebnf.js +92 -0
- package/dist/highlight.js +250 -0
- package/dist/ids.js +24 -0
- package/dist/markdown-diagnostics.js +233 -0
- package/dist/math.js +168 -0
- package/dist/mcp/server.js +886 -0
- package/dist/memory/embedder.js +145 -0
- package/dist/memory/embedding-worker.js +71 -0
- package/dist/memory/embeddings.js +91 -0
- package/dist/memory/index.js +232 -0
- package/dist/memory/storage.js +587 -0
- package/dist/memory/wiki.js +688 -0
- package/dist/memory/wordpiece.js +247 -0
- package/dist/mentions-scan.js +40 -0
- package/dist/presence.js +259 -0
- package/dist/session-affinity.js +185 -0
- package/dist/shared/api-request.js +124 -0
- package/dist/shared/app-origin.js +26 -0
- package/dist/shared/block-glyphs.js +114 -0
- package/dist/shared/diagram-css.js +17 -0
- package/dist/shared/export-selection.js +101 -0
- package/dist/shared/find-in-text.js +110 -0
- package/dist/shared/glow-hues.js +225 -0
- package/dist/shared/hex-dump.js +62 -0
- package/dist/shared/hex-pages.js +52 -0
- package/dist/shared/hit-drum.js +79 -0
- package/dist/shared/known-nids.js +58 -0
- package/dist/shared/list-scroll-keys.js +56 -0
- package/dist/shared/markdown-lists.js +72 -0
- package/dist/shared/markdown-math.js +485 -0
- package/dist/shared/markdown-mentions.js +79 -0
- package/dist/shared/markdown-nids.js +41 -0
- package/dist/shared/markdown-protocols.js +53 -0
- package/dist/shared/markdown-refs.js +123 -0
- package/dist/shared/membrane-invasion.js +342 -0
- package/dist/shared/membrane-motion.js +83 -0
- package/dist/shared/membrane-palette.js +253 -0
- package/dist/shared/membrane-terrain.js +212 -0
- package/dist/shared/message-selection.js +123 -0
- package/dist/shared/mo-glyph.js +57 -0
- package/dist/shared/narrative-format.js +237 -0
- package/dist/shared/narrative-menu.js +84 -0
- package/dist/shared/narrative-sort.js +47 -0
- package/dist/shared/narrative-status.js +52 -0
- package/dist/shared/no-pan.js +85 -0
- package/dist/shared/pane-layout.js +50 -0
- package/dist/shared/payload-inline.js +83 -0
- package/dist/shared/speaker-color.js +61 -0
- package/dist/shared/token-rotation.js +14 -0
- package/dist/shared/view-url.js +216 -0
- package/dist/shared/wiki-links.js +70 -0
- package/dist/shared/wiki-window.js +4 -0
- package/dist/tailscale.js +55 -0
- package/dist/test/absorb-claude.test.js +892 -0
- package/dist/test/agent-scope.test.js +193 -0
- package/dist/test/agents.test.js +55 -0
- package/dist/test/api-request.test.js +122 -0
- package/dist/test/app-host-literal.test.js +53 -0
- package/dist/test/awaiting-glow.test.js +54 -0
- package/dist/test/awaiting.test.js +122 -0
- package/dist/test/bind-token.test.js +88 -0
- package/dist/test/block-glyphs.test.js +68 -0
- package/dist/test/cert-manager.test.js +262 -0
- package/dist/test/claude-live-session.test.js +110 -0
- package/dist/test/codex-tool-log.test.js +568 -0
- package/dist/test/config.test.js +71 -0
- package/dist/test/copilot-tool-log.test.js +93 -0
- package/dist/test/cors-preflight.test.js +56 -0
- package/dist/test/cost-linger.test.js +31 -0
- package/dist/test/cost-pricing.test.js +72 -0
- package/dist/test/cost-reprice.test.js +139 -0
- package/dist/test/cost-sessions.test.js +301 -0
- package/dist/test/crush-tool-log.test.js +289 -0
- package/dist/test/daemon-client.test.js +109 -0
- package/dist/test/daemon-socket.test.js +293 -0
- package/dist/test/diagram-arrows.test.js +205 -0
- package/dist/test/diagram-export.test.js +210 -0
- package/dist/test/diagram-labels.test.js +313 -0
- package/dist/test/diagram-parse.test.js +191 -0
- package/dist/test/diagram-render.test.js +175 -0
- package/dist/test/diagram-review.test.js +288 -0
- package/dist/test/diagram-self-loop-labels.test.js +303 -0
- package/dist/test/embedder.test.js +84 -0
- package/dist/test/events.test.js +123 -0
- package/dist/test/export-selection.test.js +102 -0
- package/dist/test/export.test.js +505 -0
- package/dist/test/files-browse.test.js +393 -0
- package/dist/test/files-find.test.js +272 -0
- package/dist/test/find-in-text.test.js +86 -0
- package/dist/test/first-paint-viewport.test.js +59 -0
- package/dist/test/fixtures/embedding-golden.js +23 -0
- package/dist/test/fixtures/wordpiece-golden.js +167 -0
- package/dist/test/glow-hues.test.js +202 -0
- package/dist/test/harness-cost.test.js +423 -0
- package/dist/test/helpers.js +21 -0
- package/dist/test/hex-pages.test.js +68 -0
- package/dist/test/highlight.test.js +69 -0
- package/dist/test/hit-drum.test.js +71 -0
- package/dist/test/hook-config.test.js +299 -0
- package/dist/test/json-gzip.test.js +70 -0
- package/dist/test/key-routing.test.js +58 -0
- package/dist/test/known-nids.test.js +106 -0
- package/dist/test/link-preview.test.js +67 -0
- package/dist/test/list-scroll-keys.test.js +66 -0
- package/dist/test/listen-own-words.test.js +147 -0
- package/dist/test/listen-timeout.test.js +33 -0
- package/dist/test/markdown-diagnostics.test.js +121 -0
- package/dist/test/markdown-lists.test.js +182 -0
- package/dist/test/markdown-math.test.js +256 -0
- package/dist/test/markdown-nids.test.js +146 -0
- package/dist/test/markdown-protocols.test.js +139 -0
- package/dist/test/mcp-bridge-cancel.test.js +98 -0
- package/dist/test/mcp-bridge-reconnect.test.js +121 -0
- package/dist/test/mcp-narrative.test.js +193 -0
- package/dist/test/mcp.test.js +548 -0
- package/dist/test/meld.test.js +143 -0
- package/dist/test/membrane-invasion.test.js +482 -0
- package/dist/test/membrane-motion.test.js +101 -0
- package/dist/test/membrane-palette.test.js +166 -0
- package/dist/test/membrane-terrain.test.js +170 -0
- package/dist/test/memory-log.test.js +202 -0
- package/dist/test/mentions-e2e.test.js +261 -0
- package/dist/test/mentions-ledger.test.js +265 -0
- package/dist/test/mentions-listen.test.js +265 -0
- package/dist/test/mentions.test.js +238 -0
- package/dist/test/message-selection.test.js +114 -0
- package/dist/test/mo-glyph.test.js +22 -0
- package/dist/test/model-resolution.test.js +166 -0
- package/dist/test/narrate-validate.test.js +77 -0
- package/dist/test/narrate-wait.test.js +160 -0
- package/dist/test/narrative-format.test.js +219 -0
- package/dist/test/narrative-menu.test.js +150 -0
- package/dist/test/narrative-sort.test.js +75 -0
- package/dist/test/no-pan.test.js +60 -0
- package/dist/test/pane-layout.test.js +153 -0
- package/dist/test/paths.test.js +37 -0
- package/dist/test/payload-inline.test.js +91 -0
- package/dist/test/precache-fonts.test.js +72 -0
- package/dist/test/presence-gate.test.js +440 -0
- package/dist/test/presence.test.js +160 -0
- package/dist/test/read-events.test.js +220 -0
- package/dist/test/remote-meld.test.js +128 -0
- package/dist/test/revive-cli.test.js +93 -0
- package/dist/test/search-floor.test.js +206 -0
- package/dist/test/search-fusion.test.js +168 -0
- package/dist/test/search.test.js +205 -0
- package/dist/test/security.test.js +48 -0
- package/dist/test/selection-wiring.test.js +61 -0
- package/dist/test/session-affinity.test.js +295 -0
- package/dist/test/signal-source.test.js +32 -0
- package/dist/test/socket-account.test.js +268 -0
- package/dist/test/spawn-toml.test.js +72 -0
- package/dist/test/speaker-color.test.js +90 -0
- package/dist/test/tailscale-rebind.test.js +79 -0
- package/dist/test/terminal-cursor.test.js +61 -0
- package/dist/test/terminal-resync.test.js +23 -0
- package/dist/test/terminal-size.test.js +36 -0
- package/dist/test/thin-cli.test.js +382 -0
- package/dist/test/token-bootstrap.test.js +272 -0
- package/dist/test/token-devices.test.js +646 -0
- package/dist/test/token-lapse.test.js +94 -0
- package/dist/test/token-ring.test.js +188 -0
- package/dist/test/token-rotate-cli.test.js +58 -0
- package/dist/test/token-routes.test.js +95 -0
- package/dist/test/tool-uses.test.js +1006 -0
- package/dist/test/usage-ledger.test.js +126 -0
- package/dist/test/view-url.test.js +627 -0
- package/dist/test/wiki-links.test.js +57 -0
- package/dist/test/wiki-sources.test.js +85 -0
- package/dist/test/wiki-window.test.js +31 -0
- package/dist/test/wiki.test.js +399 -0
- package/dist/test/wordpiece.test.js +68 -0
- package/dist/test/wrangler-route.test.js +27 -0
- package/dist/tls.js +148 -0
- package/dist/tmux.js +213 -0
- package/dist/user-agent.js +46 -0
- package/docker-entrypoint.sh +154 -0
- package/docs/README.md +3 -0
- package/docs/absorb.md +231 -0
- package/docs/cost-ledger.md +125 -0
- package/docs/daemon-api.md +189 -0
- package/docs/diagrams.md +163 -0
- package/docs/doctor.md +36 -0
- package/docs/export-scope.md +69 -0
- package/docs/files-view.md +133 -0
- package/docs/link-previews.md +38 -0
- package/docs/math.md +32 -0
- package/docs/mcp.md +69 -0
- package/docs/memory-search.md +75 -0
- package/docs/mentions.md +101 -0
- package/docs/multi-brane.md +14 -0
- package/docs/passkeys.md +165 -0
- package/docs/presence.md +202 -0
- package/docs/releasing.md +104 -0
- package/docs/rendering-warnings.md +22 -0
- package/docs/request-layer.md +72 -0
- package/docs/selection-store.md +50 -0
- package/docs/session-affinity.md +61 -0
- package/docs/terminal-mirroring.md +243 -0
- package/docs/token-rotation.md +431 -0
- package/docs/view-urls.md +134 -0
- package/docs/web-app.md +23 -0
- package/docs/wiki.md +43 -0
- package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -0
- package/expo/dist/apple-touch-icon.png +0 -0
- package/expo/dist/favicon.svg +3 -0
- package/expo/dist/index.html +177 -0
- package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/expo/dist/katex/katex.min.css +1 -0
- package/expo/dist/manifest.json +16 -0
- package/expo/dist/metadata.json +1 -0
- package/expo/dist/og/membrane.v1.png +0 -0
- package/expo/dist/pdf.min.mjs +28 -0
- package/expo/dist/pdf.worker.min.mjs +28 -0
- package/expo/dist/sw.js +154 -0
- package/package.json +79 -27
- package/protocols/LIVE_loop.md +407 -0
- package/protocols/bebrief.md +41 -0
- package/protocols/digest.md +231 -0
- package/protocols/fly.md +266 -0
- package/protocols/idle.md +81 -0
- package/protocols/keeper.md +829 -0
- package/protocols/markdown.md +80 -0
- package/protocols/memory.md +419 -0
- package/protocols/mentions.md +93 -0
- package/protocols/payloads.md +53 -0
- package/protocols/prelude.md +91 -0
- package/protocols/ramify.md +105 -0
- package/protocols/review.md +356 -0
- package/protocols/rrr.md +232 -0
- package/protocols/scribe.md +429 -0
- package/protocols/summarize.md +203 -0
- package/protocols/twin.md +174 -0
- package/protocols/wiki.md +438 -0
- package/protocols/worker.md +38 -0
- package/wiki/shared/append-only-event-log.md +148 -0
- package/wiki/shared/index.md +53 -0
- package/wiki/shared/owning-your-narratives.md +161 -0
- package/wiki/shared/premature-ontologies.md +435 -0
- package/wiki/shared/sedimentary-superintelligence.md +128 -0
- package/wiki/shared/sequestering-the-narrative.md +143 -0
- package/wiki/shared/why-wikis-work.md +216 -0
- package/.travis.yml +0 -7
- package/lib/brane.cjs.js +0 -12
- package/lib/brane.cjs.js.map +0 -1
- package/lib/brane.d.ts +0 -11
- package/lib/brane.js +0 -6
- package/lib/brane.js.map +0 -1
- package/lib/tests.d.ts +0 -1
- package/lib/tests.js +0 -8
- package/lib/tests.js.map +0 -1
- package/rollup.config.js +0 -21
- package/scripts/test.sh +0 -8
- package/src/brane.ts +0 -21
- package/src/tests.ts +0 -8
- 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
|