pi-mega-compact 0.6.2 → 0.6.3

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/README.md CHANGED
@@ -6,28 +6,39 @@ sessions into a **local SQLite store** and offers **deduped inline recall** —
6
6
  running **locally inside the extension**, with **no remote MCP server** and
7
7
  **zero network calls at runtime** (PREVENT-PI-004).
8
8
 
9
- > **Current version:** `v0.6.1` — storage backend is **`node:sqlite`**
9
+ > **Current version:** `v0.6.2` — storage backend is **`node:sqlite`**
10
10
  > (`DatabaseSync`, a Node ≥22.13 built-in), replacing the old `better-sqlite3`
11
11
  > native addon and the per-session gzipped JSON checkpoint files. **Zero native
12
12
  > build step, fully local, zero network at runtime.** Legacy
13
13
  > `.checkpoints.json.gz` snapshots are retained as disaster-recovery fallbacks
14
- > and auto-imported on first run. Cross-repo recall, durable memory, and a
15
- > localhost dashboard round out the continuity story.
14
+ > and auto-imported on first run. The S24 line ties auto-compact, the tier
15
+ > label, trim depth, and durable-memory review to one **unified pressure
16
+ > signal**, adds a **cross-repo memory-RAG index**, and relieves context
17
+ > **during team runs** (not just at the end).
16
18
 
17
19
  ---
18
20
 
19
21
  ## What this is (the 30-second version)
20
22
 
21
- pi's context window is finite. When a session gets long, pi-mega-compact:
22
-
23
- 1. **Watches** context usage and, past a threshold, **compacts** the older part of
24
- the conversation into a short structured summary + key facts ("a checkpoint").
25
- 2. **Stores** each checkpoint in a **local vector database** (SQLite) with an
26
- embedding, so similar regions can be found later and so **duplicate
27
- work is never stored twice**.
28
- 3. **Recalls** the right checkpoints automatically when you resume a session or
29
- invoke a recall command, re-injecting only what's relevant (deduped against
30
- what's already in view).
23
+ pi's context window is finite. When a session gets long — especially a team run
24
+ with sub-agents — pi-mega-compact keeps it going without overflowing:
25
+
26
+ 1. **Watches one signal.** A single live `pressure = currentTokens / thresholdTokens`
27
+ drives everything the tier label, how aggressively the live trim drops
28
+ context, and how often durable memory is reviewed. As context fills, the whole
29
+ system reacts together; as it's relieved, it backs off.
30
+ 2. **Compacts in two layers.** On every LLM call it returns a **live, compacted
31
+ view** (the model sees a summary + recent anchor, non-destructively). And it
32
+ persists a durable **checkpoint** — and, at each agent settle during a team
33
+ run, fires pi's **native durable trim** so the on-disk transcript is actually
34
+ truncated (context relieves mid-run, and resume reloads the trimmed transcript
35
+ instead of a 150k window).
36
+ 3. **Stores** each checkpoint in a **local vector database** (SQLite) with an
37
+ embedding, so similar regions are found later and **duplicate work is never
38
+ stored twice**.
39
+ 4. **Recalls** the right context automatically — same-repo checkpoints on resume,
40
+ plus **cross-repo memory-RAG**: decisions you saved in one repo are inlined as
41
+ context when you start a session in another.
31
42
 
32
43
  Everything lives on **your disk**. No telemetry, no API, no MCP server, no cloud.
33
44
  The only optional network surface is a **user-triggered localhost dashboard** you
@@ -35,11 +46,13 @@ open yourself.
35
46
 
36
47
  ### Why "mega"?
37
48
 
38
- The compaction pipeline is a **Trident** — three deterministic stages that run
39
- over your conversation before anything is persisted. The checkpoint it produces
40
- is small (a summary + key decisions + next steps + files touched), so the same
41
- session that would otherwise overflow its window keeps going on a fraction of the
42
- tokens.
49
+ The compaction pipeline is a **Trident** — three deterministic stages
50
+ (supersede collapse cluster) that run over your conversation before anything
51
+ is persisted. The checkpoint it produces is small (a summary + key decisions +
52
+ next steps + files touched), so the same session that would otherwise overflow
53
+ its window keeps going on a fraction of the tokens. On top of the Trident, a
54
+ single pressure signal orchestrates the live trim, the durable trim, and memory
55
+ review as one coherent system rather than four independent triggers.
43
56
 
44
57
  ---
45
58
 
@@ -52,9 +65,12 @@ Layer 3 Cluster (vectorize) local vector index → semantic dedup + recall
52
65
  Layer 2 Collapse (summarize) summarizeMessages() heuristic + agent summary on /mega-compact
53
66
  Layer 1 Supersede (prune) drop obsolete file-reads / superseded turns (zero cost)
54
67
  ─────────────────────────────────────────────────────────────────────────
55
- Trigger context/turn_end% gate → auto_compact_checkfire
68
+ Trigger context → token fast-gate → autoCompactChecklive trim (per call)
69
+ Durable agent_end (idle + over threshold) → ctx.compact() → session_before_compact
70
+ supplies the summary; pi truncates the transcript (relieves context)
71
+ Live context handler returns { messages:[summary, …recent] } — model sees a
72
+ compacted window every LLM call; the on-disk transcript is untouched
56
73
  Marker insert compact-marker; dedupe so repeated triggers cost ~0 tokens
