@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
@@ -52,5 +52,5 @@
52
52
  }
53
53
  }
54
54
  },
55
- "version": "0.6.48"
55
+ "version": "0.7.4"
56
56
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeplake/hivemind",
3
- "version": "0.6.48",
3
+ "version": "0.7.4",
4
4
  "type": "module",
5
5
  "description": "Hivemind — cloud-backed persistent shared memory for AI agents, powered by DeepLake",
6
6
  "license": "Apache-2.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeplake/hivemind",
3
- "version": "0.6.48",
3
+ "version": "0.7.4",
4
4
  "description": "Cloud-backed persistent shared memory for AI agents powered by Deeplake",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -46,6 +46,7 @@
46
46
  "*.md": []
47
47
  },
48
48
  "dependencies": {
49
+ "@huggingface/transformers": "^3.0.0",
49
50
  "@modelcontextprotocol/sdk": "^1.29.0",
50
51
  "deeplake": "^0.3.30",
51
52
  "js-yaml": "^4.1.1",
@@ -24,9 +24,34 @@
24
24
  // available to pi's compiler when this is loaded.
25
25
 
26
26
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
27
- import { readFileSync, existsSync } from "node:fs";
28
- import { homedir } from "node:os";
29
- import { join } from "node:path";
27
+ import {
28
+ readFileSync, existsSync, appendFileSync, mkdirSync, writeFileSync,
29
+ openSync, closeSync, constants as fsConstants,
30
+ } from "node:fs";
31
+ import { homedir, tmpdir } from "node:os";
32
+ import { join, dirname } from "node:path";
33
+ import { connect } from "node:net";
34
+ import { spawn, execSync } from "node:child_process";
35
+
36
+ // ---------- diagnostic logging --------------------------------------------------
37
+ //
38
+ // The capture path is fully async + swallows errors (writeSessionRow's catch
39
+ // is intentionally non-fatal, so a transient deeplake outage never breaks pi).
40
+ // That means a buggy extension is silent: rows just don't appear, with no
41
+ // indication where things went wrong. When HIVEMIND_DEBUG=1 we dump a
42
+ // breadcrumb to ~/.deeplake/hivemind-pi.log at every meaningful step so the
43
+ // failure mode is observable. Off by default to keep `pi` quiet for normal
44
+ // users.
45
+
46
+ const LOG_PATH = join(homedir(), ".deeplake", "hivemind-pi.log");
47
+
48
+ function logHm(msg: string): void {
49
+ if (process.env.HIVEMIND_DEBUG !== "1") return;
50
+ try {
51
+ mkdirSync(dirname(LOG_PATH), { recursive: true });
52
+ appendFileSync(LOG_PATH, `${new Date().toISOString()} [pi] ${msg}\n`);
53
+ } catch { /* logging must never break the agent */ }
54
+ }
30
55
 
31
56
  // ---------- credentials / config -----------------------------------------------
32
57
 
@@ -111,6 +136,322 @@ async function dlQuery(creds: Creds, sql: string): Promise<unknown[]> {
111
136
  return json.rows.map((r) => Object.fromEntries(json.columns!.map((c, i) => [c, r[i]])));
112
137
  }
113
138
 
139
+ // ---------- embedding client (inline; reuses the shared daemon) ----------------
140
+ //
141
+ // Pi avoids importing EmbedClient (which is bundled into other agents but
142
+ // here would break the "raw .ts, zero deps" promise of pi extensions).
143
+ // Instead we open a Unix socket directly to the daemon at the same well-known
144
+ // path EmbedClient uses. If the socket isn't there yet, we spawn the
145
+ // canonical daemon at ~/.hivemind/embed-deps/embed-daemon.js (deposited by
146
+ // `hivemind embeddings install`) and wait for it to listen, mirroring the
147
+ // auto-spawn-on-miss logic in src/embeddings/client.ts. Subsequent agents
148
+ // (codex, CC, cursor, hermes, …) connect to the SAME daemon — pi pays the
149
+ // cold-start cost only when it's the first user on the box.
150
+ //
151
+ // Graceful fallback: any failure → return null → caller writes NULL into
152
+ // message_embedding. Embedding is never on the critical path.
153
+
154
+ const EMBED_DAEMON_ENTRY = join(homedir(), ".hivemind", "embed-deps", "embed-daemon.js");
155
+ const EMBED_SOCKET_PATH = (() => {
156
+ const uid = typeof process.getuid === "function" ? String(process.getuid()) : (process.env.USER ?? "default");
157
+ return `/tmp/hivemind-embed-${uid}.sock`;
158
+ })();
159
+
160
+ function tryEmbedOverSocket(text: string, kind: "document" | "query"): Promise<number[] | null> {
161
+ return new Promise((resolve) => {
162
+ let resolved = false;
163
+ const settle = (v: number[] | null) => { if (!resolved) { resolved = true; resolve(v); } };
164
+ const sock = connect(EMBED_SOCKET_PATH);
165
+ let buf = "";
166
+ const timer = setTimeout(() => { sock.destroy(); settle(null); }, 5000);
167
+ sock.on("connect", () => {
168
+ // Protocol shape comes from src/embeddings/protocol.ts: {op, id, kind, text}.
169
+ // id is a string ("1"), not a number, and the verb field is "op" not "type".
170
+ sock.write(JSON.stringify({ op: "embed", id: "1", kind, text }) + "\n");
171
+ });
172
+ sock.on("data", (chunk: Buffer) => {
173
+ buf += chunk.toString("utf-8");
174
+ const nl = buf.indexOf("\n");
175
+ if (nl !== -1) {
176
+ clearTimeout(timer);
177
+ try {
178
+ const resp = JSON.parse(buf.slice(0, nl));
179
+ settle(Array.isArray(resp.embedding) ? resp.embedding : null);
180
+ } catch { settle(null); }
181
+ sock.destroy();
182
+ }
183
+ });
184
+ sock.on("error", () => { clearTimeout(timer); settle(null); });
185
+ sock.on("close", () => { clearTimeout(timer); settle(null); });
186
+ });
187
+ }
188
+
189
+ // ---------- summary state + wiki-worker spawn ---------------------------------
190
+ //
191
+ // Mirror of src/hooks/summary-state.ts (same dir, same JSON shape, shared
192
+ // across CC/codex/cursor/hermes — session ids are UUIDs so collisions are
193
+ // impossible). The pi extension increments totalCount on every captured
194
+ // event and spawns the bundled wiki-worker (see pi/bundle/wiki-worker.js)
195
+ // when the threshold is hit. The worker, after generating the summary,
196
+ // calls finalizeSummary() / releaseLock() against this same dir. So the
197
+ // extension and the worker share state.
198
+
199
+ const SUMMARY_STATE_DIR = join(homedir(), ".claude", "hooks", "summary-state");
200
+ const PI_WIKI_WORKER_PATH = join(homedir(), ".pi", "agent", "hivemind", "wiki-worker.js");
201
+
202
+ interface SummaryState {
203
+ lastSummaryAt: number;
204
+ lastSummaryCount: number;
205
+ totalCount: number;
206
+ }
207
+ interface SummaryConfig {
208
+ everyNMessages: number;
209
+ everyHours: number;
210
+ }
211
+
212
+ function summaryStatePath(sessionId: string): string {
213
+ return join(SUMMARY_STATE_DIR, `${sessionId}.json`);
214
+ }
215
+ function summaryLockPath(sessionId: string): string {
216
+ return join(SUMMARY_STATE_DIR, `${sessionId}.lock`);
217
+ }
218
+
219
+ function loadSummaryConfig(): SummaryConfig {
220
+ const n = Number(process.env.HIVEMIND_SUMMARY_EVERY_N_MSGS ?? "");
221
+ const h = Number(process.env.HIVEMIND_SUMMARY_EVERY_HOURS ?? "");
222
+ return {
223
+ everyNMessages: Number.isInteger(n) && n > 0 ? n : 50,
224
+ everyHours: Number.isFinite(h) && h > 0 ? h : 2,
225
+ };
226
+ }
227
+
228
+ // Mirrors src/hooks/summary-state.ts — the very first summary fires at
229
+ // totalCount=10 (vs the steady-state N=50) so a fresh chat gets indexed
230
+ // quickly without waiting for ~50 messages.
231
+ const FIRST_SUMMARY_AT = 10;
232
+
233
+ function readSummaryState(sessionId: string): SummaryState | null {
234
+ try {
235
+ const p = summaryStatePath(sessionId);
236
+ if (!existsSync(p)) return null;
237
+ const raw = JSON.parse(readFileSync(p, "utf-8"));
238
+ return {
239
+ lastSummaryAt: Number(raw.lastSummaryAt) || 0,
240
+ lastSummaryCount: Number(raw.lastSummaryCount) || 0,
241
+ totalCount: Number(raw.totalCount) || 0,
242
+ };
243
+ } catch { return null; }
244
+ }
245
+
246
+ function writeSummaryState(sessionId: string, state: SummaryState): void {
247
+ try {
248
+ mkdirSync(SUMMARY_STATE_DIR, { recursive: true });
249
+ writeFileSync(summaryStatePath(sessionId), JSON.stringify(state));
250
+ } catch { /* non-fatal */ }
251
+ }
252
+
253
+ function bumpCounter(sessionId: string): SummaryState {
254
+ const cur = readSummaryState(sessionId) ?? { lastSummaryAt: 0, lastSummaryCount: 0, totalCount: 0 };
255
+ cur.totalCount += 1;
256
+ writeSummaryState(sessionId, cur);
257
+ return cur;
258
+ }
259
+
260
+ function shouldTriggerNow(state: SummaryState, cfg: SummaryConfig): boolean {
261
+ const msgsSince = state.totalCount - state.lastSummaryCount;
262
+ // First-chat trigger: index a fresh session quickly (10 events) instead of
263
+ // waiting until N=50. Mirrors summary-state.ts in CC/codex.
264
+ if (state.lastSummaryCount === 0 && state.totalCount >= FIRST_SUMMARY_AT) return true;
265
+ if (msgsSince >= cfg.everyNMessages) return true;
266
+ if (msgsSince > 0 && state.lastSummaryAt > 0
267
+ && Date.now() - state.lastSummaryAt >= cfg.everyHours * 3600 * 1000) return true;
268
+ return false;
269
+ }
270
+
271
+ function tryAcquireSummaryLock(sessionId: string): boolean {
272
+ try {
273
+ mkdirSync(SUMMARY_STATE_DIR, { recursive: true });
274
+ const fd = openSync(summaryLockPath(sessionId),
275
+ fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY);
276
+ closeSync(fd);
277
+ return true;
278
+ } catch { return false; }
279
+ }
280
+
281
+ function findPiBin(): string {
282
+ try {
283
+ const out = execSync("which pi 2>/dev/null", { encoding: "utf-8" }).trim();
284
+ if (out) return out;
285
+ } catch { /* fall through */ }
286
+ return "pi";
287
+ }
288
+
289
+ // Same template the CC/codex spawn-wiki-worker.ts ships. Inlined here
290
+ // because the pi extension is raw .ts and can't import it.
291
+ const WIKI_PROMPT_TEMPLATE = `You are building a personal wiki from a coding session. Your goal is to extract every piece of knowledge — entities, decisions, relationships, and facts — into a structured, searchable wiki entry.
292
+
293
+ SESSION JSONL path: __JSONL__
294
+ SUMMARY FILE to write: __SUMMARY__
295
+ SESSION ID: __SESSION_ID__
296
+ PROJECT: __PROJECT__
297
+ PREVIOUS JSONL OFFSET (lines already processed): __PREV_OFFSET__
298
+ CURRENT JSONL LINES: __JSONL_LINES__
299
+
300
+ Steps:
301
+ 1. Read the session JSONL at the path above.
302
+ - If PREVIOUS JSONL OFFSET > 0, this is a resumed session. Read the existing summary file first,
303
+ then focus on lines AFTER the offset for new content. Merge new facts into the existing summary.
304
+ - If offset is 0, generate from scratch.
305
+
306
+ 2. Write the summary file at the path above with this EXACT format:
307
+
308
+ # Session __SESSION_ID__
309
+ - **Source**: __JSONL_SERVER_PATH__
310
+ - **Started**: <extract from JSONL>
311
+ - **Ended**: <now>
312
+ - **Project**: __PROJECT__
313
+ - **JSONL offset**: __JSONL_LINES__
314
+
315
+ ## What Happened
316
+ <2-3 dense sentences. What was the goal, what was accomplished, what's left.>
317
+
318
+ ## People
319
+ <For each person mentioned: name, role, what they did/said. Format: **Name** — role — action>
320
+
321
+ ## Entities
322
+ <Every named thing: repos, branches, files, APIs, tools, services, tables, features, bugs.
323
+ Format: **entity** (type) — what was done with it, its current state>
324
+
325
+ ## Decisions & Reasoning
326
+ <Every decision made and WHY.>
327
+
328
+ ## Key Facts
329
+ <Bullet list of atomic facts that could answer future questions.>
330
+
331
+ ## Files Modified
332
+ <bullet list: path (new/modified/deleted) — what changed>
333
+
334
+ ## Open Questions / TODO
335
+ <Anything unresolved, blocked, or explicitly deferred>
336
+
337
+ IMPORTANT: Be exhaustive. Extract EVERY entity, decision, and fact.
338
+ PRIVACY: Never include absolute filesystem paths in the summary.
339
+ LENGTH LIMIT: Keep the total summary under 4000 characters.`;
340
+
341
+ function spawnWikiWorker(
342
+ creds: Creds,
343
+ sessionId: string,
344
+ cwd: string,
345
+ reason: "periodic" | "final",
346
+ ): void {
347
+ if (!existsSync(PI_WIKI_WORKER_PATH)) {
348
+ logHm(`spawnWikiWorker(${reason}): no worker at ${PI_WIKI_WORKER_PATH} — install via 'hivemind pi install' or rebuild`);
349
+ return;
350
+ }
351
+ // Periodic: only one in-flight; lock prevents races between events.
352
+ // Final: also takes the lock — if a periodic was mid-flight at session_shutdown,
353
+ // skip the final to avoid two concurrent workers writing back to the same row.
354
+ if (!tryAcquireSummaryLock(sessionId)) {
355
+ logHm(`spawnWikiWorker(${reason}): lock held — skipping (a worker is already running)`);
356
+ return;
357
+ }
358
+ // tmp dir owned by the worker; it removes it on completion.
359
+ const tmpDir = join(tmpdir(), `deeplake-wiki-${sessionId}-${Date.now()}`);
360
+ try { mkdirSync(tmpDir, { recursive: true }); } catch { /* ignore */ }
361
+ const configPath = join(tmpDir, "config.json");
362
+ const project = (cwd ?? "").split("/").pop() || "unknown";
363
+ const config = {
364
+ apiUrl: creds.apiUrl,
365
+ token: creds.token,
366
+ orgId: creds.orgId,
367
+ workspaceId: creds.workspaceId,
368
+ memoryTable: MEMORY_TABLE,
369
+ sessionsTable: SESSIONS_TABLE,
370
+ sessionId,
371
+ userName: creds.userName,
372
+ project,
373
+ tmpDir,
374
+ piBin: findPiBin(),
375
+ piProvider: process.env.HIVEMIND_PI_PROVIDER ?? "google",
376
+ piModel: process.env.HIVEMIND_PI_MODEL ?? "gemini-2.5-flash",
377
+ wikiLog: join(homedir(), ".deeplake", "hivemind-pi.log"),
378
+ hooksDir: join(homedir(), ".pi", "agent", "hivemind"),
379
+ promptTemplate: WIKI_PROMPT_TEMPLATE,
380
+ };
381
+ try { writeFileSync(configPath, JSON.stringify(config)); }
382
+ catch (e: any) { logHm(`spawnWikiWorker(${reason}): writeFileSync failed: ${e?.message ?? e}`); return; }
383
+ logHm(`spawnWikiWorker(${reason}): spawning ${PI_WIKI_WORKER_PATH} session=${sessionId} provider=${config.piProvider} model=${config.piModel}`);
384
+ try {
385
+ spawn(process.execPath, [PI_WIKI_WORKER_PATH, configPath], {
386
+ detached: true,
387
+ stdio: "ignore",
388
+ env: { ...process.env, HIVEMIND_WIKI_WORKER: "1", HIVEMIND_CAPTURE: "false" },
389
+ }).unref();
390
+ } catch (e: any) {
391
+ logHm(`spawnWikiWorker(${reason}): spawn failed: ${e?.message ?? e}`);
392
+ }
393
+ }
394
+
395
+ function maybeTriggerPeriodicSummary(creds: Creds, sessionId: string, cwd: string): void {
396
+ if (process.env.HIVEMIND_CAPTURE === "false") return;
397
+ const state = bumpCounter(sessionId);
398
+ const cfg = loadSummaryConfig();
399
+ if (!shouldTriggerNow(state, cfg)) return;
400
+ logHm(`periodic threshold hit (total=${state.totalCount}, since=${state.totalCount - state.lastSummaryCount}, N=${cfg.everyNMessages}, hours=${cfg.everyHours})`);
401
+ spawnWikiWorker(creds, sessionId, cwd, "periodic");
402
+ }
403
+
404
+ async function embed(text: string): Promise<number[] | null> {
405
+ if (process.env.HIVEMIND_EMBEDDINGS === "false") {
406
+ logHm(`embed: skipped (HIVEMIND_EMBEDDINGS=false)`);
407
+ return null;
408
+ }
409
+ if (!text || text.length === 0) {
410
+ logHm(`embed: skipped (empty text)`);
411
+ return null;
412
+ }
413
+ // 1) socket already up (another agent or us in a previous turn) → fast path
414
+ let v = await tryEmbedOverSocket(text, "document");
415
+ if (v !== null) {
416
+ logHm(`embed: ok via existing socket (dims=${v.length})`);
417
+ return v;
418
+ }
419
+ // 2) no daemon binary deposited → fallback NULL
420
+ if (!existsSync(EMBED_DAEMON_ENTRY)) {
421
+ logHm(`embed: no daemon at ${EMBED_DAEMON_ENTRY} — run 'hivemind embeddings install'`);
422
+ return null;
423
+ }
424
+ // 3) spawn the canonical daemon detached; daemon's own pidfile lock guards
425
+ // against double-spawn if multiple pi turns race.
426
+ logHm(`embed: spawning daemon at ${EMBED_DAEMON_ENTRY}`);
427
+ try {
428
+ spawn(process.execPath, [EMBED_DAEMON_ENTRY], { detached: true, stdio: "ignore" }).unref();
429
+ } catch (e: any) {
430
+ logHm(`embed: spawn failed: ${e?.message ?? e}`);
431
+ return null;
432
+ }
433
+ // 4) poll for the socket up to ~5s, then retry the embed once
434
+ for (let i = 0; i < 25; i++) {
435
+ await new Promise(r => setTimeout(r, 200));
436
+ if (existsSync(EMBED_SOCKET_PATH)) {
437
+ v = await tryEmbedOverSocket(text, "document");
438
+ if (v !== null) {
439
+ logHm(`embed: ok after spawn (dims=${v.length}, polls=${i + 1})`);
440
+ return v;
441
+ }
442
+ }
443
+ }
444
+ logHm(`embed: timed out after spawn (5s)`);
445
+ return null;
446
+ }
447
+
448
+ function embedSqlLiteral(emb: number[] | null): string {
449
+ if (!emb || emb.length === 0) return "NULL";
450
+ // FLOAT4[] literal. Numbers serialize without quotes; emb is a plain
451
+ // number[] from the daemon so JSON-style join is safe.
452
+ return `ARRAY[${emb.join(",")}]::FLOAT4[]`;
453
+ }
454
+
114
455
  // ---------- session-row writer -------------------------------------------------
115
456
 
116
457
  function buildSessionPath(creds: Creds, sessionId: string): string {
@@ -118,6 +459,13 @@ function buildSessionPath(creds: Creds, sessionId: string): string {
118
459
  return `/sessions/${creds.userName}/${filename}`;
119
460
  }
120
461
 
462
+ // Deeplake quirk: CREATE TABLE IF NOT EXISTS returns 200 before the table
463
+ // is queryable for INSERTs (the propagation can take 30+ seconds on a fresh
464
+ // table). Other agents don't hit this in steady state because they reuse
465
+ // existing tables; pi's e2e tests use fresh timestamped tables every run.
466
+ // Fix: tolerate "Table does not exist" specifically and retry with backoff.
467
+ const INSERT_RETRY_BACKOFFS_MS = [1000, 3000, 8000, 15000];
468
+
121
469
  async function writeSessionRow(
122
470
  creds: Creds,
123
471
  sessionId: string,
@@ -132,11 +480,33 @@ async function writeSessionRow(
132
480
  const projectName = (cwd ?? "").split("/").pop() || "unknown";
133
481
  const line = JSON.stringify(entry);
134
482
  const jsonForSql = sqlJsonb(line);
483
+ logHm(`writeSessionRow: event=${event} session=${sessionId} bytes=${line.length} table=${SESSIONS_TABLE}`);
484
+ const emb = await embed(line);
485
+ logHm(`writeSessionRow: embed=${emb ? `dims=${emb.length}` : "null"}`);
135
486
  const insertSql =
136
- `INSERT INTO "${SESSIONS_TABLE}" (id, path, filename, message, author, size_bytes, project, description, agent, creation_date, last_update_date) ` +
137
- `VALUES ('${crypto.randomUUID()}', '${sqlStr(sessionPath)}', '${sqlStr(filename)}', '${jsonForSql}'::jsonb, '${sqlStr(creds.userName)}', ` +
487
+ `INSERT INTO "${SESSIONS_TABLE}" (id, path, filename, message, message_embedding, author, size_bytes, project, description, agent, creation_date, last_update_date) ` +
488
+ `VALUES ('${crypto.randomUUID()}', '${sqlStr(sessionPath)}', '${sqlStr(filename)}', '${jsonForSql}'::jsonb, ${embedSqlLiteral(emb)}, '${sqlStr(creds.userName)}', ` +
138
489
  `${Buffer.byteLength(line, "utf-8")}, '${sqlStr(projectName)}', '${sqlStr(event)}', '${agent}', '${ts}', '${ts}')`;
139
- await dlQuery(creds, insertSql);
490
+ let lastErr: any = null;
491
+ for (let attempt = 0; attempt <= INSERT_RETRY_BACKOFFS_MS.length; attempt++) {
492
+ try {
493
+ await dlQuery(creds, insertSql);
494
+ logHm(`writeSessionRow: INSERT ok (event=${event}, attempt=${attempt + 1})`);
495
+ return;
496
+ } catch (e: any) {
497
+ lastErr = e;
498
+ const msg = e?.message ?? String(e);
499
+ const isPropagationDelay = /table does not exist|relation .* does not exist/i.test(msg);
500
+ if (!isPropagationDelay || attempt === INSERT_RETRY_BACKOFFS_MS.length) {
501
+ logHm(`writeSessionRow: INSERT FAILED (event=${event}, attempt=${attempt + 1}): ${msg}`);
502
+ throw e;
503
+ }
504
+ const delay = INSERT_RETRY_BACKOFFS_MS[attempt];
505
+ logHm(`writeSessionRow: table not yet visible, retrying in ${delay}ms (attempt=${attempt + 1}/${INSERT_RETRY_BACKOFFS_MS.length + 1})`);
506
+ await new Promise(r => setTimeout(r, delay));
507
+ }
508
+ }
509
+ throw lastErr;
140
510
  }
141
511
 
142
512
  // ---------- search primitive (used by hivemind_search) -------------------------
@@ -267,7 +637,62 @@ export default function hivemindExtension(pi: ExtensionAPI): void {
267
637
  // themselves don't carry them.
268
638
 
269
639
  pi.on("session_start", async (_event: any, _ctx: any) => {
640
+ logHm(`session_start: fired (capture=${captureEnabled}, embed=${process.env.HIVEMIND_EMBEDDINGS !== "false"}, table=${SESSIONS_TABLE})`);
270
641
  const creds = loadCreds();
642
+ if (!creds) {
643
+ logHm(`session_start: no credentials at ~/.deeplake/credentials.json — capture disabled this session`);
644
+ } else {
645
+ logHm(`session_start: creds org=${creds.orgName ?? creds.orgId} ws=${creds.workspaceId}`);
646
+ }
647
+ if (creds && captureEnabled) {
648
+ // Other agents' session-start hooks create the memory + sessions tables
649
+ // via DeeplakeApi.ensureTable / ensureSessionsTable. The pi extension is
650
+ // standalone (no shared lib import to keep it raw-.ts), so we issue the
651
+ // CREATE TABLE IF NOT EXISTS directly. Schema matches the canonical one
652
+ // in src/deeplake-api.ts so all agents read/write the same shape.
653
+ const memCreate = `CREATE TABLE IF NOT EXISTS "${MEMORY_TABLE}" (` +
654
+ `id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', ` +
655
+ `filename TEXT NOT NULL DEFAULT '', summary TEXT NOT NULL DEFAULT '', ` +
656
+ `summary_embedding FLOAT4[], author TEXT NOT NULL DEFAULT '', ` +
657
+ `mime_type TEXT NOT NULL DEFAULT 'text/plain', size_bytes BIGINT NOT NULL DEFAULT 0, ` +
658
+ `project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', ` +
659
+ `agent TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', ` +
660
+ `last_update_date TEXT NOT NULL DEFAULT ''` +
661
+ `) USING deeplake`;
662
+ const sessCreate = `CREATE TABLE IF NOT EXISTS "${SESSIONS_TABLE}" (` +
663
+ `id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', ` +
664
+ `filename TEXT NOT NULL DEFAULT '', message JSONB, message_embedding FLOAT4[], ` +
665
+ `author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'application/json', ` +
666
+ `size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', ` +
667
+ `description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', ` +
668
+ `creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT ''` +
669
+ `) USING deeplake`;
670
+ try { await dlQuery(creds, memCreate); logHm(`session_start: memory CREATE TABLE ok (${MEMORY_TABLE})`); }
671
+ catch (e: any) { logHm(`session_start: memory CREATE failed: ${e?.message ?? e}`); }
672
+ try { await dlQuery(creds, sessCreate); logHm(`session_start: sessions CREATE TABLE ok (${SESSIONS_TABLE})`); }
673
+ catch (e: any) { logHm(`session_start: sessions CREATE failed: ${e?.message ?? e}`); }
674
+ // Proactively poll until the sessions table is queryable. CREATE TABLE
675
+ // returns 200 before propagation completes on Deeplake; the first INSERT
676
+ // can otherwise fail with "Table does not exist" for ~30s. Polling here
677
+ // amortises the delay before any event fires.
678
+ const probeSql = `SELECT 1 FROM "${SESSIONS_TABLE}" LIMIT 1`;
679
+ const start = Date.now();
680
+ let visible = false;
681
+ for (let i = 0; i < 30 && !visible; i++) {
682
+ try {
683
+ await dlQuery(creds, probeSql);
684
+ visible = true;
685
+ } catch (e: any) {
686
+ const msg = e?.message ?? String(e);
687
+ if (!/table does not exist|relation .* does not exist/i.test(msg)) {
688
+ logHm(`session_start: probe failed (non-propagation): ${msg}`);
689
+ break;
690
+ }
691
+ await new Promise(r => setTimeout(r, 1000));
692
+ }
693
+ }
694
+ logHm(`session_start: sessions table visible=${visible} (probe took ${Date.now() - start}ms)`);
695
+ }
271
696
  const additional = creds
272
697
  ? `${CONTEXT_PREAMBLE}\nLogged in to Deeplake as org: ${creds.orgName ?? creds.orgId} (workspace: ${creds.workspaceId}).`
273
698
  : `${CONTEXT_PREAMBLE}\nNot logged in to Deeplake. Run \`hivemind login\` to authenticate.`;
@@ -275,12 +700,13 @@ export default function hivemindExtension(pi: ExtensionAPI): void {
275
700
  });
276
701
 
277
702
  pi.on("input", async (event: any, ctx: any) => {
278
- if (!captureEnabled) return;
279
- if (event.source === "extension") return; // skip our own injected inputs
703
+ logHm(`input: fired source=${event?.source ?? "?"}`);
704
+ if (!captureEnabled) { logHm(`input: capture disabled, skipping`); return; }
705
+ if (event.source === "extension") { logHm(`input: extension-injected, skipping`); return; }
280
706
  const creds = loadCreds();
281
- if (!creds) return;
707
+ if (!creds) { logHm(`input: no creds, skipping`); return; }
282
708
  const text = typeof event.text === "string" ? event.text : "";
283
- if (!text) return;
709
+ if (!text) { logHm(`input: empty text, skipping`); return; }
284
710
  const sessionId = ctx?.sessionManager?.getSessionId?.() ?? `pi-${Date.now()}`;
285
711
  const cwd = ctx?.cwd ?? ctx?.sessionManager?.getCwd?.() ?? process.cwd();
286
712
  try {
@@ -291,13 +717,17 @@ export default function hivemindExtension(pi: ExtensionAPI): void {
291
717
  content: text,
292
718
  timestamp: new Date().toISOString(),
293
719
  });
294
- } catch { /* non-fatal */ }
720
+ } catch (e: any) {
721
+ logHm(`input: writeSessionRow swallowed: ${e?.message ?? e}`);
722
+ }
723
+ maybeTriggerPeriodicSummary(creds, sessionId, cwd);
295
724
  });
296
725
 
297
726
  pi.on("tool_result", async (event: any, ctx: any) => {
298
- if (!captureEnabled) return;
727
+ logHm(`tool_result: fired tool=${event?.toolName ?? "?"} isError=${event?.isError === true}`);
728
+ if (!captureEnabled) { logHm(`tool_result: capture disabled, skipping`); return; }
299
729
  const creds = loadCreds();
300
- if (!creds) return;
730
+ if (!creds) { logHm(`tool_result: no creds, skipping`); return; }
301
731
  const sessionId = ctx?.sessionManager?.getSessionId?.() ?? `pi-${Date.now()}`;
302
732
  const cwd = ctx?.cwd ?? ctx?.sessionManager?.getCwd?.() ?? process.cwd();
303
733
  // event.content is (TextContent | ImageContent)[]; extract text blocks.
@@ -318,24 +748,31 @@ export default function hivemindExtension(pi: ExtensionAPI): void {
318
748
  is_error: event.isError === true,
319
749
  timestamp: new Date().toISOString(),
320
750
  });
321
- } catch { /* non-fatal */ }
751
+ } catch (e: any) {
752
+ logHm(`tool_result: writeSessionRow swallowed: ${e?.message ?? e}`);
753
+ }
754
+ maybeTriggerPeriodicSummary(creds, sessionId, cwd);
322
755
  });
