@rohirik/openltm-core 2.8.1 → 2.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rohirik/openltm-core",
3
- "version": "2.8.1",
3
+ "version": "2.9.1",
4
4
  "description": "Shared LTM storage engine — path-agnostic SQLite core used by Claude Code, OpenCode, and Pi adapters",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -14,6 +14,7 @@ import {
14
14
  MEMORY_ADDED,
15
15
  } from "../../events/index.js";
16
16
  import { _resetHonkerForTesting } from "../../lib/honker.js";
17
+ import { resetCapabilitiesForTesting } from "../../extensions.js";
17
18
 
18
19
  function seedDb(): Database {
19
20
  const db = new Database(":memory:");
@@ -25,6 +26,9 @@ describe("cross-process sync — flag + dormant contract (no honker)", () => {
25
26
  beforeEach(() => {
26
27
  _resetHonkerForTesting();
27
28
  delete process.env["LTM_HONKER_EXT"];
29
+ // Reset cached caps so a prior test's honker=true (vendored binary is
30
+ // discoverable here) does not leak in — keeps this the dormant contract.
31
+ resetCapabilitiesForTesting();
28
32
  });
29
33
  afterEach(() => {
30
34
  _resetHonkerForTesting();
@@ -7,11 +7,15 @@
7
7
  import { describe, it, expect, beforeEach } from "bun:test";
8
8
  import { notifyLtm, startLtmListener, LTM_CHANNEL } from "../../events/index.js";
9
9
  import { _resetHonkerForTesting } from "../../lib/honker.js";
10
+ import { resetCapabilitiesForTesting } from "../../extensions.js";
10
11
 
11
12
  describe("events — dormant path (no honker)", () => {
12
13
  beforeEach(() => {
13
14
  _resetHonkerForTesting();
14
15
  delete process.env["LTM_HONKER_EXT"];
16
+ // Reset cached caps so a prior test's honker=true (vendored binary is
17
+ // discoverable here) does not leak in — keeps this the dormant contract.
18
+ resetCapabilitiesForTesting();
15
19
  });
16
20
 
17
21
  it("exposes the stable channel name", () => {
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach } from "bun:test";
2
2
  import { Database } from "bun:sqlite";
3
3
  import {
4
4
  locateSystemSqlite,
5
+ locateHonkerExt,
5
6
  ensureCustomSqlite,
6
7
  loadExtensions,
7
8
  getCapabilities,
@@ -32,14 +33,27 @@ describe("extensions — capability probe", () => {
32
33
  expect(typeof caps.honker).toBe("boolean");
33
34
  });
34
35
 
35
- it("honker is false when no LTM_HONKER_EXT binary is configured", () => {
36
- const prev = process.env["LTM_HONKER_EXT"];
37
- delete process.env["LTM_HONKER_EXT"];
36
+ it("honker is false when force-disabled via LTM_DISABLE_HONKER", () => {
37
+ const prev = process.env["LTM_DISABLE_HONKER"];
38
+ process.env["LTM_DISABLE_HONKER"] = "1";
38
39
  ensureCustomSqlite();
39
40
  const db = new Database(":memory:");
40
41
  const caps = loadExtensions(db);
41
42
  expect(caps.honker).toBe(false);
42
- if (prev) process.env["LTM_HONKER_EXT"] = prev;
43
+ if (prev === undefined) delete process.env["LTM_DISABLE_HONKER"];
44
+ else process.env["LTM_DISABLE_HONKER"] = prev;
45
+ });
46
+
47
+ it("honker auto-loads when a vendored libhonker_ext binary is discoverable", () => {
48
+ // Mirrors the vec test: activation needs customSqlite (process-global, only
49
+ // before the first DB open) AND a discoverable binary. Skip when either is
50
+ // unavailable — that is the graceful-degradation path, not a failure.
51
+ if (locateHonkerExt() === null || process.env["LTM_DISABLE_HONKER"]) return;
52
+ ensureCustomSqlite();
53
+ const db = new Database(":memory:");
54
+ const caps = loadExtensions(db);
55
+ if (!caps.customSqlite) return;
56
+ expect(caps.honker).toBe(true);
43
57
  });
44
58
 
45
59
  it("respects LTM_DISABLE_VEC — vec capability is false when force-disabled", () => {
@@ -15,10 +15,15 @@ import {
15
15
  } from "../../queue/index.js";
16
16
  import { startEmbeddingWorker } from "../../queue/worker.js";
17
17
  import { isHonkerAvailable, _resetHonkerForTesting } from "../../lib/honker.js";
18
+ import { resetCapabilitiesForTesting } from "../../extensions.js";
18
19
 
19
20
  describe("queue — dormant path (no honker)", () => {
20
21
  beforeEach(() => {
21
22
  _resetHonkerForTesting();
23
+ // Force the dormant contract: a prior test may have cached caps.honker=true
24
+ // process-wide (the vendored binary is discoverable here). Reset so
25
+ // getCapabilities() returns the all-false default and honker stays dormant.
26
+ resetCapabilitiesForTesting();
22
27
  });
23
28
 
24
29
  it("EMBED_QUEUE has a stable name", () => {
@@ -13,11 +13,15 @@ import {
13
13
  DEFAULT_JANITOR_CRON,
14
14
  } from "../../scheduler/index.js";
15
15
  import { _resetHonkerForTesting } from "../../lib/honker.js";
16
+ import { resetCapabilitiesForTesting } from "../../extensions.js";
16
17
 
17
18
  describe("scheduler — dormant path (no honker)", () => {
18
19
  beforeEach(() => {
19
20
  _resetHonkerForTesting();
20
21
  delete process.env["LTM_HONKER_EXT"];
22
+ // Reset cached caps so a prior test's honker=true (vendored binary is
23
+ // discoverable here) does not leak in — keeps this the dormant contract.
24
+ resetCapabilitiesForTesting();
21
25
  });
22
26
 
23
27
  it("exposes stable names + default cron", () => {
package/src/extensions.ts CHANGED
@@ -13,6 +13,11 @@
13
13
  */
14
14
  import { Database } from "bun:sqlite";
15
15
  import { existsSync } from "fs";
16
+ import { dirname, join } from "path";
17
+ import { fileURLToPath } from "url";
18
+
19
+ /** Directory of this module — used to resolve the vendored Honker binary. */
20
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
16
21
 
17
22
  export interface Capabilities {
18
23
  /** A system extension-enabled SQLite is active for this process. */
@@ -48,9 +53,9 @@ function envDisabled(name: string): boolean {
48
53
  return v === "1" || v === "true";
49
54
  }
50
55
 
51
- /** Locate a system extension-enabled libsqlite3, or null if none found. */
52
- export function locateSystemSqlite(): string | null {
53
- for (const p of systemSqliteCandidates()) {
56
+ /** First candidate path that exists on disk, or null. Never throws. */
57
+ function firstExisting(candidates: string[]): string | null {
58
+ for (const p of candidates) {
54
59
  try {
55
60
  if (existsSync(p)) return p;
56
61
  } catch {
@@ -60,6 +65,32 @@ export function locateSystemSqlite(): string | null {
60
65
  return null;
61
66
  }
62
67
 
68
+ /** Locate a system extension-enabled libsqlite3, or null if none found. */
69
+ export function locateSystemSqlite(): string | null {
70
+ return firstExisting(systemSqliteCandidates());
71
+ }
72
+
73
+ // Probe order: explicit override first, then the vendored binary for this
74
+ // platform (packages/openltm-core/vendor/honker/<platform>/libhonker_ext.<ext>).
75
+ function honkerExtCandidates(): string[] {
76
+ const out: string[] = [];
77
+ const override = process.env["LTM_HONKER_EXT"];
78
+ if (override) out.push(override);
79
+ const platform = `${process.platform}-${process.arch}`;
80
+ const suffix = process.platform === "darwin" ? "dylib" : "so";
81
+ out.push(join(MODULE_DIR, "..", "vendor", "honker", platform, `libhonker_ext.${suffix}`));
82
+ return out;
83
+ }
84
+
85
+ /**
86
+ * Locate the Honker loadable extension, or null if none is available for this
87
+ * platform. Mirrors locateSystemSqlite(): LTM_HONKER_EXT override wins, then the
88
+ * vendored per-platform binary. A null result means Honker stays dormant.
89
+ */
90
+ export function locateHonkerExt(): string | null {
91
+ return firstExisting(honkerExtCandidates());
92
+ }
93
+
63
94
  /**
64
95
  * Switch the process to a system extension-enabled SQLite. Idempotent and
65
96
  * never throws. Must run before the first Database is constructed to take
@@ -74,8 +105,16 @@ export function ensureCustomSqlite(): boolean {
74
105
  const lib = locateSystemSqlite();
75
106
  if (!lib) return false;
76
107
  try {
77
- (Database as unknown as { setCustomSQLite: (p: string) => void }).setCustomSQLite(lib);
108
+ const DB = Database as unknown as { setCustomSQLite: (p: string) => void };
109
+ DB.setCustomSQLite(lib);
78
110
  _customSqliteApplied = true;
111
+ // setCustomSQLite is a once-per-process global (Bun throws "SQLite already
112
+ // loaded" on a second call). honker-bun's open() calls it again on its own
113
+ // connection and would throw — silently killing the honker handle even
114
+ // though the extension-enabled SQLite is already active process-wide. Now
115
+ // that we've loaded it, neutralise further calls so honker-bun (and any
116
+ // other consumer) no-ops instead of throwing.
117
+ DB.setCustomSQLite = () => {};
79
118
  return true;
80
119
  } catch {
81
120
  return false;
@@ -99,8 +138,8 @@ function loadVec(db: Database): boolean {
99
138
  }
100
139
 
101
140
  function loadHonker(db: Database): boolean {
102
- const ext = process.env["LTM_HONKER_EXT"];
103
- if (!ext || !existsSync(ext)) return false;
141
+ const ext = locateHonkerExt();
142
+ if (!ext) return false;
104
143
  try {
105
144
  db.loadExtension(ext);
106
145
  return true;
@@ -114,12 +153,14 @@ function loadHonker(db: Database): boolean {
114
153
  * capabilities for the process. Never throws.
115
154
  *
116
155
  * @param opts.vec attempt sqlite-vec (default true; env LTM_DISABLE_VEC wins)
117
- * @param opts.honker attempt Honker (default false; env LTM_DISABLE_HONKER wins,
118
- * and a libhonker_ext path must be set via LTM_HONKER_EXT)
156
+ * @param opts.honker attempt Honker (default: auto-on when a libhonker_ext binary
157
+ * is discoverable via locateHonkerExt(); env LTM_DISABLE_HONKER
158
+ * force-disables, and LTM_HONKER_EXT overrides the binary path)
119
159
  */
120
160
  export function loadExtensions(db: Database, opts?: { vec?: boolean; honker?: boolean }): Capabilities {
121
161
  const wantVec = (opts?.vec ?? true) && !envDisabled("LTM_DISABLE_VEC");
122
- const wantHonker = (opts?.honker ?? false) && !envDisabled("LTM_DISABLE_HONKER");
162
+ const wantHonker =
163
+ (opts?.honker ?? locateHonkerExt() !== null) && !envDisabled("LTM_DISABLE_HONKER");
123
164
 
124
165
  const customSqlite = ensureCustomSqlite();
125
166
  const vec = customSqlite && wantVec ? loadVec(db) : false;
package/src/lib/honker.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * pre-Honker behaviour (inline embed, HTTP poll, file-watcher).
14
14
  */
15
15
  import type { HonkerDb, HonkerModule } from "./honkerTypes.js";
16
- import { getCapabilities, locateSystemSqlite } from "../extensions.js";
16
+ import { getCapabilities, locateSystemSqlite, locateHonkerExt } from "../extensions.js";
17
17
  import { DB_PATH } from "../shared-db.js";
18
18
 
19
19
  let _handle: HonkerDb | null | undefined = undefined;
@@ -31,7 +31,7 @@ export function getHonker(): HonkerDb | null {
31
31
 
32
32
  function openHonker(): HonkerDb | null {
33
33
  if (!getCapabilities().honker) return null;
34
- const ext = process.env["LTM_HONKER_EXT"];
34
+ const ext = locateHonkerExt();
35
35
  if (!ext) return null;
36
36
  try {
37
37
  // eslint-disable-next-line @typescript-eslint/no-var-requires