57
- Cancel session_before_compact → { cancel:true } once persisted (no double-compact)
58
74
  ```
59
75
 
60
76
  **One store, three ways to read it back — one dedup engine:**
@@ -168,7 +184,7 @@ building.
168
184
  pi-mega-compact uses a dual local backend — **zero network, no native build step**:
169
185
 
170
186
  - **`node:sqlite`** (`DatabaseSync`, Node ≥22.13 built-in) — the synchronous source of truth for checkpoints, session state, and the dedup index. No dependency, no install script, survives pi's `install-scripts` block.
171
- - **PGlite + `@electric-sql/pglite-pgvector`** (WASM Postgres + HNSW `vector_cosine_ops`) — an optional, best-effort async vector index for **cross-repo recall** at `~/.pi/mega-compact-vector`. The sync store stays authoritative; the index degrades to the sync per-session scan on any failure.
187
+ - **PGlite + `@electric-sql/pglite-pgvector`** (WASM Postgres + HNSW `vector_cosine_ops`) — an optional, best-effort async vector index for **cross-repo recall** at `~/.pi/mega-compact-vector`. It holds both checkpoint embeddings and durable-memory embeddings, so decisions saved in one repo are findable from another. The sync store stays authoritative; the index degrades to the sync per-session scan on any failure.
172
188
 
173
189
  Kill-switch: `MEGACOMPACT_PGLITE_DISABLED=1` fully disables the PGlite index (falls back to sync scan). Requires Node ≥22.13 (`engines.node`).
174
190
 
@@ -178,7 +194,7 @@ On resume, recall augments from other repos' checkpoints when this repo's store
178
194
 
179
195
  ### Memory
180
196
 
181
- pi-mega-compact auto-reviews the conversation every 10 turns and writes durable `decision`/`fact`/`preference` memories to SQLite (local, hallucination-guarded). Relevant memories are injected as RAG context on recall (capped, deduped). Manual: `/mega-memory save|list|forget`.
197
+ pi-mega-compact auto-reviews the conversation every 10 turns (the cadence shortens as pressure climbs) and writes durable `decision`/`fact`/`preference` memories to SQLite (local, hallucination-guarded). Relevant memories are injected as RAG context on recall (capped, deduped). **Cross-repo memory-RAG (S24):** every memory write is mirrored into the PGlite/HNSW index, so when same-repo recall is thin the system augments with the nearest memories from *other* repos (stricter `MEGACOMPACT_CROSSREPO_COSINE` floor, deduped against what's already in view). Manual: `/mega-memory save|list|forget` (or `/m`).
182
198
 
183
199
  ### Uninstall
184
200
 
@@ -352,7 +368,7 @@ The extension entry adapts between the engine and pi's runtime types.
352
368
 
353
369
  ```bash
354
370
  npm run build # tsc
355
- npm test # build + node --test on dist/**/*.test.js (346 tests)
371
+ npm test # build + node --test on dist/**/*.test.js (353 tests)
356
372
  npm run lint # tsc --noEmit + guardrails-scan
357
373
  npm run guardrails # regression_check + guardrails-scan