323
756
 
324
757
  pi.on("message_end", async (event: any, ctx: any) => {
325
- if (!captureEnabled) return;
758
+ logHm(`message_end: fired role=${event?.message?.role ?? "?"}`);
759
+ if (!captureEnabled) { logHm(`message_end: capture disabled, skipping`); return; }
326
760
  const creds = loadCreds();
327
- if (!creds) return;
761
+ if (!creds) { logHm(`message_end: no creds, skipping`); return; }
328
762
  const message = event.message ?? null;
329
763
  // AgentMessage is UserMessage | AssistantMessage | ToolResultMessage.
330
764
  // user is captured via `input`; toolResult via `tool_result`. Only assistant here.
331
- if (!message || message.role !== "assistant") return;
765
+ if (!message || message.role !== "assistant") {
766
+ logHm(`message_end: skipping (role=${message?.role ?? "null"} — only assistant rows are written here)`);
767
+ return;
768
+ }
332
769
  // AssistantMessage.content is (TextContent | ThinkingContent | ToolCall)[].
333
770
  const blocks: any[] = Array.isArray(message.content) ? message.content : [];
334
771
  const text = blocks
335
772
  .filter((b: any) => b?.type === "text" && typeof b.text === "string")
336
773
  .map((b: any) => b.text)
337
774
  .join("\n");
338
- if (!text) return;
775
+ if (!text) { logHm(`message_end: assistant message had no text blocks, skipping`); return; }
339
776
  const sessionId = ctx?.sessionManager?.getSessionId?.() ?? `pi-${Date.now()}`;
340
777
  const cwd = ctx?.cwd ?? ctx?.sessionManager?.getCwd?.() ?? process.cwd();
341
778
  try {
@@ -346,10 +783,25 @@ export default function hivemindExtension(pi: ExtensionAPI): void {
346
783
  content: text,
347
784
  timestamp: new Date().toISOString(),
348
785
  });
349
- } catch { /* non-fatal */ }
786
+ } catch (e: any) {
787
+ logHm(`message_end: writeSessionRow swallowed: ${e?.message ?? e}`);
788
+ }
789
+ maybeTriggerPeriodicSummary(creds, sessionId, cwd);
350
790
  });
