@deeplake/hivemind 0.6.48 → 0.7.4

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 (40) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +147 -20
  4. package/bundle/cli.js +552 -95
  5. package/codex/bundle/capture.js +509 -89
  6. package/codex/bundle/commands/auth-login.js +209 -66
  7. package/codex/bundle/embeddings/embed-daemon.js +243 -0
  8. package/codex/bundle/pre-tool-use.js +629 -104
  9. package/codex/bundle/session-start-setup.js +194 -57
  10. package/codex/bundle/session-start.js +25 -10
  11. package/codex/bundle/shell/deeplake-shell.js +679 -112
  12. package/codex/bundle/stop.js +476 -58
  13. package/codex/bundle/wiki-worker.js +312 -11
  14. package/cursor/bundle/capture.js +768 -57
  15. package/cursor/bundle/commands/auth-login.js +209 -66
  16. package/cursor/bundle/embeddings/embed-daemon.js +243 -0
  17. package/cursor/bundle/pre-tool-use.js +561 -70
  18. package/cursor/bundle/session-end.js +223 -2
  19. package/cursor/bundle/session-start.js +192 -54
  20. package/cursor/bundle/shell/deeplake-shell.js +679 -112
  21. package/cursor/bundle/wiki-worker.js +571 -0
  22. package/hermes/bundle/capture.js +771 -58
  23. package/hermes/bundle/commands/auth-login.js +209 -66
  24. package/hermes/bundle/embeddings/embed-daemon.js +243 -0
  25. package/hermes/bundle/pre-tool-use.js +560 -69
  26. package/hermes/bundle/session-end.js +224 -1
  27. package/hermes/bundle/session-start.js +195 -54
  28. package/hermes/bundle/shell/deeplake-shell.js +679 -112
  29. package/hermes/bundle/wiki-worker.js +572 -0
  30. package/mcp/bundle/server.js +253 -68
  31. package/openclaw/dist/chunks/auth-creds-AEKS6D3P.js +14 -0
  32. package/openclaw/dist/chunks/chunk-SRCBBT4H.js +37 -0
  33. package/openclaw/dist/chunks/config-G23NI5TV.js +33 -0
  34. package/openclaw/dist/chunks/index-marker-store-PGT5CW6T.js +33 -0
  35. package/openclaw/dist/chunks/setup-config-C35UK4LP.js +114 -0
  36. package/openclaw/dist/index.js +752 -702
  37. package/openclaw/openclaw.plugin.json +1 -1
  38. package/openclaw/package.json +1 -1
  39. package/package.json +2 -1
  40. package/pi/extension-source/hivemind.ts +473 -21
