trajectories 0.0.0 → 0.1.2

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 (205) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +201 -0
  3. package/README.md +288 -2
  4. package/dist/cli.js +30210 -0
  5. package/dist/convert.d.ts +31 -0
  6. package/dist/convert.js +431 -0
  7. package/dist/deepagents_checkpoint.py +362 -0
  8. package/dist/index.d.ts +7 -0
  9. package/dist/index.js +8402 -0
  10. package/dist/log-baseline.d.ts +26 -0
  11. package/dist/log-baseline.js +104 -0
  12. package/dist/log-content.d.ts +16 -0
  13. package/dist/log-content.js +80 -0
  14. package/dist/log-io.d.ts +10 -0
  15. package/dist/log-io.js +121 -0
  16. package/dist/log-read.d.ts +24 -0
  17. package/dist/log-read.js +117 -0
  18. package/dist/log-reconcile.d.ts +5 -0
  19. package/dist/log-reconcile.js +38 -0
  20. package/dist/log-records.d.ts +35 -0
  21. package/dist/log-records.js +85 -0
  22. package/dist/log-recovery.d.ts +18 -0
  23. package/dist/log-recovery.js +123 -0
  24. package/dist/log-stage.d.ts +14 -0
  25. package/dist/log-stage.js +138 -0
  26. package/dist/log-state.d.ts +89 -0
  27. package/dist/log-state.js +92 -0
  28. package/dist/log-switch.d.ts +3 -0
  29. package/dist/log-switch.js +98 -0
  30. package/dist/log-types.d.ts +129 -0
  31. package/dist/log-types.js +8 -0
  32. package/dist/log.d.ts +8 -0
  33. package/dist/log.js +62 -0
  34. package/dist/migrate.d.ts +29 -0
  35. package/dist/migrate.js +173 -0
  36. package/dist/normalization-runtime.d.ts +1 -0
  37. package/dist/normalization-runtime.js +3150 -0
  38. package/dist/portable.d.ts +9 -0
  39. package/dist/portable.js +26 -0
  40. package/dist/readers/canonical.d.ts +13 -0
  41. package/dist/readers/canonical.js +54 -0
  42. package/dist/readers/cline.d.ts +10 -0
  43. package/dist/readers/cline.js +107 -0
  44. package/dist/readers/cursor.d.ts +20 -0
  45. package/dist/readers/cursor.js +374 -0
  46. package/dist/readers/deepagents.d.ts +41 -0
  47. package/dist/readers/deepagents.js +163 -0
  48. package/dist/readers/fx.d.ts +11 -0
  49. package/dist/readers/fx.js +573 -0
  50. package/dist/readers/github-copilot.d.ts +8 -0
  51. package/dist/readers/github-copilot.js +216 -0
  52. package/dist/readers/grok-build.d.ts +12 -0
  53. package/dist/readers/grok-build.js +391 -0
  54. package/dist/readers/guards.d.ts +4 -0
  55. package/dist/readers/guards.js +121 -0
  56. package/dist/readers/pi-compaction.d.ts +7 -0
  57. package/dist/readers/pi-compaction.js +112 -0
  58. package/dist/readers/pi-context.d.ts +13 -0
  59. package/dist/readers/pi-context.js +64 -0
  60. package/dist/store.d.ts +25 -0
  61. package/dist/store.js +218 -0
  62. package/dist/types.d.ts +91 -0
  63. package/dist/types.js +1 -0
  64. package/dist/writers/claude-code.d.ts +5 -0
  65. package/dist/writers/claude-code.js +51 -0
  66. package/dist/writers/cline.d.ts +3 -0
  67. package/dist/writers/cline.js +77 -0
  68. package/dist/writers/codex.d.ts +3 -0
  69. package/dist/writers/codex.js +32 -0
  70. package/dist/writers/cursor.d.ts +3 -0
  71. package/dist/writers/cursor.js +197 -0
  72. package/dist/writers/deepagents.d.ts +3 -0
  73. package/dist/writers/deepagents.js +116 -0
  74. package/dist/writers/entropy.d.ts +5 -0
  75. package/dist/writers/entropy.js +33 -0
  76. package/dist/writers/fx.d.ts +3 -0
  77. package/dist/writers/fx.js +135 -0
  78. package/dist/writers/github-copilot.d.ts +3 -0
  79. package/dist/writers/github-copilot.js +71 -0
  80. package/dist/writers/grok-build.d.ts +3 -0
  81. package/dist/writers/grok-build.js +43 -0
  82. package/dist/writers/helpers.d.ts +17 -0
  83. package/dist/writers/helpers.js +75 -0
  84. package/dist/writers/omp.d.ts +3 -0
  85. package/dist/writers/omp.js +7 -0
  86. package/dist/writers/opencode.d.ts +3 -0
  87. package/dist/writers/opencode.js +65 -0
  88. package/dist/writers/pi.d.ts +3 -0
  89. package/dist/writers/pi.js +45 -0
  90. package/docs/cline-native-format.md +43 -0
  91. package/docs/copilot-native-format.md +78 -0
  92. package/docs/cursor-native-format.md +103 -0
  93. package/docs/deepagents-native-format.md +98 -0
  94. package/docs/evidence-api.md +170 -0
  95. package/docs/fx-native-format.md +87 -0
  96. package/docs/harness-parity-bootstrap.md +78 -0
  97. package/docs/harness-support-parity.md +107 -0
  98. package/docs/inspirations.md +103 -0
  99. package/docs/ledger-migration.md +141 -0
  100. package/docs/pi-compaction-context.md +75 -0
  101. package/docs/pi-provider-context.md +76 -0
  102. package/docs/research/2026-10-06-huihua-and-oar.md +176 -0
  103. package/docs/research/ledger/README.md +96 -0
  104. package/docs/research/ledger/confer/2026-09-13-round-10/README.md +61 -0
  105. package/docs/research/ledger/confer/2026-09-13-round-2/cass.md +72 -0
  106. package/docs/research/ledger/confer/2026-09-13-round-3/README.md +70 -0
  107. package/docs/research/ledger/confer/2026-09-13-round-5/README.md +61 -0
  108. package/docs/research/ledger/confer/2026-09-13-round-6/README.md +68 -0
  109. package/docs/research/ledger/confer/2026-09-13-round-7/README.md +64 -0
  110. package/docs/research/ledger/confer/2026-09-13-round-9/README.md +59 -0
  111. package/docs/research/ledger/confer/2026-09-14-wake/wake.md +146 -0
  112. package/docs/research/ledger/docs/inspirations.md +320 -0
  113. package/docs/research/ledger/docs/pr2-pr6-integration.md +53 -0
  114. package/docs/research/ledger/docs/research/2026-09-07-confer-inspirations.md +163 -0
  115. package/docs/research/ledger/docs/research/2026-09-07-letta-continuous-service.md +104 -0
  116. package/docs/research/ledger/docs/research/2026-09-08-claude-history-retrieval.md +117 -0
  117. package/docs/research/ledger/docs/research/2026-09-09-agentsview-native-discovery.md +87 -0
  118. package/docs/research/ledger/docs/research/2026-09-09-daemon-observation.md +231 -0
  119. package/docs/research/ledger/docs/research/2026-09-09-discovery-pagination.md +114 -0
  120. package/docs/research/ledger/docs/research/2026-09-09-harness-transcript-contract.md +145 -0
  121. package/docs/research/ledger/docs/research/2026-09-09-native-history-discovery.md +180 -0
  122. package/docs/research/ledger/docs/research/2026-09-09-native-history-implementation.md +137 -0
  123. package/docs/research/ledger/docs/research/2026-09-09-other-inspirations.md +279 -0
  124. package/docs/research/ledger/docs/research/2026-09-09-trajectory-and-letta-code.md +128 -0
  125. package/docs/research/ledger/docs/research/2026-09-13-diagnostics-coverage.md +116 -0
  126. package/docs/research/ledger/docs/research/2026-09-13-fts-rebuild.md +83 -0
  127. package/docs/research/ledger/docs/research/2026-09-13-read-path-split.md +74 -0
  128. package/docs/research/ledger/docs/research/2026-09-13-runtime-rollout.md +102 -0
  129. package/docs/research/ledger/docs/research/2026-09-13-scan-outcomes.md +80 -0
  130. package/docs/research/ledger/docs/research/2026-09-13-search-quality.md +105 -0
  131. package/docs/research/ledger/docs/research/2026-09-13-source-coverage.md +64 -0
  132. package/docs/research/ledger/docs/research/2026-09-13-sync-wall-budget.md +82 -0
  133. package/docs/research/ledger/docs/research/2026-09-13-typed-read-gate.md +81 -0
  134. package/docs/research/ledger/docs/research/2026-09-13-writer-lease.md +84 -0
  135. package/docs/research/ledger/docs/research/2026-09-16-c1-copy-fallback.md +227 -0
  136. package/docs/research/ledger/docs/research/2026-09-16-c10-kimi.md +414 -0
  137. package/docs/research/ledger/docs/research/2026-09-16-c2-derive-counter.md +216 -0
  138. package/docs/research/ledger/docs/research/2026-09-16-c3-echo-filter.md +305 -0
  139. package/docs/research/ledger/docs/research/2026-09-16-c4-title-index.md +349 -0
  140. package/docs/research/ledger/docs/research/2026-09-16-c5-trigram.md +268 -0
  141. package/docs/research/ledger/docs/research/2026-09-16-c6-recency-prior.md +438 -0
  142. package/docs/research/ledger/docs/research/2026-09-16-c7-tombstones.md +467 -0
  143. package/docs/research/ledger/docs/research/2026-09-16-c8-remote-truth.md +390 -0
  144. package/docs/research/ledger/docs/research/2026-09-16-c9-grok.md +380 -0
  145. package/docs/research/ledger/docs/research/2026-09-16-wake-port-program.md +38 -0
  146. package/docs/research/ledger/docs/research/2026-09-17-cursor-blobstore.md +157 -0
  147. package/docs/research/ledger/manifest.json +540 -0
  148. package/legacy/NOTICE.md +50 -0
  149. package/legacy/bin/ledger-core.ts +7 -0
  150. package/legacy/bin/ledger.ts +7 -0
  151. package/legacy/bin/traj-bridge.ts +7 -0
  152. package/legacy/scripts/import-claude-history.ts +130 -0
  153. package/legacy/scripts/native-history-smoke.ts +52 -0
  154. package/legacy/scripts/observe-daemon.ts +570 -0
  155. package/legacy/scripts/retitle-sessions.md +19 -0
  156. package/legacy/scripts/verify-runtime-rollout.ts +151 -0
  157. package/legacy/src/canonical.ts +108 -0
  158. package/legacy/src/cli.ts +1293 -0
  159. package/legacy/src/db.ts +2254 -0
  160. package/legacy/src/diagnostics.ts +87 -0
  161. package/legacy/src/discovery.ts +56 -0
  162. package/legacy/src/embeddings.ts +382 -0
  163. package/legacy/src/filter.ts +602 -0
  164. package/legacy/src/gemini_embeddings.ts +103 -0
  165. package/legacy/src/identity.ts +9 -0
  166. package/legacy/src/index.ts +10 -0
  167. package/legacy/src/ingest.ts +791 -0
  168. package/legacy/src/managed.ts +178 -0
  169. package/legacy/src/native/common.ts +68 -0
  170. package/legacy/src/native/cursor.ts +647 -0
  171. package/legacy/src/native/grok.ts +540 -0
  172. package/legacy/src/native/kimi.ts +706 -0
  173. package/legacy/src/native/opencode.ts +470 -0
  174. package/legacy/src/native/sqlite-ro.ts +281 -0
  175. package/legacy/src/native.ts +97 -0
  176. package/legacy/src/normalizer.ts +457 -0
  177. package/legacy/src/parentage.ts +562 -0
  178. package/legacy/src/pressure.ts +162 -0
  179. package/legacy/src/private-storage.ts +74 -0
  180. package/legacy/src/reader.ts +1255 -0
  181. package/legacy/src/scanner.ts +591 -0
  182. package/legacy/src/schema-gate.ts +263 -0
  183. package/legacy/src/schema.ts +829 -0
  184. package/legacy/src/semantic-top-k.ts +71 -0
  185. package/legacy/src/session-hash.ts +49 -0
  186. package/legacy/src/store-status.ts +218 -0
  187. package/legacy/src/sync-preparation.ts +45 -0
  188. package/legacy/src/sync-truth.ts +843 -0
  189. package/legacy/src/sync.ts +1233 -0
  190. package/legacy/src/types.ts +436 -0
  191. package/legacy/src/vocab.txt +30522 -0
  192. package/legacy/src/watch.ts +673 -0
  193. package/legacy/src/writer-lease.ts +310 -0
  194. package/ops/README.md +150 -0
  195. package/ops/install-cli.sh +63 -0
  196. package/ops/install-native.sh +35 -0
  197. package/ops/install-release.sh +49 -0
  198. package/ops/linux/install-systemd-user.sh +103 -0
  199. package/ops/macos/install-launchd.sh +72 -0
  200. package/ops/sync-settings.sh +37 -0
  201. package/package.json +21 -5
  202. package/skills/trajectories/SKILL.md +138 -0
  203. package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
  204. package/skills/trajectories/scripts/trajectories_search.sh +26 -0
  205. package/index.js +0 -1