351
791
 
352
- pi.on("session_shutdown", async (_event: any, _ctx: any) => {
353
- // No-op for now. Future: trigger wiki-worker for AI summary.
792
+ pi.on("session_shutdown", async (_event: any, ctx: any) => {
793
+ logHm(`session_shutdown: fired`);
794
+ if (process.env.HIVEMIND_CAPTURE === "false") return;
795
+ const creds = loadCreds();
796
+ if (!creds) { logHm(`session_shutdown: no creds, skipping final summary`); return; }
797
+ const sessionId = ctx?.sessionManager?.getSessionId?.() ?? null;
798
+ if (!sessionId) { logHm(`session_shutdown: no sessionId, skipping final summary`); return; }
799
+ const cwd = ctx?.cwd ?? ctx?.sessionManager?.getCwd?.() ?? process.cwd();
800
+ // Always spawn for "final" — but the lock check inside spawnWikiWorker
801
+ // skips if a periodic worker is mid-flight. Non-fatal either way.
802
+ spawnWikiWorker(creds, sessionId, cwd, "final");
354
803
  });
804
+
805
+ // Module-load breadcrumb so we know the extension's default export ran at all.
806
+ logHm(`extension loaded (table=${SESSIONS_TABLE}, mem=${MEMORY_TABLE})`);
355
807
  }