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