358
374
  ```
@@ -0,0 +1,115 @@
1
+ /**
2
+ * conflict-scan.test.ts — unit tests for the extension-conflict scanner.
3
+ *
4
+ * Fixture trees are written under a temp dir and scanned via
5
+ * MEGACOMPACT_EXT_SCAN_DIR (which makes collectScanRoots() return that
6
+ * single root). This covers the S24 follow-up fix:
7
+ *
8
+ * 1. node_modules-style code extensions (package.json + pi.extensions) are
9
+ * still detected by source-marker grep (regression).
10
+ * 2. USER-LEVEL extensions installed outside npm (e.g. pi-hermes-memory)
11
+ * now get scanned too — previously only `node_modules` was walked, so a
12
+ * data-only memory store (MEMORY.md + sessions.db, no package.json)
13
+ * was never flagged (the 5000-char file-buffer error slipped through).
14
+ * 3. The data-only memory-store signature is detected even with no source.
15
+ * 4. pi-mega-compact (selfName) is always skipped.
16
+ */
17
+ import { test, after } from "node:test";
18
+ import assert from "node:assert/strict";
19
+ import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
20
+ import { tmpdir } from "node:os";
21
+ import { join } from "node:path";
22
+ import { detectConflicts, collectScanRoots } from "./conflict-scan.js";
23
+ const base = mkdtempSync(join(tmpdir(), "mc-scan-"));
24
+ let n = 0;
25
+ /** Make a fixture root containing one or more fake extensions, return its path. */
26
+ function fixture(build) {
27
+ const root = join(base, `case-${n++}`);
28
+ mkdirSync(root, { recursive: true });
29
+ build(root);
30
+ return root;
31
+ }
32
+ after(() => {
33
+ rmSync(base, { recursive: true, force: true });
34
+ });
35
+ test("scans a user-level, data-only memory store (no package.json)", () => {
36
+ const root = fixture((r) => {
37
+ const ext = join(r, "pi-hermes-memory");
38
+ mkdirSync(ext, { recursive: true });
39
+ // No package.json, no source — just pi's memory-store signature.
40
+ writeFileSync(join(ext, "MEMORY.md"), "# memory\n");
41
+ writeFileSync(join(ext, "sessions.db"), "");
42
+ });
43
+ process.env.MEGACOMPACT_EXT_SCAN_DIR = root;
44
+ try {
45
+ const { conflicts } = detectConflicts();
46
+ assert.ok(conflicts.length >= 1, "expected a memory conflict");
47
+ const hit = conflicts.find((c) => c.kind === "memory");
48
+ assert.ok(hit, "expected a memory-kind conflict");
49
+ assert.equal(hit.severity, "high");
50
+ assert.ok(hit.evidence.includes("MEMORY.md") ||
51
+ hit.evidence.includes("sessions.db"), "evidence should name the on-disk memory signature");
52
+ }
53
+ finally {
54
+ delete process.env.MEGACOMPACT_EXT_SCAN_DIR;
55
+ }
56
+ });
57
+ test("still detects a code extension by source marker (regression)", () => {
58
+ const root = fixture((r) => {
59
+ // A code extension is a DIRECT child of the scan root (mirrors the
60
+ // node_modules layout: packages live one level under the root).
61
+ const ext = join(r, "some-memory-ext");
62
+ mkdirSync(ext, { recursive: true });
63
+ writeFileSync(join(ext, "package.json"), JSON.stringify({ name: "some-memory-ext", pi: { extensions: ["x.ts"] } }));
64
+ writeFileSync(join(ext, "index.ts"), "export const MEMORY_TOOL = true;");
65
+ });
66
+ process.env.MEGACOMPACT_EXT_SCAN_DIR = root;
67
+ try {
68
+ const { conflicts } = detectConflicts();
69
+ const hit = conflicts.find((c) => c.package === "some-memory-ext");
70
+ assert.ok(hit, "expected some-memory-ext to be flagged");
71
+ assert.equal(hit.kind, "memory");
72
+ }
73
+ finally {
74
+ delete process.env.MEGACOMPACT_EXT_SCAN_DIR;
75
+ }
76
+ });
77
+ test("skips pi-mega-compact (selfName) and non-extension dirs", () => {
78
+ const root = fixture((r) => {
79
+ // selfName dir with a memory signature — must be ignored.
80
+ const me = join(r, "node_modules", "pi-mega-compact");
81
+ mkdirSync(me, { recursive: true });
82
+ writeFileSync(join(me, "sessions.db"), "");
83
+ // unrelated dir with no pi.extensions and no memory signature.
84
+ mkdirSync(join(r, "node_modules", "totally-fine"), { recursive: true });
85
+ });
86
+ process.env.MEGACOMPACT_EXT_SCAN_DIR = root;
87
+ try {
88
+ const { scanned, conflicts } = detectConflicts();
89
+ assert.equal(conflicts.length, 0, "no conflicts expected");
90
+ assert.ok(!scanned.some((s) => s.includes("pi-mega-compact")), "selfName should not appear in scanned");
91
+ }
92
+ finally {
93
+ delete process.env.MEGACOMPACT_EXT_SCAN_DIR;
94
+ }
95
+ });
96
+ test("collectScanRoots honors MEGACOMPACT_EXT_SCAN_DIR override", () => {
97
+ const root = fixture(() => { });
98
+ process.env.MEGACOMPACT_EXT_SCAN_DIR = root;
99
+ try {
100
+ const roots = collectScanRoots();
101
+ assert.deepEqual(roots, [root], "override replaces the whole root list");
102
+ }
103
+ finally {
104
+ delete process.env.MEGACOMPACT_EXT_SCAN_DIR;
105
+ }
106
+ });
107
+ test("collectScanRoots falls back to node_modules + user dir when no override", () => {
108
+ delete process.env.MEGACOMPACT_EXT_SCAN_DIR;
109
+ delete process.env.MEGACOMPACT_EXT_USER_DIR;
110
+ // No override set and this test file lives under extensions/, so node_modules
111
+ // resolution walks up from here; the user dir (~/.pi/agent) may or may
112
+ // not exist in CI. We only assert the call returns a non-throwing array.
113
+ const roots = collectScanRoots();
114
+ assert.ok(Array.isArray(roots), "collectScanRoots must return an array");
115
+ });
@@ -22,17 +22,15 @@
22
22
  import { homedir } from "node:os";
23
23
  import { join } from "node:path";
24
24
  import { mkdirSync, rmSync, existsSync } from "node:fs";
25
- // PGlite + pgvector are script-free WASM (no native build) → survive pi's
26
- // install-script block. Imported lazily so a missing/broken package degrades
27
- // gracefully instead of crashing module load.
28
- import { PGlite } from "@electric-sql/pglite";
29
- import { vector } from "@electric-sql/pglite-pgvector";
30
25
  /** Vector dimension produced by the default TrigramEmbedder (src/embedder.ts). */
31
26
  export const MEMORY_INDEX_DIM = 512;
32
27
  let db;
33
28
  let initPromise;
34
29
  let disabled = false;
35
30
  let warned = false;
31
+ /** Lazily-loaded PGlite module + pgvector extension (see loadPgLite). */
32
+ let pgliteMod;
33
+ let pgliteLoadFailed = false;
36
34
  function indexDir() {
37
35
  const override = process.env.MEGACOMPACT_INDEX_DIR;
38
36
  if (override && override.trim() !== "")
@@ -77,17 +75,44 @@ export function initMemoryIndex() {
77
75
  initPromise = openPgLite(/* retryOnCorrupt */ true);
78
76
  return initPromise;
79
77
  }
78
+ /**
79
+ * Lazily load the PGlite module + pgvector extension via dynamic import. Caches
80
+ * success and permanent failure. Returns undefined (once, then forever) when the
81
+ * package is missing/broken so callers fall back to the same-repo scan. Never throws.
82
+ */
83
+ async function loadPgLite() {
84
+ if (pgliteMod)
85
+ return pgliteMod;
86
+ if (pgliteLoadFailed)
87
+ return undefined;
88
+ try {
89
+ const [pglitePkg, pgvectorPkg] = await Promise.all([
90
+ import("@electric-sql/pglite"),
91
+ import("@electric-sql/pglite-pgvector"),
92
+ ]);
93
+ pgliteMod = { PGlite: pglitePkg.PGlite, vector: pgvectorPkg.vector };
94
+ return pgliteMod;
95
+ }
96
+ catch (err) {
97
+ pgliteLoadFailed = true;
98
+ logWarn(`package unavailable: ${err instanceof Error ? err.message : String(err)}`);
99
+ return undefined;
100
+ }
101
+ }
80
102
  /**
81
103
  * Open + schema-init PGlite. When `retryOnCorrupt` is true, a WASM-level abort
82
104
  * (typically from a corrupted/torn data dir) triggers a delete + one retry.
83
105
  */
84
106
  async function openPgLite(retryOnCorrupt) {
85
107
  try {
108
+ const mod = await loadPgLite();
109
+ if (!mod)
110
+ return undefined;
86
111
  const dir = indexDir();
87
112
  mkdirSync(dir, { recursive: true });
88
- const pg = await new PGlite({
113
+ const pg = await new mod.PGlite({
89
114
  dataDir: dir,
90
- extensions: { vector },
115
+ extensions: { vector: mod.vector },
91
116
  });
92
117
  await pg.exec("CREATE EXTENSION IF NOT EXISTS vector;");
93
118
  await pg.exec(`
@@ -18,17 +18,15 @@
18
18
  import { homedir } from "node:os";
19
19
  import { join } from "node:path";
20
20
  import { mkdirSync, rmSync, existsSync } from "node:fs";
21
- // PGlite + pgvector are script-free WASM (no native build) → survive pi's
22
- // install-script block. Imported lazily so a missing/broken package degrades
23
- // gracefully instead of crashing module load.
24
- import { PGlite } from "@electric-sql/pglite";
25
- import { vector } from "@electric-sql/pglite-pgvector";
26
21
  /** Vector dimension produced by the default TrigramEmbedder (src/embedder.ts). */
27
22
  export const EMBEDDING_DIM = 512;
28
23
  let db;
29
24
  let initPromise;
30
25
  let disabled = false;
31
26
  let warned = false;