@@ -0,0 +1,87 @@
1
+ import type { IngestDiagnosticAggregate } from "./types.js";
2
+
3
+ type IngestDiagnostic = { code: string; message: string; count?: number };
4
+
5
+ // Upstream emits one diagnostic per record, so a stored sample keeps only the
6
+ // last message per code, bounded so no single message can inflate a row.
7
+ const DIAGNOSTIC_SAMPLE_CODE_POINTS = 200;
8
+
9
+ /**
10
+ * Bound a diagnostic message to the stored sample length. Counting code points,
11
+ * not UTF-16 units, keeps an astral character at the boundary whole instead of
12
+ * leaving a lone surrogate behind. Empty messages store as null.
13
+ */
14
+ export function capDiagnosticSample(message: string): string | null {
15
+ if (!message) return null;
16
+ const codePoints = Array.from(message);
17
+ return codePoints.length > DIAGNOSTIC_SAMPLE_CODE_POINTS
18
+ ? codePoints.slice(0, DIAGNOSTIC_SAMPLE_CODE_POINTS).join("")
19
+ : message;
20
+ }
21
+
22
+ // `count` is upstream's own aggregation when it reports one (e.g. records
23
+ // affected by timestamps_interpolated). Zero is a real observation and must
24
+ // survive; only an absent or nonsensical count falls back to one occurrence.
25
+ function occurrenceCountOf(diagnostic: IngestDiagnostic): number {
26
+ const count = diagnostic.count;
27
+ return typeof count === "number" && Number.isSafeInteger(count) && count >= 0 ? count : 1;
28
+ }
29
+
30
+ /**
31
+ * Collapse normalization and conversion diagnostics into one entry per code.
32
+ * Diagnostic text never carries raw transcript content (Trajectory's canonical
33
+ * contract), so the last message per code is safe to store as a bounded sample.
34
+ */
35
+ export function aggregateIngestDiagnostics(
36
+ diagnostics: readonly IngestDiagnostic[],
37
+ observedAt = new Date().toISOString(),
38
+ ): IngestDiagnosticAggregate[] {
39
+ const byCode = new Map<string, IngestDiagnosticAggregate>();
40
+ for (const diagnostic of diagnostics) {
41
+ const count = occurrenceCountOf(diagnostic);
42
+ const sample = capDiagnosticSample(diagnostic.message);
43
+ const existing = byCode.get(diagnostic.code);
44
+ if (existing) {
45
+ existing.occurrenceCount += count;
46
+ existing.sample = sample;
47
+ } else {
48
+ byCode.set(diagnostic.code, {
49
+ code: diagnostic.code,
50
+ occurrenceCount: count,
51
+ sample,
52
+ firstSeenAt: observedAt,
53
+ });
54
+ }
55
+ }
56
+ return [...byCode.values()];
57
+ }
58
+
59
+ /**
60
+ * Union two aggregates by code, as a staged replacement accumulates the chunks
61
+ * that produced it: counts add (a chunk was observed once), the later sample
62
+ * wins, and the earliest observation is kept. `undefined` means "no
63
+ * information" and is never invented from an empty group.
64
+ */
65
+ export function mergeIngestDiagnosticAggregates(
66
+ carried: readonly IngestDiagnosticAggregate[] | undefined,
67
+ incoming: readonly IngestDiagnosticAggregate[] | undefined,
68
+ ): IngestDiagnosticAggregate[] | undefined {
69
+ if (!carried && !incoming) return undefined;
70
+ const merged = new Map<string, IngestDiagnosticAggregate>();
71
+ for (const entry of carried ?? []) {
72
+ merged.set(entry.code, { ...entry });
73
+ }
74
+ for (const entry of incoming ?? []) {
75
+ const existing = merged.get(entry.code);
76
+ if (!existing) {
77
+ merged.set(entry.code, { ...entry });
78
+ continue;
79
+ }
80
+ existing.occurrenceCount += entry.occurrenceCount;
81
+ if (entry.sample) existing.sample = entry.sample;
82
+ if (entry.firstSeenAt && (!existing.firstSeenAt || entry.firstSeenAt < existing.firstSeenAt)) {
83
+ existing.firstSeenAt = entry.firstSeenAt;
84
+ }
85
+ }
86
+ return [...merged.values()];
87
+ }
@@ -0,0 +1,56 @@
1
+ import type { DiscoveredSession } from "./types.js";
2
+
3
+ /** Resumable discovery, distinct from each session's normalization cursor. */
4
+ export interface DiscoveryCheckpoint {
5
+ source: string;
6
+ root_key: string;
7
+ cursor: string | null;
8
+ status: "pending" | "exhausted" | "unknown";
9
+ degraded: boolean;
10
+ last_completed_at: string | null;
11
+ updated_at: string;
12
+ }
13
+ export interface DiscoverySummary {
14
+ total_sources: number;
15
+ pending_sources: number;
16
+ unknown_sources: number;
17
+ exhausted_sources: number;
18
+ /** Oldest completed sweep, null until every recorded source has completed one. */
19
+ oldest_completed_at: string | null;
20
+ }
21
+
22
+ function identity(item: DiscoveredSession): string {
23
+ return JSON.stringify([item.path, item.id]);
24
+ }
25
+ export function uniqueDiscovered(items: DiscoveredSession[]): DiscoveredSession[] {
26
+ const seen = new Set<string>();
27
+ return items.filter(item => {
28
+ const key = identity(item);
29
+ if (seen.has(key)) return false;
30
+ seen.add(key);
31
+ return true;
32
+ });
33
+ }
34
+ export function discoveryHeadBudget(limit: number, cursor: string | null): number {
35
+ return cursor && limit > 1 ? Math.min(32, Math.max(1, Math.floor(limit / 4))) : 0;
36
+ }
37
+
38
+ /**
39
+ * Native scanners already produce a metadata inventory. Page it by stable
40
+ * identity, not mutable mtime/offset; reserve a small head lane for freshness.
41
+ * Metadata enumeration itself remains isolated by the preparation deadline.
42
+ */
43
+ export function pageDiscoveryInventory(
44
+ inventory: DiscoveredSession[], limit: number, cursor: string | null,
45
+ ): { items: DiscoveredSession[]; next: string | null } {
46
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new RangeError("Discovery limit must be positive.");
47
+ const after = cursor?.startsWith("k:") ? cursor.slice(2) : null;
48
+ const head = inventory.slice(0, discoveryHeadBudget(limit, cursor));
49
+ const ordered = inventory.map(item => ({ item, key: identity(item) }))
50
+ .filter(entry => after === null || entry.key > after)
51
+ .sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
52
+ const historical = ordered.slice(0, limit - head.length);
53
+ const next = historical.length && historical.length < ordered.length
54
+ ? `k:${historical.at(-1)!.key}` : null;
55
+ return { items: uniqueDiscovered([...head, ...historical.map(entry => entry.item)]), next };
56
+ }
@@ -0,0 +1,382 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import type * as Ort from "onnxruntime-node";
5
+ let ort: typeof Ort;
6
+
7
+ export const DEFAULT_MODEL_NAME = "all-MiniLM-L6-v2";
8
+ export const EMBEDDING_DIMENSION = 384;
9
+
10
+ const HF_MODEL_URL =
11
+ "https://huggingface.co/Xenova/all-MiniLM-L6-v2/resolve/main/onnx/model_quantized.onnx";
12
+ const HF_VOCAB_URL =
13
+ "https://huggingface.co/Xenova/all-MiniLM-L6-v2/raw/main/vocab.txt";
14
+
15
+ export interface EmbeddingProvider {
16
+ name: string;
17
+ dimension: number;
18
+ embed(texts: string[]): Promise<Float32Array[]>;
19
+ }
20
+
21
+ export function getModelsDir(): string {
22
+ return (
23
+ process.env.LEDGER_MODELS_DIR ||
24
+ join(homedir(), ".cache", "ledger", "models")
25
+ );
26
+ }
27
+
28
+ export function getModelPath(): string {
29
+ const baseDir = getModelsDir();
30
+ // Check both direct path and subfolder
31
+ const nestedPath = join(baseDir, DEFAULT_MODEL_NAME, "model_quantized.onnx");
32
+ if (existsSync(nestedPath)) return nestedPath;
33
+
34
+ const directPath = join(baseDir, "model_quantized.onnx");
35
+ if (existsSync(directPath)) return directPath;
36
+
37
+ return nestedPath;
38
+ }
39
+
40
+ export function getVocabPath(): string {
41
+ const baseDir = getModelsDir();
42
+ const nestedPath = join(baseDir, DEFAULT_MODEL_NAME, "vocab.txt");
43
+ if (existsSync(nestedPath)) return nestedPath;
44
+
45
+ const directPath = join(baseDir, "vocab.txt");
46
+ if (existsSync(directPath)) return directPath;
47
+
48
+ // Check bundled vocab in package
49
+ const bundledPath = join(import.meta.dir, "vocab.txt");
50
+ if (existsSync(bundledPath)) return bundledPath;
51
+
52
+ return nestedPath;
53
+ }
54
+
55
+ export async function pullModel(options: { verbose?: boolean } = {}): Promise<{
56
+ modelPath: string;
57
+ vocabPath: string;
58
+ }> {
59
+ const modelPath = getModelPath();
60
+ const vocabPath = getVocabPath();
61
+ const modelDir = dirname(modelPath);
62
+
63
+ if (!existsSync(modelDir)) {
64
+ mkdirSync(modelDir, { recursive: true });
65
+ }
66
+
67
+ if (!existsSync(modelPath)) {
68
+ if (options.verbose) {
69
+ console.log(`Downloading embedding model from Hugging Face to ${modelPath}...`);
70
+ }
71
+ const res = await fetch(HF_MODEL_URL, { redirect: "follow" });
72
+ if (!res.ok) {
73
+ throw new Error(
74
+ `Failed to download model from ${HF_MODEL_URL}: ${res.status} ${res.statusText}`,
75
+ );
76
+ }
77
+ const buffer = await res.arrayBuffer();
78
+ writeFileSync(modelPath, Buffer.from(buffer));
79
+ if (options.verbose) {
80
+ console.log(`Saved model (${(buffer.byteLength / (1024 * 1024)).toFixed(1)} MB) to ${modelPath}`);
81
+ }
82
+ }
83
+
84
+ if (!existsSync(vocabPath)) {
85
+ // Check bundled first before downloading
86
+ const bundledPath = join(import.meta.dir, "vocab.txt");
87
+ if (existsSync(bundledPath)) {
88
+ const vocabContent = readFileSync(bundledPath);
89
+ writeFileSync(vocabPath, vocabContent);
90
+ } else {
91
+ if (options.verbose) {
92
+ console.log(`Downloading vocabulary from ${HF_VOCAB_URL}...`);
93
+ }
94
+ const res = await fetch(HF_VOCAB_URL, { redirect: "follow" });
95
+ if (!res.ok) {
96
+ throw new Error(
97
+ `Failed to download vocab from ${HF_VOCAB_URL}: ${res.status} ${res.statusText}`,
98
+ );
99
+ }
100
+ const text = await res.text();
101
+ writeFileSync(vocabPath, text, "utf-8");
102
+ }
103
+ }
104
+
105
+ return { modelPath, vocabPath };
106
+ }
107
+
108
+ export async function ensureModelFiles(options: { verbose?: boolean } = {}): Promise<{
109
+ modelPath: string;
110
+ vocabPath: string;
111
+ }> {
112
+ const modelPath = getModelPath();
113
+ const vocabPath = getVocabPath();
114
+
115
+ if (existsSync(modelPath) && existsSync(vocabPath)) {
116
+ return { modelPath, vocabPath };
117
+ }
118
+
119
+ return pullModel(options);
120
+ }
121
+
122
+ class WordPieceTokenizer {
123
+ private vocab = new Map<string, number>();
124
+ private unkId = 100;
125
+ private clsId = 101;
126
+ private sepId = 102;
127
+ private padId = 0;
128
+
129
+ constructor(vocabPath: string) {
130
+ const vocabText = readFileSync(vocabPath, "utf-8");
131
+ const lines = vocabText.split("\n");
132
+ for (let i = 0; i < lines.length; i++) {
133
+ const line = lines[i].trim();
134
+ if (line) this.vocab.set(line, i);
135
+ }
136
+ this.unkId = this.vocab.get("[UNK]") ?? 100;
137
+ this.clsId = this.vocab.get("[CLS]") ?? 101;
138
+ this.sepId = this.vocab.get("[SEP]") ?? 102;
139
+ this.padId = this.vocab.get("[PAD]") ?? 0;
140
+ }
141
+
142
+ tokenize(text: string): number[] {
143
+ const cleaned = text
144
+ .toLowerCase()
145
+ .normalize("NFD")
146
+ .replace(/[\u0300-\u036f]/g, "");
147
+ const words = cleaned.match(/[\p{L}\p{N}]+|[^\s\p{L}\p{N}]/gu) ?? [];
148
+ const tokens: number[] = [this.clsId];
149
+
150
+ for (const word of words) {
151
+ let start = 0;
152
+ const subTokens: number[] = [];
153
+ while (start < word.length) {
154
+ let end = word.length;
155
+ let curId: number | null = null;
156
+ while (start < end) {
157
+ let sub = word.slice(start, end);
158
+ if (start > 0) sub = "##" + sub;
159
+ if (this.vocab.has(sub)) {
160
+ curId = this.vocab.get(sub)!;
161
+ break;
162
+ }
163
+ end--;
164
+ }
165
+ if (curId === null) {
166
+ subTokens.push(this.unkId);
167
+ break;
168
+ }
169
+ subTokens.push(curId);
170
+ start = end;
171
+ }
172
+ tokens.push(...subTokens);
173
+ }
174
+ tokens.push(this.sepId);
175
+ return tokens;
176
+ }
177
+
178
+ get PAD_ID(): number {
179
+ return this.padId;
180
+ }
181
+ }
182
+
183
+ export class OnnxEmbeddingProvider implements EmbeddingProvider {
184
+ readonly name = DEFAULT_MODEL_NAME;
185
+ readonly dimension = EMBEDDING_DIMENSION;
186
+
187
+ private session: Ort.InferenceSession | null = null;
188
+ private tokenizer: WordPieceTokenizer | null = null;
189
+ private initPromise: Promise<void> | null = null;
190
+
191
+ private async init(): Promise<void> {
192
+ if (this.session && this.tokenizer) return;
193
+ if (this.initPromise) return this.initPromise;
194
+
195
+ this.initPromise = (async () => {
196
+ const { modelPath, vocabPath } = await ensureModelFiles();
197
+ this.tokenizer = new WordPieceTokenizer(vocabPath);
198
+ ort ??= await import("onnxruntime-node");
199
+ this.session = await ort.InferenceSession.create(modelPath);
200
+ })();
201
+
202
+ return this.initPromise;
203
+ }
204
+
205
+ async embed(texts: string[]): Promise<Float32Array[]> {
206
+ if (texts.length === 0) return [];
207
+ await this.init();
208
+
209
+ // Chunk to bound ONNX memory: whole-session batches OOM on large
210
+ // sessions (observed: 4GB+ RSS, cgroup kill). 32 x 256 tokens is a safe
211
+ // ceiling on a 4 vCPU/15GB host.
212
+ const CHUNK = 32;
213
+ const out: Float32Array[] = [];
214
+ for (let start = 0; start < texts.length; start += CHUNK) {
215
+ const chunk = await this.embedBatch(texts.slice(start, start + CHUNK));
216
+ out.push(...chunk);
217
+ }
218
+ return out;
219
+ }
220
+
221
+ private async embedBatch(texts: string[]): Promise<Float32Array[]> {
222
+ if (texts.length === 0) return [];
223
+ await this.init();
224
+
225
+ const tokenizer = this.tokenizer!;
226
+ const session = this.session!;
227
+
228
+ const maxSeqLen = 256;
229
+ const tokenized = texts.map((t) => tokenizer.tokenize(t).slice(0, maxSeqLen));
230
+ const maxLen = Math.max(1, ...tokenized.map((t) => t.length));
231
+ const batchSize = texts.length;
232
+
233
+ const inputIds = new BigInt64Array(batchSize * maxLen);
234
+ const attentionMask = new BigInt64Array(batchSize * maxLen);
235
+ const tokenTypeIds = new BigInt64Array(batchSize * maxLen);
236
+
237
+ for (let i = 0; i < batchSize; i++) {
238
+ const tokens = tokenized[i];
239
+ for (let j = 0; j < maxLen; j++) {
240
+ const idx = i * maxLen + j;
241
+ if (j < tokens.length) {
242
+ inputIds[idx] = BigInt(tokens[j]);
243
+ attentionMask[idx] = 1n;
244
+ } else {
245
+ inputIds[idx] = BigInt(tokenizer.PAD_ID);
246
+ attentionMask[idx] = 0n;
247
+ }
248
+ tokenTypeIds[idx] = 0n;
249
+ }
250
+ }
251
+
252
+ const feeds = {
253
+ input_ids: new ort.Tensor("int64", inputIds, [batchSize, maxLen]),
254
+ attention_mask: new ort.Tensor("int64", attentionMask, [batchSize, maxLen]),
255
+ token_type_ids: new ort.Tensor("int64", tokenTypeIds, [batchSize, maxLen]),
256
+ };
257
+
258
+ const results = await session.run(feeds);
259
+ const hiddenState = results.last_hidden_state.data as Float32Array;
260
+ const hiddenDim = this.dimension;
261
+
262
+ const embeddings: Float32Array[] = [];
263
+ for (let i = 0; i < batchSize; i++) {
264
+ const pooled = new Float32Array(hiddenDim);
265
+ let tokenCount = 0;
266
+ for (let j = 0; j < maxLen; j++) {
267
+ if (attentionMask[i * maxLen + j] === 1n) {
268
+ tokenCount++;
269
+ const offset = i * maxLen * hiddenDim + j * hiddenDim;
270
+ for (let d = 0; d < hiddenDim; d++) {
271
+ pooled[d] += hiddenState[offset + d];
272
+ }
273
+ }
274
+ }
275
+ const count = Math.max(tokenCount, 1);
276
+ let normSq = 0;
277
+ for (let d = 0; d < hiddenDim; d++) {
278
+ pooled[d] /= count;
279
+ normSq += pooled[d] * pooled[d];
280
+ }
281
+ const norm = Math.sqrt(normSq);
282
+ if (norm > 0) {
283
+ for (let d = 0; d < hiddenDim; d++) {
284
+ pooled[d] /= norm;
285
+ }
286
+ }
287
+ embeddings.push(pooled);
288
+ }
289
+
290
+ return embeddings;
291
+ }
292
+ }
293
+
294
+ let activeProvider: EmbeddingProvider = new OnnxEmbeddingProvider();
295
+
296
+ export function getEmbeddingProvider(): EmbeddingProvider {
297
+ return activeProvider;
298
+ }
299
+
300
+ export function setEmbeddingProvider(provider: EmbeddingProvider): void {
301
+ activeProvider = provider;
302
+ }
303
+
304
+ export async function embed(texts: string[]): Promise<Float32Array[]> {
305
+ return activeProvider.embed(texts);
306
+ }
307
+
308
+ export function cosineSimilarity(a: Float32Array, b: Float32Array): number {
309
+ if (a.length !== b.length) return 0;
310
+ let dot = 0;
311
+ for (let i = 0; i < a.length; i++) {
312
+ dot += a[i] * b[i];
313
+ }
314
+ return dot;
315
+ }
316
+
317
+ export function serializeEmbedding(vec: Float32Array): Uint8Array {
318
+ return new Uint8Array(vec.buffer, vec.byteOffset, vec.byteLength);
319
+ }
320
+
321
+ export function deserializeEmbedding(bytes: Uint8Array | Buffer): Float32Array {
322
+ return new Float32Array(
323
+ bytes.buffer,
324
+ bytes.byteOffset,
325
+ bytes.byteLength / 4,
326
+ );
327
+ }
328
+
329
+ /**
330
+ * Harness-injected boilerplate wrappers: big machine-generated preamble
331
+ * records (plugin catalogs, environment contexts) that match every query
332
+ * and carry no human/agent intent. Never embed these.
333
+ */
334
+ const BOILERPLATE_PREFIXES = [
335
+ "<recommended_plugins>",
336
+ "<environment_context>",
337
+ "<user_info>",
338
+ "<system_warning>",
339
+ ];
340
+
341
+ export function isBoilerplateRecord(content: string | null | undefined): boolean {
342
+ if (!content) return false;
343
+ const t = content.trimStart();
344
+ return BOILERPLATE_PREFIXES.some((p) => t.startsWith(p));
345
+ }
346
+
347
+ export function getRecordEmbeddingText(r: {
348
+ record_type: string;
349
+ content?: string | null;
350
+ tool_name?: string | null;
351
+ tool_arguments_json?: string | null;
352
+ tool_result_json?: string | null;
353
+ }): string | null {
354
+ const parts: string[] = [];
355
+
356
+ if (r.content && r.content.trim() && !isBoilerplateRecord(r.content)) {
357
+ parts.push(r.content.trim());
358
+ }
359
+
360
+ if (r.tool_name) {
361
+ let toolStr = `Tool: ${r.tool_name}`;
362
+ if (r.tool_arguments_json) {
363
+ const truncatedArgs =
364
+ r.tool_arguments_json.length > 500
365
+ ? r.tool_arguments_json.slice(0, 500) + "..."
366
+ : r.tool_arguments_json;
367
+ toolStr += ` (${truncatedArgs})`;
368
+ }
369
+ parts.push(toolStr);
370
+ }
371
+
372
+ if (r.record_type === "tool" && r.tool_result_json && !r.content) {
373
+ const truncatedRes =
374
+ r.tool_result_json.length > 500
375
+ ? r.tool_result_json.slice(0, 500) + "..."
376
+ : r.tool_result_json;
377
+ parts.push(`Result: ${truncatedRes}`);
378
+ }
379
+
380
+ const text = parts.join("\n").trim();
381
+ return text.length > 0 ? text : null;
382
+ }