@@ -1,3 +1,56 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // dist/src/index-marker-store.js
12
+ var index_marker_store_exports = {};
13
+ __export(index_marker_store_exports, {
14
+ buildIndexMarkerPath: () => buildIndexMarkerPath,
15
+ getIndexMarkerDir: () => getIndexMarkerDir,
16
+ hasFreshIndexMarker: () => hasFreshIndexMarker,
17
+ writeIndexMarker: () => writeIndexMarker
18
+ });
19
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
20
+ import { join as join3 } from "node:path";
21
+ import { tmpdir } from "node:os";
22
+ function getIndexMarkerDir() {
23
+ return process.env.HIVEMIND_INDEX_MARKER_DIR ?? join3(tmpdir(), "hivemind-deeplake-indexes");
24
+ }
25
+ function buildIndexMarkerPath(workspaceId, orgId, table, suffix) {
26
+ const markerKey = [workspaceId, orgId, table, suffix].join("__").replace(/[^a-zA-Z0-9_.-]/g, "_");
27
+ return join3(getIndexMarkerDir(), `${markerKey}.json`);
28
+ }
29
+ function hasFreshIndexMarker(markerPath) {
30
+ if (!existsSync2(markerPath))
31
+ return false;
32
+ try {
33
+ const raw = JSON.parse(readFileSync2(markerPath, "utf-8"));
34
+ const updatedAt = raw.updatedAt ? new Date(raw.updatedAt).getTime() : NaN;
35
+ if (!Number.isFinite(updatedAt) || Date.now() - updatedAt > INDEX_MARKER_TTL_MS)
36
+ return false;
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+ function writeIndexMarker(markerPath) {
43
+ mkdirSync(getIndexMarkerDir(), { recursive: true });
44
+ writeFileSync(markerPath, JSON.stringify({ updatedAt: (/* @__PURE__ */ new Date()).toISOString() }), "utf-8");
45
+ }
46
+ var INDEX_MARKER_TTL_MS;
47
+ var init_index_marker_store = __esm({
48
+ "dist/src/index-marker-store.js"() {
49
+ "use strict";
50
+ INDEX_MARKER_TTL_MS = Number(process.env.HIVEMIND_INDEX_MARKER_TTL_MS ?? 6 * 60 * 6e4);
51
+ }
52
+ });
53
+
1
54
  // dist/src/utils/stdin.js
2
55
  function readStdin() {
3
56
  return new Promise((resolve, reject) => {
@@ -49,9 +102,6 @@ function loadConfig() {
49
102
 
50
103
  // dist/src/deeplake-api.js
51
104
  import { randomUUID } from "node:crypto";
52
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
53
- import { join as join3 } from "node:path";
54
- import { tmpdir } from "node:os";
55
105
 
56
106
  // dist/src/utils/debug.js
57
107
  import { appendFileSync } from "node:fs";
@@ -59,6 +109,9 @@ import { join as join2 } from "node:path";
59
109
  import { homedir as homedir2 } from "node:os";
60
110
  var DEBUG = process.env.HIVEMIND_DEBUG === "1";
61
111
  var LOG = join2(homedir2(), ".deeplake", "hook-debug.log");
112
+ function utcTimestamp(d = /* @__PURE__ */ new Date()) {
113
+ return d.toISOString().replace("T", " ").slice(0, 19) + " UTC";
114
+ }
62
115
  function log(tag, msg) {
63
116
  if (!DEBUG)
64
117
  return;
@@ -71,7 +124,26 @@ function sqlStr(value) {
71
124
  return value.replace(/\\/g, "\\\\").replace(/'/g, "''").replace(/\0/g, "").replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
72
125
  }
73
126
 
127
+ // dist/src/embeddings/columns.js
128
+ var SUMMARY_EMBEDDING_COL = "summary_embedding";
129
+ var MESSAGE_EMBEDDING_COL = "message_embedding";
130
+
131
+ // dist/src/utils/client-header.js
132
+ var DEEPLAKE_CLIENT_HEADER = "X-Deeplake-Client";
133
+ function deeplakeClientValue() {
134
+ return "hivemind";
135
+ }
136
+ function deeplakeClientHeader() {
137
+ return { [DEEPLAKE_CLIENT_HEADER]: deeplakeClientValue() };
138
+ }
139
+
74
140
  // dist/src/deeplake-api.js
141
+ var indexMarkerStorePromise = null;
142
+ function getIndexMarkerStore() {
143
+ if (!indexMarkerStorePromise)
144
+ indexMarkerStorePromise = Promise.resolve().then(() => (init_index_marker_store(), index_marker_store_exports));
145
+ return indexMarkerStorePromise;
146
+ }
75
147
  var log2 = (msg) => log("sdk", msg);
76
148
  function summarizeSql(sql, maxLen = 220) {
77
149
  const compact = sql.replace(/\s+/g, " ").trim();
@@ -91,7 +163,6 @@ var MAX_RETRIES = 3;
91
163
  var BASE_DELAY_MS = 500;
92
164
  var MAX_CONCURRENCY = 5;
93
165
  var QUERY_TIMEOUT_MS = Number(process.env.HIVEMIND_QUERY_TIMEOUT_MS ?? 1e4);
94
- var INDEX_MARKER_TTL_MS = Number(process.env.HIVEMIND_INDEX_MARKER_TTL_MS ?? 6 * 60 * 6e4);
95
166
  function sleep(ms) {
96
167
  return new Promise((resolve) => setTimeout(resolve, ms));
97
168
  }
@@ -111,9 +182,6 @@ function isTransientHtml403(text) {
111
182
  const body = text.toLowerCase();
112
183
  return body.includes("<html") || body.includes("403 forbidden") || body.includes("cloudflare") || body.includes("nginx");
113
184
  }
114
- function getIndexMarkerDir() {
115
- return process.env.HIVEMIND_INDEX_MARKER_DIR ?? join3(tmpdir(), "hivemind-deeplake-indexes");
116
- }
117
185
  var Semaphore = class {
118
186
  max;
119
187
  waiting = [];
@@ -182,7 +250,8 @@ var DeeplakeApi = class {
182
250
  headers: {
183
251
  Authorization: `Bearer ${this.token}`,
184
252
  "Content-Type": "application/json",
185
- "X-Activeloop-Org-Id": this.orgId
253
+ "X-Activeloop-Org-Id": this.orgId,
254
+ ...deeplakeClientHeader()
186
255
  },
187
256
  signal,
188
257
  body: JSON.stringify({ query: sql })
@@ -209,7 +278,8 @@ var DeeplakeApi = class {
209
278
  }
210
279
  const text = await resp.text().catch(() => "");
211
280
  const retryable403 = isSessionInsertQuery(sql) && (resp.status === 401 || resp.status === 403 && (text.length === 0 || isTransientHtml403(text)));
212
- if (attempt < MAX_RETRIES && (RETRYABLE_CODES.has(resp.status) || retryable403)) {
281
+ const alreadyExists = resp.status === 500 && isDuplicateIndexError(text);
282
+ if (!alreadyExists && attempt < MAX_RETRIES && (RETRYABLE_CODES.has(resp.status) || retryable403)) {
213
283
  const delay = BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 200;
214
284
  log2(`query retry ${attempt + 1}/${MAX_RETRIES} (${resp.status}) in ${delay.toFixed(0)}ms`);
215
285
  await sleep(delay);
@@ -243,7 +313,7 @@ var DeeplakeApi = class {
243
313
  const lud = row.lastUpdateDate ?? ts;
244
314
  const exists = await this.query(`SELECT path FROM "${this.tableName}" WHERE path = '${sqlStr(row.path)}' LIMIT 1`);
245
315
  if (exists.length > 0) {
246
- let setClauses = `summary = E'${sqlStr(row.contentText)}', mime_type = '${sqlStr(row.mimeType)}', size_bytes = ${row.sizeBytes}, last_update_date = '${lud}'`;
316
+ let setClauses = `summary = E'${sqlStr(row.contentText)}', ${SUMMARY_EMBEDDING_COL} = NULL, mime_type = '${sqlStr(row.mimeType)}', size_bytes = ${row.sizeBytes}, last_update_date = '${lud}'`;
247
317
  if (row.project !== void 0)
248
318
  setClauses += `, project = '${sqlStr(row.project)}'`;
249
319
  if (row.description !== void 0)
@@ -251,8 +321,8 @@ var DeeplakeApi = class {
251
321
  await this.query(`UPDATE "${this.tableName}" SET ${setClauses} WHERE path = '${sqlStr(row.path)}'`);
252
322
  } else {
253
323
  const id = randomUUID();
254
- let cols = "id, path, filename, summary, mime_type, size_bytes, creation_date, last_update_date";
255
- let vals = `'${id}', '${sqlStr(row.path)}', '${sqlStr(row.filename)}', E'${sqlStr(row.contentText)}', '${sqlStr(row.mimeType)}', ${row.sizeBytes}, '${cd}', '${lud}'`;
324
+ let cols = `id, path, filename, summary, ${SUMMARY_EMBEDDING_COL}, mime_type, size_bytes, creation_date, last_update_date`;
325
+ let vals = `'${id}', '${sqlStr(row.path)}', '${sqlStr(row.filename)}', E'${sqlStr(row.contentText)}', NULL, '${sqlStr(row.mimeType)}', ${row.sizeBytes}, '${cd}', '${lud}'`;
256
326
  if (row.project !== void 0) {
257
327
  cols += ", project";
258
328
  vals += `, '${sqlStr(row.project)}'`;
@@ -277,48 +347,83 @@ var DeeplakeApi = class {
277
347
  buildLookupIndexName(table, suffix) {
278
348
  return `idx_${table}_${suffix}`.replace(/[^a-zA-Z0-9_]/g, "_");
279
349
  }
280
- getLookupIndexMarkerPath(table, suffix) {
281
- const markerKey = [
282
- this.workspaceId,
283
- this.orgId,
284
- table,
285
- suffix
286
- ].join("__").replace(/[^a-zA-Z0-9_.-]/g, "_");
287
- return join3(getIndexMarkerDir(), `${markerKey}.json`);
288
- }
289
- hasFreshLookupIndexMarker(table, suffix) {
290
- const markerPath = this.getLookupIndexMarkerPath(table, suffix);
291
- if (!existsSync2(markerPath))
292
- return false;
293
- try {
294
- const raw = JSON.parse(readFileSync2(markerPath, "utf-8"));
295
- const updatedAt = raw.updatedAt ? new Date(raw.updatedAt).getTime() : NaN;
296
- if (!Number.isFinite(updatedAt) || Date.now() - updatedAt > INDEX_MARKER_TTL_MS)
297
- return false;
298
- return true;
299
- } catch {
300
- return false;
301
- }
302
- }
303
- markLookupIndexReady(table, suffix) {
304
- mkdirSync(getIndexMarkerDir(), { recursive: true });
305
- writeFileSync(this.getLookupIndexMarkerPath(table, suffix), JSON.stringify({ updatedAt: (/* @__PURE__ */ new Date()).toISOString() }), "utf-8");
306
- }
307
350
  async ensureLookupIndex(table, suffix, columnsSql) {
308
- if (this.hasFreshLookupIndexMarker(table, suffix))
351
+ const markers = await getIndexMarkerStore();
352
+ const markerPath = markers.buildIndexMarkerPath(this.workspaceId, this.orgId, table, suffix);
353
+ if (markers.hasFreshIndexMarker(markerPath))
309
354
  return;
310
355
  const indexName = this.buildLookupIndexName(table, suffix);
311
356
  try {
312
357
  await this.query(`CREATE INDEX IF NOT EXISTS "${indexName}" ON "${table}" ${columnsSql}`);
313
- this.markLookupIndexReady(table, suffix);
358
+ markers.writeIndexMarker(markerPath);
314
359
  } catch (e) {
315
360
  if (isDuplicateIndexError(e)) {
316
- this.markLookupIndexReady(table, suffix);
361
+ markers.writeIndexMarker(markerPath);
317
362
  return;
318
363
  }
319
364
  log2(`index "${indexName}" skipped: ${e.message}`);
320
365
  }
321
366
  }
367
+ /**
368
+ * Ensure a vector column exists on the given table.
369
+ *
370
+ * The previous implementation always issued `ALTER TABLE ADD COLUMN IF NOT
371
+ * EXISTS …` on every SessionStart. On a long-running workspace that's
372
+ * already migrated, every call returns 500 "Column already exists" — noisy
373
+ * in the log and a wasted round-trip. Worse, the very first call after the
374
+ * column is genuinely added triggers Deeplake's post-ALTER `vector::at`
375
+ * window (~30s) during which subsequent INSERTs fail; minimising the
376
+ * number of ALTER calls minimises exposure to that window.
377
+ *
378
+ * New flow:
379
+ * 1. Check the local marker file (mirrors ensureLookupIndex). If fresh,
380
+ * return — zero network calls.
381
+ * 2. SELECT 1 FROM information_schema.columns WHERE table_name = T AND
382
+ * column_name = C. Read-only, idempotent, can't tickle the post-ALTER
383
+ * bug. If the column is present → mark + return.
384
+ * 3. Only if step 2 says the column is missing, fall back to ALTER ADD
385
+ * COLUMN IF NOT EXISTS. Mark on success, also mark if Deeplake reports
386
+ * "already exists" (race: another client added it between our SELECT
387
+ * and ALTER).
388
+ *
389
+ * Marker uses the same dir / TTL as ensureLookupIndex so both schema
390
+ * caches share an opt-out (HIVEMIND_INDEX_MARKER_DIR) and a TTL knob.
391
+ */
392
+ async ensureEmbeddingColumn(table, column) {
393
+ await this.ensureColumn(table, column, "FLOAT4[]");
394
+ }
395
+ /**
396
+ * Generic marker-gated column migration. Same SELECT-then-ALTER flow as
397
+ * ensureEmbeddingColumn, parameterized by SQL type so it can patch up any
398
+ * column that was added to the schema after the table was originally
399
+ * created. Used today for `summary_embedding`, `message_embedding`, and
400
+ * the `agent` column (added 2026-04-11) — the latter has no fallback if
401
+ * a user upgraded over a pre-2026-04-11 table, so every INSERT fails
402
+ * with `column "agent" does not exist`.
403
+ */
404
+ async ensureColumn(table, column, sqlType) {
405
+ const markers = await getIndexMarkerStore();
406
+ const markerPath = markers.buildIndexMarkerPath(this.workspaceId, this.orgId, table, `col_${column}`);
407
+ if (markers.hasFreshIndexMarker(markerPath))
408
+ return;
409
+ const colCheck = `SELECT 1 FROM information_schema.columns WHERE table_name = '${sqlStr(table)}' AND column_name = '${sqlStr(column)}' AND table_schema = '${sqlStr(this.workspaceId)}' LIMIT 1`;
410
+ const rows = await this.query(colCheck);
411
+ if (rows.length > 0) {
412
+ markers.writeIndexMarker(markerPath);
413
+ return;
414
+ }
415
+ try {
416
+ await this.query(`ALTER TABLE "${table}" ADD COLUMN ${column} ${sqlType}`);
417
+ } catch (e) {
418
+ const msg = e instanceof Error ? e.message : String(e);
419
+ if (!/already exists/i.test(msg))
420
+ throw e;
421
+ const recheck = await this.query(colCheck);
422
+ if (recheck.length === 0)
423
+ throw e;
424
+ }
425
+ markers.writeIndexMarker(markerPath);
426
+ }
322
427
  /** List all tables in the workspace (with retry). */
323
428
  async listTables(forceRefresh = false) {
324
429
  if (!forceRefresh && this._tablesCache)
@@ -334,7 +439,8 @@ var DeeplakeApi = class {
334
439
  const resp = await fetch(`${this.apiUrl}/workspaces/${this.workspaceId}/tables`, {
335
440
  headers: {
336
441
  Authorization: `Bearer ${this.token}`,
337
- "X-Activeloop-Org-Id": this.orgId
442
+ "X-Activeloop-Org-Id": this.orgId,
443
+ ...deeplakeClientHeader()
338
444
  }
339
445
  });
340
446
  if (resp.ok) {
@@ -359,28 +465,60 @@ var DeeplakeApi = class {
359
465
  }
360
466
  return { tables: [], cacheable: false };
361
467
  }
468
+ /**
469
+ * Run a `CREATE TABLE` with an extra outer retry budget. The base
470
+ * `query()` already retries 3 times on fetch errors (~3.5s total), but a
471
+ * failed CREATE is permanent corruption — every subsequent SELECT against
472
+ * the missing table fails. Wrapping in an outer loop with longer backoff
473
+ * (2s, 5s, then 10s) gives us ~17s of reach across transient network
474
+ * blips before giving up. Failures still propagate; getApi() resets its
475
+ * cache on init failure (openclaw plugin) so the next call retries the
476
+ * whole init flow.
477
+ */
478
+ async createTableWithRetry(sql, label) {
479
+ const OUTER_BACKOFFS_MS = [2e3, 5e3, 1e4];
480
+ let lastErr = null;
481
+ for (let attempt = 0; attempt <= OUTER_BACKOFFS_MS.length; attempt++) {
482
+ try {
483
+ await this.query(sql);
484
+ return;
485
+ } catch (err) {
486
+ lastErr = err;
487
+ const msg = err instanceof Error ? err.message : String(err);
488
+ log2(`CREATE TABLE "${label}" attempt ${attempt + 1}/${OUTER_BACKOFFS_MS.length + 1} failed: ${msg}`);
489
+ if (attempt < OUTER_BACKOFFS_MS.length) {
490
+ await sleep(OUTER_BACKOFFS_MS[attempt]);
491
+ }
492
+ }
493
+ }
494
+ throw lastErr;
495
+ }
362
496
  /** Create the memory table if it doesn't already exist. Migrate columns on existing tables. */
363
497
  async ensureTable(name) {
364
498
  const tbl = name ?? this.tableName;
365
499
  const tables = await this.listTables();
366
500
  if (!tables.includes(tbl)) {
367
501
  log2(`table "${tbl}" not found, creating`);
368
- await this.query(`CREATE TABLE IF NOT EXISTS "${tbl}" (id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', filename TEXT NOT NULL DEFAULT '', summary TEXT NOT NULL DEFAULT '', author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'text/plain', size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT '') USING deeplake`);
502
+ await this.createTableWithRetry(`CREATE TABLE IF NOT EXISTS "${tbl}" (id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', filename TEXT NOT NULL DEFAULT '', summary TEXT NOT NULL DEFAULT '', summary_embedding FLOAT4[], author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'text/plain', size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT '') USING deeplake`, tbl);
369
503
  log2(`table "${tbl}" created`);
370
504
  if (!tables.includes(tbl))
371
505
  this._tablesCache = [...tables, tbl];
372
506
  }
507
+ await this.ensureEmbeddingColumn(tbl, SUMMARY_EMBEDDING_COL);
508
+ await this.ensureColumn(tbl, "agent", "TEXT NOT NULL DEFAULT ''");
373
509
  }
374
510
  /** Create the sessions table (uses JSONB for message since every row is a JSON event). */
375
511
  async ensureSessionsTable(name) {
376
512
  const tables = await this.listTables();
377
513
  if (!tables.includes(name)) {
378
514
  log2(`table "${name}" not found, creating`);
379
- await this.query(`CREATE TABLE IF NOT EXISTS "${name}" (id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', filename TEXT NOT NULL DEFAULT '', message JSONB, author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'application/json', size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT '') USING deeplake`);
515
+ await this.createTableWithRetry(`CREATE TABLE IF NOT EXISTS "${name}" (id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', filename TEXT NOT NULL DEFAULT '', message JSONB, message_embedding FLOAT4[], author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'application/json', size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT '') USING deeplake`, name);
380
516
  log2(`table "${name}" created`);
381
517
  if (!tables.includes(name))
382
518
  this._tablesCache = [...tables, name];
383
519
  }
520
+ await this.ensureEmbeddingColumn(name, MESSAGE_EMBEDDING_COL);
521
+ await this.ensureColumn(name, "agent", "TEXT NOT NULL DEFAULT ''");
384
522
  await this.ensureLookupIndex(name, "path_creation_date", `("path", "creation_date")`);
385
523
  }
386
524
  };
@@ -391,8 +529,548 @@ function buildSessionPath(config, sessionId) {
391
529
  return `/sessions/${config.userName}/${config.userName}_${config.orgName}_${workspace}_${sessionId}.jsonl`;
392
530
  }
393
531
 
532
+ // dist/src/embeddings/client.js
533
+ import { connect } from "node:net";
534
+ import { spawn } from "node:child_process";
535
+ import { openSync, closeSync, writeSync, unlinkSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
536
+ import { homedir as homedir3 } from "node:os";
537
+ import { join as join4 } from "node:path";
538
+
539
+ // dist/src/embeddings/protocol.js
540
+ var DEFAULT_SOCKET_DIR = "/tmp";
541
+ var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
542
+ var DEFAULT_CLIENT_TIMEOUT_MS = 2e3;
543
+ function socketPathFor(uid, dir = DEFAULT_SOCKET_DIR) {
544
+ return `${dir}/hivemind-embed-${uid}.sock`;
545
+ }
546
+ function pidPathFor(uid, dir = DEFAULT_SOCKET_DIR) {
547
+ return `${dir}/hivemind-embed-${uid}.pid`;
548
+ }
549
+
550
+ // dist/src/embeddings/client.js
551
+ var SHARED_DAEMON_PATH = join4(homedir3(), ".hivemind", "embed-deps", "embed-daemon.js");
552
+ var log3 = (m) => log("embed-client", m);
553
+ function getUid() {
554
+ const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
555
+ return uid !== void 0 ? String(uid) : process.env.USER ?? "default";
556
+ }
557
+ var EmbedClient = class {
558
+ socketPath;
559
+ pidPath;
560
+ timeoutMs;
561
+ daemonEntry;
562
+ autoSpawn;
563
+ spawnWaitMs;
564
+ nextId = 0;
565
+ constructor(opts = {}) {
566
+ const uid = getUid();
567
+ const dir = opts.socketDir ?? "/tmp";
568
+ this.socketPath = socketPathFor(uid, dir);
569
+ this.pidPath = pidPathFor(uid, dir);
570
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_CLIENT_TIMEOUT_MS;
571
+ this.daemonEntry = opts.daemonEntry ?? process.env.HIVEMIND_EMBED_DAEMON ?? (existsSync3(SHARED_DAEMON_PATH) ? SHARED_DAEMON_PATH : void 0);
572
+ this.autoSpawn = opts.autoSpawn ?? true;
573
+ this.spawnWaitMs = opts.spawnWaitMs ?? 5e3;
574
+ }
575
+ /**
576
+ * Returns an embedding vector, or null on timeout/failure. Hooks MUST treat
577
+ * null as "skip embedding column" — never block the write path on us.
578
+ *
579
+ * Fire-and-forget spawn on miss: if the daemon isn't up, this call returns
580
+ * null AND kicks off a background spawn. The next call finds a ready daemon.
581
+ */
582
+ async embed(text, kind = "document") {
583
+ let sock;
584
+ try {
585
+ sock = await this.connectOnce();
586
+ } catch {
587
+ if (this.autoSpawn)
588
+ this.trySpawnDaemon();
589
+ return null;
590
+ }
591
+ try {
592
+ const id = String(++this.nextId);
593
+ const req = { op: "embed", id, kind, text };
594
+ const resp = await this.sendAndWait(sock, req);
595
+ if (resp.error || !("embedding" in resp) || !resp.embedding) {
596
+ log3(`embed err: ${resp.error ?? "no embedding"}`);
597
+ return null;
598
+ }
599
+ return resp.embedding;
600
+ } catch (e) {
601
+ const err = e instanceof Error ? e.message : String(e);
602
+ log3(`embed failed: ${err}`);
603
+ return null;
604
+ } finally {
605
+ try {
606
+ sock.end();
607
+ } catch {
608
+ }
609
+ }
610
+ }
611
+ /**
612
+ * Wait up to spawnWaitMs for the daemon to accept connections, spawning if
613
+ * necessary. Meant for SessionStart / long-running batches — not the hot path.
614
+ */
615
+ async warmup() {
616
+ try {
617
+ const s = await this.connectOnce();
618
+ s.end();
619
+ return true;
620
+ } catch {
621
+ if (!this.autoSpawn)
622
+ return false;
623
+ this.trySpawnDaemon();
624
+ try {
625
+ const s = await this.waitForSocket();
626
+ s.end();
627
+ return true;
628
+ } catch {
629
+ return false;
630
+ }
631
+ }
632
+ }
633
+ connectOnce() {
634
+ return new Promise((resolve, reject) => {
635
+ const sock = connect(this.socketPath);
636
+ const to = setTimeout(() => {
637
+ sock.destroy();
638
+ reject(new Error("connect timeout"));
639
+ }, this.timeoutMs);
640
+ sock.once("connect", () => {
641
+ clearTimeout(to);
642
+ resolve(sock);
643
+ });
644
+ sock.once("error", (e) => {
645
+ clearTimeout(to);
646
+ reject(e);
647
+ });
648
+ });
649
+ }
650
+ trySpawnDaemon() {
651
+ let fd;
652
+ try {
653
+ fd = openSync(this.pidPath, "wx", 384);
654
+ writeSync(fd, String(process.pid));
655
+ } catch (e) {
656
+ if (this.isPidFileStale()) {
657
+ try {
658
+ unlinkSync(this.pidPath);
659
+ } catch {
660
+ }
661
+ try {
662
+ fd = openSync(this.pidPath, "wx", 384);
663
+ writeSync(fd, String(process.pid));
664
+ } catch {
665
+ return;
666
+ }
667
+ } else {
668
+ return;
669
+ }
670
+ }
671
+ if (!this.daemonEntry || !existsSync3(this.daemonEntry)) {
672
+ log3(`daemonEntry not configured or missing: ${this.daemonEntry}`);
673
+ try {
674
+ closeSync(fd);
675
+ unlinkSync(this.pidPath);
676
+ } catch {
677
+ }
678
+ return;
679
+ }
680
+ try {
681
+ const child = spawn(process.execPath, [this.daemonEntry], {
682
+ detached: true,
683
+ stdio: "ignore",
684
+ env: process.env
685
+ });
686
+ child.unref();
687
+ log3(`spawned daemon pid=${child.pid}`);
688
+ } finally {
689
+ closeSync(fd);
690
+ }
691
+ }
692
+ isPidFileStale() {
693
+ try {
694
+ const raw = readFileSync3(this.pidPath, "utf-8").trim();
695
+ const pid = Number(raw);
696
+ if (!pid || Number.isNaN(pid))
697
+ return true;
698
+ try {
699
+ process.kill(pid, 0);
700
+ return false;
701
+ } catch {
702
+ return true;
703
+ }
704
+ } catch {
705
+ return true;
706
+ }
707
+ }
708
+ async waitForSocket() {
709
+ const deadline = Date.now() + this.spawnWaitMs;
710
+ let delay = 30;
711
+ while (Date.now() < deadline) {
712
+ await sleep2(delay);
713
+ delay = Math.min(delay * 1.5, 300);
714
+ if (!existsSync3(this.socketPath))
715
+ continue;
716
+ try {
717
+ return await this.connectOnce();
718
+ } catch {
719
+ }
720
+ }
721
+ throw new Error("daemon did not become ready within spawnWaitMs");
722
+ }
723
+ sendAndWait(sock, req) {
724
+ return new Promise((resolve, reject) => {
725
+ let buf = "";
726
+ const to = setTimeout(() => {
727
+ sock.destroy();
728
+ reject(new Error("request timeout"));
729
+ }, this.timeoutMs);
730
+ sock.setEncoding("utf-8");
731
+ sock.on("data", (chunk) => {
732
+ buf += chunk;
733
+ const nl = buf.indexOf("\n");
734
+ if (nl === -1)
735
+ return;
736
+ const line = buf.slice(0, nl);
737
+ clearTimeout(to);
738
+ try {
739
+ resolve(JSON.parse(line));
740
+ } catch (e) {
741
+ reject(e);
742
+ }
743
+ });
744
+ sock.on("error", (e) => {
745
+ clearTimeout(to);
746
+ reject(e);
747
+ });
748
+ sock.on("end", () => {
749
+ clearTimeout(to);
750
+ reject(new Error("connection closed without response"));
751
+ });
752
+ sock.write(JSON.stringify(req) + "\n");
753
+ });
754
+ }
755
+ };
756
+ function sleep2(ms) {
757
+ return new Promise((r) => setTimeout(r, ms));
758
+ }
759
+
760
+ // dist/src/embeddings/sql.js
761
+ function embeddingSqlLiteral(vec) {
762
+ if (!vec || vec.length === 0)
763
+ return "NULL";
764
+ const parts = [];
765
+ for (const v of vec) {
766
+ if (!Number.isFinite(v))
767
+ return "NULL";
768
+ parts.push(String(v));
769
+ }
770
+ return `ARRAY[${parts.join(",")}]::float4[]`;
771
+ }
772
+
773
+ // dist/src/embeddings/disable.js
774
+ import { createRequire } from "node:module";
775
+ import { homedir as homedir4 } from "node:os";
776
+ import { join as join5 } from "node:path";
777
+ import { pathToFileURL } from "node:url";
778
+ var cachedStatus = null;
779
+ function defaultResolveTransformers() {
780
+ try {
781
+ createRequire(import.meta.url).resolve("@huggingface/transformers");
782
+ return;
783
+ } catch {
784
+ }
785
+ const sharedDir = join5(homedir4(), ".hivemind", "embed-deps");
786
+ createRequire(pathToFileURL(`${sharedDir}/`).href).resolve("@huggingface/transformers");
787
+ }
788
+ var _resolve = defaultResolveTransformers;
789
+ function detectStatus() {
790
+ if (process.env.HIVEMIND_EMBEDDINGS === "false")
791
+ return "env-disabled";
792
+ try {
793
+ _resolve();
794
+ return "enabled";
795
+ } catch {
796
+ return "no-transformers";
797
+ }
798
+ }
799
+ function embeddingsStatus() {
800
+ if (cachedStatus !== null)
801
+ return cachedStatus;
802
+ cachedStatus = detectStatus();
803
+ return cachedStatus;
804
+ }
805
+ function embeddingsDisabled() {
806
+ return embeddingsStatus() !== "enabled";
807
+ }
808
+
394
809
  // dist/src/hooks/hermes/capture.js
395
- var log3 = (msg) => log("hermes-capture", msg);
810
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
811
+ import { dirname as dirname2, join as join9 } from "node:path";
812
+
813
+ // dist/src/hooks/summary-state.js
814
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, writeSync as writeSync2, mkdirSync as mkdirSync2, renameSync, existsSync as existsSync4, unlinkSync as unlinkSync2, openSync as openSync2, closeSync as closeSync2 } from "node:fs";
815
+ import { homedir as homedir5 } from "node:os";
816
+ import { join as join6 } from "node:path";
817
+ var dlog = (msg) => log("summary-state", msg);
818
+ var STATE_DIR = join6(homedir5(), ".claude", "hooks", "summary-state");
819
+ var YIELD_BUF = new Int32Array(new SharedArrayBuffer(4));
820
+ function statePath(sessionId) {
821
+ return join6(STATE_DIR, `${sessionId}.json`);
822
+ }
823
+ function lockPath(sessionId) {
824
+ return join6(STATE_DIR, `${sessionId}.lock`);
825
+ }
826
+ function readState(sessionId) {
827
+ const p = statePath(sessionId);
828
+ if (!existsSync4(p))
829
+ return null;
830
+ try {
831
+ return JSON.parse(readFileSync4(p, "utf-8"));
832
+ } catch {
833
+ return null;
834
+ }
835
+ }
836
+ function writeState(sessionId, state) {
837
+ mkdirSync2(STATE_DIR, { recursive: true });
838
+ const p = statePath(sessionId);
839
+ const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
840
+ writeFileSync2(tmp, JSON.stringify(state));
841
+ renameSync(tmp, p);
842
+ }
843
+ function withRmwLock(sessionId, fn) {
844
+ mkdirSync2(STATE_DIR, { recursive: true });
845
+ const rmwLock = statePath(sessionId) + ".rmw";
846
+ const deadline = Date.now() + 2e3;
847
+ let fd = null;
848
+ while (fd === null) {
849
+ try {
850
+ fd = openSync2(rmwLock, "wx");
851
+ } catch (e) {
852
+ if (e.code !== "EEXIST")
853
+ throw e;
854
+ if (Date.now() > deadline) {
855
+ dlog(`rmw lock deadline exceeded for ${sessionId}, reclaiming stale lock`);
856
+ try {
857
+ unlinkSync2(rmwLock);
858
+ } catch (unlinkErr) {
859
+ dlog(`stale rmw lock unlink failed for ${sessionId}: ${unlinkErr.message}`);
860
+ }
861
+ continue;
862
+ }
863
+ Atomics.wait(YIELD_BUF, 0, 0, 10);
864
+ }
865
+ }
866
+ try {
867
+ return fn();
868
+ } finally {
869
+ closeSync2(fd);
870
+ try {
871
+ unlinkSync2(rmwLock);
872
+ } catch (unlinkErr) {
873
+ dlog(`rmw lock cleanup failed for ${sessionId}: ${unlinkErr.message}`);
874
+ }
875
+ }
876
+ }
877
+ function bumpTotalCount(sessionId) {
878
+ return withRmwLock(sessionId, () => {
879
+ const now = Date.now();
880
+ const existing = readState(sessionId);
881
+ const next = existing ? { ...existing, totalCount: existing.totalCount + 1 } : { lastSummaryAt: now, lastSummaryCount: 0, totalCount: 1 };
882
+ writeState(sessionId, next);
883
+ return next;
884
+ });
885
+ }
886
+ function loadTriggerConfig() {
887
+ const n = Number(process.env.HIVEMIND_SUMMARY_EVERY_N_MSGS ?? "");
888
+ const h = Number(process.env.HIVEMIND_SUMMARY_EVERY_HOURS ?? "");
889
+ return {
890
+ everyNMessages: Number.isInteger(n) && n > 0 ? n : 50,
891
+ everyHours: Number.isFinite(h) && h > 0 ? h : 2
892
+ };
893
+ }
894
+ var FIRST_SUMMARY_AT = 10;
895
+ function shouldTrigger(state, cfg, now = Date.now()) {
896
+ const msgsSince = state.totalCount - state.lastSummaryCount;
897
+ if (state.lastSummaryCount === 0 && state.totalCount >= FIRST_SUMMARY_AT)
898
+ return true;
899
+ if (msgsSince >= cfg.everyNMessages)
900
+ return true;
901
+ if (msgsSince > 0 && now - state.lastSummaryAt >= cfg.everyHours * 3600 * 1e3)
902
+ return true;
903
+ return false;
904
+ }
905
+ function tryAcquireLock(sessionId, maxAgeMs = 10 * 60 * 1e3) {
906
+ mkdirSync2(STATE_DIR, { recursive: true });
907
+ const p = lockPath(sessionId);
908
+ if (existsSync4(p)) {
909
+ try {
910
+ const ageMs = Date.now() - parseInt(readFileSync4(p, "utf-8"), 10);
911
+ if (Number.isFinite(ageMs) && ageMs < maxAgeMs)
912
+ return false;
913
+ } catch (readErr) {
914
+ dlog(`lock file unreadable for ${sessionId}, treating as stale: ${readErr.message}`);
915
+ }
916
+ try {
917
+ unlinkSync2(p);
918
+ } catch (unlinkErr) {
919
+ dlog(`could not unlink stale lock for ${sessionId}: ${unlinkErr.message}`);
920
+ return false;
921
+ }
922
+ }
923
+ try {
924
+ const fd = openSync2(p, "wx");
925
+ try {
926
+ writeSync2(fd, String(Date.now()));
927
+ } finally {
928
+ closeSync2(fd);
929
+ }
930
+ return true;
931
+ } catch (e) {
932
+ if (e.code === "EEXIST")
933
+ return false;
934
+ throw e;
935
+ }
936
+ }
937
+ function releaseLock(sessionId) {
938
+ try {
939
+ unlinkSync2(lockPath(sessionId));
940
+ } catch (e) {
941
+ if (e?.code !== "ENOENT") {
942
+ dlog(`releaseLock unlink failed for ${sessionId}: ${e.message}`);
943
+ }
944
+ }
945
+ }
946
+
947
+ // dist/src/hooks/hermes/spawn-wiki-worker.js
948
+ import { spawn as spawn2, execSync } from "node:child_process";
949
+ import { fileURLToPath } from "node:url";
950
+ import { dirname, join as join8 } from "node:path";
951
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
952
+ import { homedir as homedir6, tmpdir as tmpdir2 } from "node:os";
953
+
954
+ // dist/src/utils/wiki-log.js
955
+ import { mkdirSync as mkdirSync3, appendFileSync as appendFileSync2 } from "node:fs";
956
+ import { join as join7 } from "node:path";
957
+ function makeWikiLogger(hooksDir, filename = "deeplake-wiki.log") {
958
+ const path = join7(hooksDir, filename);
959
+ return {
960
+ path,
961
+ log(msg) {
962
+ try {
963
+ mkdirSync3(hooksDir, { recursive: true });
964
+ appendFileSync2(path, `[${utcTimestamp()}] ${msg}
965
+ `);
966
+ } catch {
967
+ }
968
+ }
969
+ };
970
+ }
971
+
972
+ // dist/src/hooks/hermes/spawn-wiki-worker.js
973
+ var HOME = homedir6();
974
+ var wikiLogger = makeWikiLogger(join8(HOME, ".hermes", "hooks"));
975
+ var WIKI_LOG = wikiLogger.path;
976
+ var WIKI_PROMPT_TEMPLATE = `You are building a personal wiki from a coding session. Your goal is to extract every piece of knowledge \u2014 entities, decisions, relationships, and facts \u2014 into a structured, searchable wiki entry.
977
+
978
+ SESSION JSONL path: __JSONL__
979
+ SUMMARY FILE to write: __SUMMARY__
980
+ SESSION ID: __SESSION_ID__
981
+ PROJECT: __PROJECT__
982
+ PREVIOUS JSONL OFFSET (lines already processed): __PREV_OFFSET__
983
+ CURRENT JSONL LINES: __JSONL_LINES__
984
+
985
+ Steps:
986
+ 1. Read the session JSONL at the path above.
987
+ - If PREVIOUS JSONL OFFSET > 0, this is a resumed session. Read the existing summary file first,
988
+ then focus on lines AFTER the offset for new content. Merge new facts into the existing summary.
989
+ - If offset is 0, generate from scratch.
990
+
991
+ 2. Write the summary file at the path above with this EXACT format:
992
+
993
+ # Session __SESSION_ID__
994
+ - **Source**: __JSONL_SERVER_PATH__
995
+ - **Started**: <extract from JSONL>
996
+ - **Ended**: <now>
997
+ - **Project**: __PROJECT__
998
+ - **JSONL offset**: __JSONL_LINES__
999
+
1000
+ ## What Happened
1001
+ <2-3 dense sentences. What was the goal, what was accomplished, what's left.>
1002
+
1003
+ ## People
1004
+ <For each person mentioned: name, role, what they did/said. Format: **Name** \u2014 role \u2014 action>
1005
+
1006
+ ## Entities
1007
+ <Every named thing: repos, branches, files, APIs, tools, services, tables, features, bugs.
1008
+ Format: **entity** (type) \u2014 what was done with it, its current state>
1009
+
1010
+ ## Decisions & Reasoning
1011
+ <Every decision made and WHY.>
1012
+
1013
+ ## Key Facts
1014
+ <Bullet list of atomic facts that could answer future questions.>
1015
+
1016
+ ## Files Modified
1017
+ <bullet list: path (new/modified/deleted) \u2014 what changed>
1018
+
1019
+ ## Open Questions / TODO
1020
+ <Anything unresolved, blocked, or explicitly deferred>
1021
+
1022
+ IMPORTANT: Be exhaustive. Extract EVERY entity, decision, and fact.
1023
+ PRIVACY: Never include absolute filesystem paths in the summary.
1024
+ LENGTH LIMIT: Keep the total summary under 4000 characters.`;
1025
+ var wikiLog = wikiLogger.log;
1026
+ function findHermesBin() {
1027
+ try {
1028
+ return execSync("which hermes 2>/dev/null", { encoding: "utf-8" }).trim() || "hermes";
1029
+ } catch {
1030
+ return "hermes";
1031
+ }
1032
+ }
1033
+ function spawnHermesWikiWorker(opts) {
1034
+ const { config, sessionId, cwd, bundleDir, reason } = opts;
1035
+ const projectName = cwd.split("/").pop() || "unknown";
1036
+ const tmpDir = join8(tmpdir2(), `deeplake-wiki-${sessionId}-${Date.now()}`);
1037
+ mkdirSync4(tmpDir, { recursive: true });
1038
+ const configFile = join8(tmpDir, "config.json");
1039
+ writeFileSync3(configFile, JSON.stringify({
1040
+ apiUrl: config.apiUrl,
1041
+ token: config.token,
1042
+ orgId: config.orgId,
1043
+ workspaceId: config.workspaceId,
1044
+ memoryTable: config.tableName,
1045
+ sessionsTable: config.sessionsTableName,
1046
+ sessionId,
1047
+ userName: config.userName,
1048
+ project: projectName,
1049
+ tmpDir,
1050
+ hermesBin: findHermesBin(),
1051
+ hermesProvider: process.env.HIVEMIND_HERMES_PROVIDER ?? "openrouter",
1052
+ hermesModel: process.env.HIVEMIND_HERMES_MODEL ?? "anthropic/claude-haiku-4-5",
1053
+ wikiLog: WIKI_LOG,
1054
+ hooksDir: join8(HOME, ".hermes", "hooks"),
1055
+ promptTemplate: WIKI_PROMPT_TEMPLATE
1056
+ }));
1057
+ wikiLog(`${reason}: spawning summary worker for ${sessionId}`);
1058
+ const workerPath = join8(bundleDir, "wiki-worker.js");
1059
+ spawn2("nohup", ["node", workerPath, configFile], {
1060
+ detached: true,
1061
+ stdio: ["ignore", "ignore", "ignore"]
1062
+ }).unref();
1063
+ wikiLog(`${reason}: spawned summary worker for ${sessionId}`);
1064
+ }
1065
+ function bundleDirFromImportMeta(importMetaUrl) {
1066
+ return dirname(fileURLToPath(importMetaUrl));
1067
+ }
1068
+
1069
+ // dist/src/hooks/hermes/capture.js
1070
+ var log4 = (msg) => log("hermes-capture", msg);
1071
+ function resolveEmbedDaemonPath() {
1072
+ return join9(dirname2(fileURLToPath2(import.meta.url)), "embeddings", "embed-daemon.js");
1073
+ }
396
1074
  var CAPTURE = process.env.HIVEMIND_CAPTURE !== "false";
397
1075
  function pickString(...candidates) {
398
1076
  for (const c of candidates) {
@@ -407,7 +1085,7 @@ async function main() {
407
1085
  const input = await readStdin();
408
1086
  const config = loadConfig();
409
1087
  if (!config) {
410
- log3("no config");
1088
+ log4("no config");
411
1089
  return;
412
1090
  }
413
1091
  const sessionId = input.session_id ?? `hermes-${Date.now()}`;
@@ -427,14 +1105,14 @@ async function main() {
427
1105
  if (event === "pre_llm_call") {
428
1106
  const prompt = pickString(extra.prompt, extra.user_message, extra.message?.content);
429
1107
  if (!prompt) {
430
- log3(`pre_llm_call: no prompt found in extra`);
1108
+ log4(`pre_llm_call: no prompt found in extra`);
431
1109
  return;
432
1110
  }
433
- log3(`user session=${sessionId}`);
1111
+ log4(`user session=${sessionId}`);
434
1112
  entry = { id: crypto.randomUUID(), ...meta, type: "user_message", content: prompt };
435
1113
  } else if (event === "post_tool_call" && typeof input.tool_name === "string") {
436
1114
  const toolResponse = extra.tool_result ?? extra.tool_output ?? extra.result ?? extra.output;
437
- log3(`tool=${input.tool_name} session=${sessionId}`);
1115
+ log4(`tool=${input.tool_name} session=${sessionId}`);
438
1116
  entry = {
439
1117
  id: crypto.randomUUID(),
440
1118
  ...meta,
@@ -446,36 +1124,71 @@ async function main() {
446
1124
  } else if (event === "post_llm_call") {
447
1125
  const text = pickString(extra.response, extra.assistant_message, extra.message?.content);
448
1126
  if (!text) {
449
- log3(`post_llm_call: no response found in extra`);
1127
+ log4(`post_llm_call: no response found in extra`);
450
1128
  return;
451
1129
  }
452
- log3(`assistant session=${sessionId}`);
1130
+ log4(`assistant session=${sessionId}`);
453
1131
  entry = { id: crypto.randomUUID(), ...meta, type: "assistant_message", content: text };
454
1132
  } else {
455
- log3(`unknown/unhandled event: ${event}, skipping`);
1133
+ log4(`unknown/unhandled event: ${event}, skipping`);
456
1134
  return;
457
1135
  }
458
1136
  const sessionPath = buildSessionPath(config, sessionId);
459
1137
  const line = JSON.stringify(entry);
460
- log3(`writing to ${sessionPath}`);
1138
+ log4(`writing to ${sessionPath}`);
461
1139
  const projectName = cwd.split("/").pop() || "unknown";
462
1140
  const filename = sessionPath.split("/").pop() ?? "";
463
1141
  const jsonForSql = line.replace(/'/g, "''");
464
- const insertSql = `INSERT INTO "${sessionsTable}" (id, path, filename, message, author, size_bytes, project, description, agent, creation_date, last_update_date) VALUES ('${crypto.randomUUID()}', '${sqlStr(sessionPath)}', '${sqlStr(filename)}', '${jsonForSql}'::jsonb, '${sqlStr(config.userName)}', ${Buffer.byteLength(line, "utf-8")}, '${sqlStr(projectName)}', '${sqlStr(event)}', 'hermes', '${ts}', '${ts}')`;
1142
+ const embedding = embeddingsDisabled() ? null : await new EmbedClient({ daemonEntry: resolveEmbedDaemonPath() }).embed(line, "document");
1143
+ const embeddingSql = embeddingSqlLiteral(embedding);
1144
+ const insertSql = `INSERT INTO "${sessionsTable}" (id, path, filename, message, message_embedding, author, size_bytes, project, description, agent, creation_date, last_update_date) VALUES ('${crypto.randomUUID()}', '${sqlStr(sessionPath)}', '${sqlStr(filename)}', '${jsonForSql}'::jsonb, ${embeddingSql}, '${sqlStr(config.userName)}', ${Buffer.byteLength(line, "utf-8")}, '${sqlStr(projectName)}', '${sqlStr(event)}', 'hermes', '${ts}', '${ts}')`;
465
1145
  try {
466
1146
  await api.query(insertSql);
467
1147
  } catch (e) {
468
1148
  if (e.message?.includes("permission denied") || e.message?.includes("does not exist")) {
469
- log3("table missing, creating and retrying");
1149
+ log4("table missing, creating and retrying");
470
1150
  await api.ensureSessionsTable(sessionsTable);
471
1151
  await api.query(insertSql);
472
1152
  } else {
473
1153
  throw e;
474
1154
  }
475
1155
  }
476
- log3("capture ok \u2192 cloud");
1156
+ log4("capture ok \u2192 cloud");
1157
+ maybeTriggerPeriodicSummary(sessionId, cwd, config);
1158
+ }
1159
+ function maybeTriggerPeriodicSummary(sessionId, cwd, config) {
1160
+ if (process.env.HIVEMIND_WIKI_WORKER === "1")
1161
+ return;
1162
+ try {
1163
+ const state = bumpTotalCount(sessionId);
1164
+ const cfg = loadTriggerConfig();
1165
+ if (!shouldTrigger(state, cfg))
1166
+ return;
1167
+ if (!tryAcquireLock(sessionId)) {
1168
+ log4(`periodic trigger suppressed (lock held) session=${sessionId}`);
1169
+ return;
1170
+ }
1171
+ wikiLog(`Periodic: threshold hit (total=${state.totalCount}, since=${state.totalCount - state.lastSummaryCount}, N=${cfg.everyNMessages}, hours=${cfg.everyHours})`);
1172
+ try {
1173
+ spawnHermesWikiWorker({
1174
+ config,
1175
+ sessionId,
1176
+ cwd,
1177
+ bundleDir: bundleDirFromImportMeta(import.meta.url),
1178
+ reason: "Periodic"
1179
+ });
1180
+ } catch (e) {
1181
+ log4(`periodic spawn failed: ${e.message}`);
1182
+ try {
1183
+ releaseLock(sessionId);
1184
+ } catch {
1185
+ }
1186
+ }
1187
+ } catch (e) {
1188
+ log4(`periodic trigger error: ${e.message}`);
1189
+ }
477
1190
  }
478
1191
  main().catch((e) => {
479
- log3(`fatal: ${e.message}`);
1192
+ log4(`fatal: ${e.message}`);
480
1193
  process.exit(0);
481
1194
  });