27
+ /** Lazily-loaded PGlite module + pgvector extension (see loadPgLite). */
28
+ let pgliteMod;
29
+ let pgliteLoadFailed = false;
32
30
  function indexDir() {
33
31
  const override = process.env.MEGACOMPACT_VECTOR_INDEX_DIR;
34
32
  if (override && override.trim() !== "")
@@ -73,6 +71,30 @@ export function initVectorIndex() {
73
71
  initPromise = openPgLite(/* retryOnCorrupt */ true);
74
72
  return initPromise;
75
73
  }
74
+ /**
75
+ * Lazily load the PGlite module + pgvector extension via dynamic import. Caches
76
+ * success and permanent failure. Returns undefined (once, then forever) when the
77
+ * package is missing/broken so callers fall back to the sync scan. Never throws.
78
+ */
79
+ async function loadPgLite() {
80
+ if (pgliteMod)
81
+ return pgliteMod;
82
+ if (pgliteLoadFailed)
83
+ return undefined;
84
+ try {
85
+ const [pglitePkg, pgvectorPkg] = await Promise.all([
86
+ import("@electric-sql/pglite"),
87
+ import("@electric-sql/pglite-pgvector"),
88
+ ]);
89
+ pgliteMod = { PGlite: pglitePkg.PGlite, vector: pgvectorPkg.vector };
90
+ return pgliteMod;
91
+ }
92
+ catch (err) {
93
+ pgliteLoadFailed = true;
94
+ logWarn(`package unavailable: ${err instanceof Error ? err.message : String(err)}`);
95
+ return undefined;
96
+ }
97
+ }
76
98
  /**
77
99
  * Open + schema-init PGlite. When `retryOnCorrupt` is true, a WASM-level
78
100
  * abort (typically from a corrupted/torn data dir) triggers a delete + one
@@ -80,11 +102,14 @@ export function initVectorIndex() {
80
102
  */
81
103
  async function openPgLite(retryOnCorrupt) {
82
104
  try {
105
+ const mod = await loadPgLite();
106
+ if (!mod)
107
+ return undefined;
83
108
  const dir = indexDir();
84
109
  mkdirSync(dir, { recursive: true });
85
- const pg = await new PGlite({
110
+ const pg = await new mod.PGlite({
86
111
  dataDir: dir,
87
- extensions: { vector },
112
+ extensions: { vector: mod.vector },
88
113
  });
89
114
  await pg.exec("CREATE EXTENSION IF NOT EXISTS vector;");
90
115
  await pg.exec(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
5
5
  "type": "module",
6
6
  "license": "BSD-2-Clause",
@@ -25,10 +25,12 @@ import { join } from "node:path";
25
25
  import { mkdirSync, rmSync, existsSync } from "node:fs";
26
26
 
27
27
  // PGlite + pgvector are script-free WASM (no native build) → survive pi's
28
- // install-script block. Imported lazily so a missing/broken package degrades
29
- // gracefully instead of crashing module load.
30
- import { PGlite, type PGlite as PGliteInstance } from "@electric-sql/pglite";
31
- import { vector } from "@electric-sql/pglite-pgvector";
28
+ // install-script block. The VALUE import is LAZY (dynamic import inside
29
+ // openPgLite) so a missing/broken package degrades to the same-repo scan instead
30
+ // of crashing module load. A static top-level `import { PGlite }` would throw
31
+ // "Cannot find module" at pi startup and take down the whole extension. The
32
+ // `import type` below is erased at compile time and emits NO runtime load.
33
+ import type { PGlite as PGliteInstance, Extension } from "@electric-sql/pglite";
32
34
 
33
35
  /** Vector dimension produced by the default TrigramEmbedder (src/embedder.ts). */
34
36
  export const MEMORY_INDEX_DIM = 512;
@@ -47,6 +49,12 @@ let db: PGliteInstance | undefined;
47
49
  let initPromise: Promise<PGliteInstance | undefined> | undefined;
48
50
  let disabled = false;
49
51
  let warned = false;
52
+ /** Lazily-loaded PGlite module + pgvector extension (see loadPgLite). */
53
+ let pgliteMod: {
54
+ PGlite: typeof import("@electric-sql/pglite")["PGlite"];
55
+ vector: Extension;
56
+ } | undefined;
57
+ let pgliteLoadFailed = false;
50
58
 
51
59
  function indexDir(): string {
52
60
  const override = process.env.MEGACOMPACT_INDEX_DIR;
@@ -91,6 +99,30 @@ export function initMemoryIndex(): Promise<PGliteInstance | undefined> {
91
99
  return initPromise;
92
100
  }
93
101
 
102
+ /**
103
+ * Lazily load the PGlite module + pgvector extension via dynamic import. Caches
104
+ * success and permanent failure. Returns undefined (once, then forever) when the
105
+ * package is missing/broken so callers fall back to the same-repo scan. Never throws.
106
+ */
107
+ async function loadPgLite(): Promise<
108
+ { PGlite: typeof import("@electric-sql/pglite")["PGlite"]; vector: Extension } | undefined
109
+ > {
110
+ if (pgliteMod) return pgliteMod;
111
+ if (pgliteLoadFailed) return undefined;
112
+ try {
113
+ const [pglitePkg, pgvectorPkg] = await Promise.all([
114
+ import("@electric-sql/pglite"),
115
+ import("@electric-sql/pglite-pgvector"),
116
+ ]);
117
+ pgliteMod = { PGlite: pglitePkg.PGlite, vector: pgvectorPkg.vector };
118
+ return pgliteMod;
119
+ } catch (err) {
120
+ pgliteLoadFailed = true;
121
+ logWarn(`package unavailable: ${err instanceof Error ? err.message : String(err)}`);
122
+ return undefined;
123
+ }
124
+ }
125
+
94
126
  /**
95
127
  * Open + schema-init PGlite. When `retryOnCorrupt` is true, a WASM-level abort
96
128
  * (typically from a corrupted/torn data dir) triggers a delete + one retry.
@@ -99,11 +131,13 @@ async function openPgLite(
99
131
  retryOnCorrupt: boolean,
100
132
  ): Promise<PGliteInstance | undefined> {
101
133
  try {
134
+ const mod = await loadPgLite();
135
+ if (!mod) return undefined;
102
136
  const dir = indexDir();
103
137
  mkdirSync(dir, { recursive: true });
104
- const pg = await new PGlite({
138
+ const pg = await new mod.PGlite({
105
139
  dataDir: dir,
106
- extensions: { vector },
140
+ extensions: { vector: mod.vector },
107
141
  });
108
142
  await pg.exec("CREATE EXTENSION IF NOT EXISTS vector;");
109
143
  await pg.exec(`
@@ -21,10 +21,12 @@ import { join } from "node:path";
21
21
  import { mkdirSync, rmSync, existsSync } from "node:fs";
22
22
 
23
23
  // PGlite + pgvector are script-free WASM (no native build) → survive pi's
24
- // install-script block. Imported lazily so a missing/broken package degrades
25
- // gracefully instead of crashing module load.
26
- import { PGlite, type PGlite as PGliteInstance } from "@electric-sql/pglite";
27
- import { vector } from "@electric-sql/pglite-pgvector";
24
+ // install-script block. The VALUE import is LAZY (dynamic import inside
25
+ // openPgLite) so a missing/broken package degrades to the sync scan instead of
26
+ // crashing module load. A static top-level `import { PGlite }` would throw
27
+ // "Cannot find module" at pi startup and take down the whole extension. The
28
+ // `import type` below is erased at compile time and emits NO runtime load.
29
+ import type { PGlite as PGliteInstance, Extension } from "@electric-sql/pglite";
28
30
 
29
31
  /** Vector dimension produced by the default TrigramEmbedder (src/embedder.ts). */
30
32
  export const EMBEDDING_DIM = 512;
@@ -42,6 +44,12 @@ let db: PGliteInstance | undefined;
42
44
  let initPromise: Promise<PGliteInstance | undefined> | undefined;
43
45
  let disabled = false;
44
46
  let warned = false;
47
+ /** Lazily-loaded PGlite module + pgvector extension (see loadPgLite). */
48
+ let pgliteMod: {
49
+ PGlite: typeof import("@electric-sql/pglite")["PGlite"];
50
+ vector: Extension;
51
+ } | undefined;
52
+ let pgliteLoadFailed = false;
45
53
 
46
54
  function indexDir(): string {
47
55
  const override = process.env.MEGACOMPACT_VECTOR_INDEX_DIR;
@@ -86,6 +94,30 @@ export function initVectorIndex(): Promise<PGliteInstance | undefined> {
86
94
  return initPromise;
87
95
  }
88
96
 
97
+ /**
98
+ * Lazily load the PGlite module + pgvector extension via dynamic import. Caches
99
+ * success and permanent failure. Returns undefined (once, then forever) when the
100
+ * package is missing/broken so callers fall back to the sync scan. Never throws.
101
+ */
102
+ async function loadPgLite(): Promise<
103
+ { PGlite: typeof import("@electric-sql/pglite")["PGlite"]; vector: Extension } | undefined
104
+ > {
105
+ if (pgliteMod) return pgliteMod;
106
+ if (pgliteLoadFailed) return undefined;
107
+ try {
108
+ const [pglitePkg, pgvectorPkg] = await Promise.all([
109
+ import("@electric-sql/pglite"),
110
+ import("@electric-sql/pglite-pgvector"),
111
+ ]);
112
+ pgliteMod = { PGlite: pglitePkg.PGlite, vector: pgvectorPkg.vector };
113
+ return pgliteMod;
114
+ } catch (err) {
115
+ pgliteLoadFailed = true;
116
+ logWarn(`package unavailable: ${err instanceof Error ? err.message : String(err)}`);
117
+ return undefined;
118
+ }
119
+ }
120
+
89
121
  /**
90
122
  * Open + schema-init PGlite. When `retryOnCorrupt` is true, a WASM-level
91
123
  * abort (typically from a corrupted/torn data dir) triggers a delete + one
@@ -95,11 +127,13 @@ async function openPgLite(
95
127
  retryOnCorrupt: boolean,
96
128
  ): Promise<PGliteInstance | undefined> {
97
129
  try {
130
+ const mod = await loadPgLite();
131
+ if (!mod) return undefined;
98
132
  const dir = indexDir();
99
133
  mkdirSync(dir, { recursive: true });
100
- const pg = await new PGlite({
134
+ const pg = await new mod.PGlite({
101
135
  dataDir: dir,
102
- extensions: { vector },
136
+ extensions: { vector: mod.vector },
103
137
  });
104
138
  await pg.exec("CREATE EXTENSION IF NOT EXISTS vector;");
105
139
  await pg.exec(`
@@ -1,291 +0,0 @@
1
- /**
2
- * openclaw-mega-compact — OpenClaw plugin adapter for the pi-mega-compact engine.
3
- *
4
- * Wires the pi-agnostic Trident engine (src/) into OpenClaw's plugin lifecycle:
5
- * - Registers a CompactionProvider that replaces the built-in summarizeInStages.
6
- * - Exposes `mega_status` and `mega_recall` tools for on-demand inspection.
7
- * - Hooks into `before_compaction` / `after_compaction` for diagnostics.
8
- *
9
- * Design constraints:
10
- * - NO imports from `@earendil-works/pi-coding-agent` or pi-agent-core.
11
- * - The engine core (src/) is pi-agnostic; this file is the sole OpenClaw boundary.
12
- * - No network at runtime — everything is local (stores + extractive summarizer).
13
- */
14
- import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
15
- import { compactSession, setDefaultStore, } from "../src/engine.js";
16
- import { recallAndInline } from "../src/recall.js";
17
- import { VectorStore } from "../src/vectorStore.js";
18
- // ---------------------------------------------------------------------------
19
- // Constants
20
- // ---------------------------------------------------------------------------
21
- const PLUGIN_ID = "mega-compact";
22
- const PLUGIN_LABEL = "Mega Compact (Trident)";
23
- /** Default state directory for vector store persistence. */
24
- const STATE_DIR = process.env.MEGA_COMPACT_STATE_DIR ?? undefined;
25
- /** Minimum messages before we bother compacting. */
26
- const MIN_MESSAGES_FOR_COMPACT = 6;
27
- // ---------------------------------------------------------------------------
28
- // Message conversion — OpenClaw unknown[] → EngineMessage[]
29
- // ---------------------------------------------------------------------------
30
- /**
31
- * Best-effort conversion from OpenClaw's opaque message array to our
32
- * EngineMessage shape. OpenClaw messages are typed as `unknown[]` so we
33
- * handle whatever shape comes through gracefully.
34
- */
35
- function toEngineMessages(messages) {
36
- return messages.map((msg) => {
37
- if (!msg || typeof msg !== "object") {
38
- // Primitive fallback — treat as custom text.
39
- return {
40
- role: "custom",
41
- text: String(msg ?? ""),
42
- };
43
- }
44
- const m = msg;
45
- const role = typeof m.role === "string" ? m.role : "custom";
46
- // Normalize role to one of our four engine roles.
47
- let engineRole;
48
- switch (role) {
49
- case "user":
50
- engineRole = "user";
51
- break;
52
- case "assistant":
53
- engineRole = "assistant";
54
- break;
55
- case "tool":
56
- case "function":
57
- engineRole = "tool";
58
- break;
59
- default:
60
- engineRole = "custom";
61
- break;
62
- }
63
- // Extract text content from common message shapes.
64
- const text = typeof m.content === "string"
65
- ? m.content
66
- : typeof m.text === "string"
67
- ? m.text
68
- : Array.isArray(m.content)
69
- ? m.content
70
- .filter((part) => part.type === "text" && typeof part.text === "string")
71
- .map((part) => part.text)
72
- .join("\n")
73
- : "";
74
- // Preserve tool metadata when present.
75
- const toolName = typeof m.name === "string"
76
- ? m.name
77
- : typeof m.toolName === "string"
78
- ? m.toolName
79
- : undefined;
80
- const input = typeof m.input === "string"
81
- ? m.input
82
- : typeof m.arguments === "string"
83
- ? m.arguments
84
- : m.arguments !== undefined
85
- ? JSON.stringify(m.arguments)
86
- : undefined;
87
- const output = typeof m.output === "string"
88
- ? m.output
89
- : engineRole === "tool" && typeof m.content === "string"
90
- ? m.content
91
- : undefined;
92
- return { role: engineRole, text, toolName, input, output };
93
- });
94
- }
95
- // ---------------------------------------------------------------------------
96
- // Compaction provider
97
- // ---------------------------------------------------------------------------
98
- function createCompactionProvider(store) {
99
- return {
100
- id: PLUGIN_ID,
101
- label: PLUGIN_LABEL,
102
- async summarize({ messages, signal, compressionRatio, }) {
103
- // Abort check — bail early if the caller cancelled.
104
- if (signal?.aborted) {
105
- throw new DOMException("Aborted", "AbortError");
106
- }
107
- const engineMessages = toEngineMessages(messages);
108
- // Nothing meaningful to compact.
109
- if (engineMessages.length < MIN_MESSAGES_FOR_COMPACT) {
110
- return "";
111
- }
112
- // Map compression ratio → keepFrom boundary.
113
- // compressionRatio=0.5 means "compact the oldest 50%".
114
- // Default to compacting the oldest half if not specified.
115
- const ratio = compressionRatio ?? 0.5;
116
- const keepFrom = Math.max(MIN_MESSAGES_FOR_COMPACT, Math.floor(engineMessages.length * (1 - ratio)));
117
- // Abort check after conversion (conversion is cheap but check anyway).
118
- if (signal?.aborted) {
119
- throw new DOMException("Aborted", "AbortError");
120
- }
121
- const sessionId = `openclaw-${Date.now()}`;
122
- const input = {
123
- sessionId,
124
- messages: engineMessages,
125
- keepFrom,
126
- };
127
- const result = compactSession(input, store);
128
- if (result.skipped) {
129
- return "";
130
- }
131
- return result.summary;
132
- },
133
- };
134
- }
135
- // ---------------------------------------------------------------------------
136
- // Plugin entry
137
- // ---------------------------------------------------------------------------
138
- export default definePluginEntry({
139
- id: PLUGIN_ID,
140
- name: "Mega Compact",
141
- description: "Layered, local, vector-backed context compressor (Trident engine) for OpenClaw compaction.",
142
- register(api) {
143
- const logger = api.logger;
144
- // Resolve state directory — prefer plugin config override.
145
- const pluginCfg = (api.pluginConfig ?? {});
146
- const stateDir = typeof pluginCfg.stateDir === "string" && pluginCfg.stateDir.length > 0
147
- ? pluginCfg.stateDir
148
- : STATE_DIR;
149
- // Initialize vector store.
150
- let store;
151
- try {
152
- store = new VectorStore({ stateDir });
153
- setDefaultStore(store);
154
- logger.info?.(`${PLUGIN_ID}: vector store initialized (stateDir=${stateDir ?? "default"})`);
155
- }
156
- catch (err) {
157
- logger.error?.(`${PLUGIN_ID}: failed to init vector store:`, err);
158
- return; // Hard bail — no point registering if store is broken.
159
- }
160
- // -----------------------------------------------------------------------
161
- // Register compaction provider
162
- // -----------------------------------------------------------------------
163
- const provider = createCompactionProvider(store);
164
- api.registerCompactionProvider(provider);
165
- logger.info?.(`${PLUGIN_ID}: registered compaction provider "${provider.id}"`);
166
- // -----------------------------------------------------------------------
167
- // Hooks — before / after compaction diagnostics
168
- // -----------------------------------------------------------------------
169
- api.registerHook({
170
- event: "before_compaction",
171
- handler: async (ctx) => {
172
- const msgCount = Array.isArray(ctx?.messages) ? ctx.messages.length : 0;
173
- logger.info?.(`${PLUGIN_ID}: before_compaction — ${msgCount} messages in scope`);
174
- },
175
- });
176
- api.registerHook({
177
- event: "after_compaction",
178
- handler: async (ctx) => {
179
- const summaryLen = typeof ctx?.summary === "string" ? ctx.summary.length : 0;
180
- logger.info?.(`${PLUGIN_ID}: after_compaction — summary ${summaryLen} chars`);
181
- },
182
- });
183
- // -----------------------------------------------------------------------
184
- // Tool: mega_status
185
- // -----------------------------------------------------------------------
186
- api.registerTool({
187
- name: "mega_status",
188
- description: "Show the current status of the mega-compact engine: vector store stats, checkpoint count, and recent compaction activity.",
189
- parameters: {
190
- type: "object",
191
- properties: {
192
- sessionId: {
193
- type: "string",
194
- description: "Optional session ID to scope stats to.",
195
- },
196
- },
197
- additionalProperties: false,
198
- },
199
- handler: async (args) => {
200
- const sessionId = args?.sessionId ?? "global";
201
- try {
202
- const stats = store.stats(sessionId);
203
- const parts = [
204
- `**Mega Compact Status**`,
205
- `Session: ${sessionId}`,
206
- `Checkpoints: ${stats.checkpointCount}`,
207
- `Total tokens saved: ${stats.totalTokenEstimate}`,
208
- `Last checkpoint: ${stats.lastCheckpointId ?? "—"}`,
209
- `Injected count: ${stats.injectedCount}`,
210
- `Dedup hit rate: ${(stats.dedupHitRate * 100).toFixed(0)}%`,
211
- ];
212
- if (stats.lastSummary) {
213
- parts.push(`\nLast summary (truncated):\n ${stats.lastSummary.slice(0, 120).replace(/\n/g, " ")}…`);
214
- }
215
- return { content: [{ type: "text", text: parts.join("\n") }] };
216
- }
217
- catch (err) {
218
- return {
219
- content: [{ type: "text", text: `Error reading mega-compact status: ${err}` }],
220
- isError: true,
221
- };
222
- }
223
- },
224
- });
225
- // -----------------------------------------------------------------------
226
- // Tool: mega_recall
227
- // -----------------------------------------------------------------------
228
- api.registerTool({
229
- name: "mega_recall",
230
- description: "Recall and inline relevant context from the mega-compact vector store for the current session.",
231
- parameters: {
232
- type: "object",
233
- properties: {
234
- sessionId: {
235
- type: "string",
236
- description: "Session ID to recall context for.",
237
- },
238
- query: {
239
- type: "string",
240
- description: "Natural language query for relevant context.",
241
- },
242
- limit: {
243
- type: "number",
244
- description: "Max checkpoints to recall (default 3).",
245
- },
246
- },
247
- required: ["sessionId", "query"],
248
- additionalProperties: false,
249
- },
250
- handler: async (args) => {
251
- const { sessionId, query, limit } = args;
252
- if (!sessionId || !query) {
253
- return {
254
- content: [{ type: "text", text: "Both `sessionId` and `query` are required." }],
255
- isError: true,
256
- };
257
- }
258
- try {
259
- const result = recallAndInline({ sessionId, query, limit: limit ?? 3, source: "command", skipInjected: false }, store);
260
- if (result.toInject.length === 0) {
261
- return {
262
- content: [{ type: "text", text: "No relevant context found in the mega-compact store." }],
263
- };
264
- }
265
- const parts = [
266
- `**Recalled ${result.toInject.length} checkpoint(s):**`,
267
- ...result.report,
268
- "",
269
- "---",
270
- result.block,
271
- ];
272
- return { content: [{ type: "text", text: parts.join("\n") }] };
273
- }
274
- catch (err) {
275
- return {
276
- content: [{ type: "text", text: `Error during mega-recall: ${err}` }],
277
- isError: true,
278
- };
279
- }
280
- },
281
- });
282
- // -----------------------------------------------------------------------
283
- // Cleanup on shutdown
284
- // -----------------------------------------------------------------------
285
- api.on("shutdown", () => {
286
- logger.info?.(`${PLUGIN_ID}: shutting down — clearing default store`);
287
- setDefaultStore(undefined);
288
- });
289
- logger.info?.(`${PLUGIN_ID}: plugin registered (tools: mega_status, mega_recall)`);
290
- },
291
- });
@@ -1,92 +0,0 @@
1
- /**
2
- * minilm.ts — local MiniLM (all-MiniLM-L6-v2) sentence embedder (Sprint 12).
3
- *
4
- * Implements the `Embedder` interface so it drops into the existing VectorStore
5
- * dedup cascade and search with no call-site changes. Inference is 100% local:
6
- * the ONNX model + WordPiece vocab are on-disk artifacts fetched once by
7
- * scripts/setup-minilm.mjs. There is NO network call at runtime (PREVENT-PI-004).
8
- *
9
- * Inputs (dynamic): input_ids, attention_mask, token_type_ids (int64).
10
- * Output: last_hidden_state (batch, seq, 384). We mean-pool over non-padded
11
- * tokens (attention_mask == 1) and L2-normalize → 384-dim unit vector.
12
- *
13
- * The ONNX session + tokenizer are loaded LAZILY on first embed() so the default
14
- * TrigramEmbedder path (and its zero native-init cost) is untouched unless
15
- * MEGACOMPACT_EMBEDDER=minilm is selected.
16
- */
17
- import { join } from "node:path";
18
- import { homedir } from "node:os";
19
- import { existsSync } from "node:fs";
20
- import { l2Normalize, awaitSync } from "./embedder.js";
21
- import { WordPieceTokenizer } from "./wordpiece.js";
22
- export const MINILM_DIM = 384;
23
- export const MINILM_MAX_LEN = 256;
24
- /** Resolve the model directory: MEGACOMPACT_MINILM_DIR > ./models/minilm > ~/.pi … */
25
- function resolveModelDir() {
26
- if (process.env.MEGACOMPACT_MINILM_DIR)
27
- return process.env.MEGACOMPACT_MINILM_DIR;
28
- // Repo-local vendored path (gitignored).
29
- const local = join(process.cwd(), "models", "minilm");
30
- if (existsSync(local))
31
- return local;
32
- return join(homedir(), ".pi", "agent", "extensions", "mega-compact", "models", "minilm");
33
- }
34
- export class MiniLMEmbedder {
35
- dim = MINILM_DIM;
36
- session = null;
37
- tokenizer = null;
38
- modelDir;
39
- loadPromise = null;
40
- constructor(modelDir = resolveModelDir()) {
41
- this.modelDir = modelDir;
42
- }
43
- async ensureLoaded() {
44
- if (this.session && this.tokenizer)
45
- return;
46
- if (this.loadPromise)
47
- return this.loadPromise;
48
- this.loadPromise = (async () => {
49
- const ort = await import("onnxruntime-node");
50
- const modelPath = join(this.modelDir, "model_quantized.onnx");
51
- const vocabPath = join(this.modelDir, "vocab.txt");
52
- if (!existsSync(modelPath) || !existsSync(vocabPath)) {
53
- throw new Error(`MiniLM artifacts missing in ${this.modelDir}. Run: node scripts/setup-minilm.mjs`);
54
- }
55
- // 1 thread is plenty for a single short-region embed and bounds CPU.
56
- this.session = await ort.InferenceSession.create(modelPath, {
57
- executionProviders: ["cpu"],
58
- graphOptimizationLevel: "all",
59
- });
60
- this.tokenizer = WordPieceTokenizer.fromVocabFile(vocabPath);
61
- })();
62
- return this.loadPromise;
63
- }
64
- embed(text) {
65
- awaitSync(this.ensureLoaded());
66
- const enc = this.tokenizer.encode(text, MINILM_MAX_LEN);
67
- const n = enc.inputIds.length;
68
- const BigInt64 = (arr) => arr.map((x) => BigInt(x));
69
- const ort = awaitSync(import("onnxruntime-node"));
70
- const tensors = {
71
- input_ids: new ort.Tensor("int64", BigInt64(enc.inputIds), [1, n]),
72
- attention_mask: new ort.Tensor("int64", BigInt64(enc.attentionMask), [1, n]),
73
- token_type_ids: new ort.Tensor("int64", BigInt64(enc.tokenTypeIds), [1, n]),
74
- };
75
- const out = awaitSync(this.session.run(tensors));
76
- const hidden = out.last_hidden_state.data;
77
- // hidden shape: [1, n, 384]. Mean-pool over non-padded positions.
78
- const pooled = new Array(MINILM_DIM).fill(0);
79
- let count = 0;
80
- for (let i = 0; i < n; i++) {
81
- if (enc.attentionMask[i] === 0)
82
- continue;
83
- const base = i * MINILM_DIM;
84
- for (let d = 0; d < MINILM_DIM; d++)
85
- pooled[d] += hidden[base + d];
86
- count++;
87
- }
88
- if (count === 0)
89
- return l2Normalize(new Array(MINILM_DIM).fill(0));
90
- return l2Normalize(pooled.map((x) => x / count));
91
- }
92
- }
@@ -1,129 +0,0 @@
1
- /**
2
- * wordpiece.ts — a self-contained WordPiece tokenizer for BERT/MiniLM.
3
- *
4
- * Loads the canonical `vocab.txt` (bert-base-uncased, ~30K tokens) from disk and
5
- * implements the standard uncased BERT preprocessing + greedy longest-match
6
- * WordPiece segmentation. No native dependency, no network — the vocab file is a
7
- * local artifact fetched once by scripts/setup-minilm.mjs (PREVENT-PI-004).
8
- *
9
- * This mirrors HuggingFace `BertTokenizer` closely enough for sentence-embedding
10
- * use: lowercase, strip accents, split on whitespace + punctuation, then
11
- * WordPiece each token with the `##` continuation convention. Special tokens
12
- * [CLS]/[SEP] are added by the caller's encode().
13
- */
14
- import { readFileSync, existsSync } from "node:fs";
15
- const UNK = "[UNK]";
16
- const CLS = "[CLS]";
17
- const SEP = "[SEP]";
18
- const PAD = "[PAD]";
19
- const MAX_INPUT_CHARS_PER_WORD = 200;
20
- export class WordPieceTokenizer {
21
- vocab;
22
- clsId;
23
- sepId;
24
- padId;
25
- unkId;
26
- constructor(vocab) {
27
- this.vocab = vocab;
28
- this.clsId = vocab.get(CLS) ?? 101;
29
- this.sepId = vocab.get(SEP) ?? 102;
30
- this.padId = vocab.get(PAD) ?? 0;
31
- this.unkId = vocab.get(UNK) ?? 100;
32
- }
33
- /** Build a tokenizer from a vocab.txt file (one token per line, index = line). */
34
- static fromVocabFile(path) {
35
- if (!existsSync(path)) {
36
- throw new Error(`WordPiece vocab not found at ${path}. Run: node scripts/setup-minilm.mjs`);
37
- }
38
- const lines = readFileSync(path, "utf-8").split("\n");
39
- const vocab = new Map();
40
- for (let i = 0; i < lines.length; i++) {
41
- const tok = lines[i].replace(/\r$/, "");
42
- if (tok.length > 0 || i < lines.length - 1)
43
- vocab.set(tok, i);
44
- }
45
- return new WordPieceTokenizer(vocab);
46
- }
47
- /** Uncased BERT basic tokenization: lowercase, strip accents, split on ws+punct. */
48
- basicTokenize(text) {
49
- // NFD + strip combining marks (accent removal), then lowercase.
50
- const cleaned = text
51
- .normalize("NFD")
52
- .replace(/[̀-ͯ]/g, "")
53
- .toLowerCase();
54
- const tokens = [];
55
- let buf = "";
56
- const flush = () => {
57
- if (buf.length > 0) {
58
- tokens.push(buf);
59
- buf = "";
60
- }
61
- };
62
- for (const ch of cleaned) {
63
- if (/\s/.test(ch)) {
64
- flush();
65
- }
66
- else if (/[!-/:-@[-`{-~¡-¿]/.test(ch)) {
67
- // Punctuation becomes its own token.
68
- flush();
69
- tokens.push(ch);
70
- }
71
- else {
72
- buf += ch;
73
- }
74
- }
75
- flush();
76
- return tokens;
77
- }
78
- /** Greedy longest-match WordPiece for a single word. */
79
- wordpiece(word) {
80
- if (word.length > MAX_INPUT_CHARS_PER_WORD)
81
- return [UNK];
82
- const pieces = [];
83
- let start = 0;
84
- while (start < word.length) {
85
- let end = word.length;
86
- let cur = null;
87
- while (start < end) {
88
- let sub = word.slice(start, end);
89
- if (start > 0)
90
- sub = "##" + sub;
91
- if (this.vocab.has(sub)) {
92
- cur = sub;
93
- break;
94
- }
95
- end--;
96
- }
97
- if (cur === null)
98
- return [UNK]; // any unmatchable piece → whole word is UNK
99
- pieces.push(cur);
100
- start = end;
101
- }
102
- return pieces;
103
- }
104
- /** Tokenize text into WordPiece token strings (no special tokens). */
105
- tokenize(text) {
106
- const out = [];
107
- for (const word of this.basicTokenize(text)) {
108
- for (const piece of this.wordpiece(word))
109
- out.push(piece);
110
- }
111
- return out;
112
- }
113
- /**
114
- * Encode text into model inputs with [CLS]…[SEP], truncated to `maxLen`.
115
- * attention_mask is all 1s (no padding for single-sequence inference).
116
- */
117
- encode(text, maxLen = 256) {
118
- const pieces = this.tokenize(text).slice(0, Math.max(0, maxLen - 2));
119
- const inputIds = [this.clsId];
120
- for (const p of pieces)
121
- inputIds.push(this.vocab.get(p) ?? this.unkId);
122
- inputIds.push(this.sepId);
123
- return {
124
- inputIds,
125
- attentionMask: inputIds.map(() => 1),
126
- tokenTypeIds: inputIds.map(() => 0),
127
- };
128
- }
129
- }