@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +147 -20
- package/bundle/cli.js +552 -95
- package/codex/bundle/capture.js +509 -89
- package/codex/bundle/commands/auth-login.js +209 -66
- package/codex/bundle/embeddings/embed-daemon.js +243 -0
- package/codex/bundle/pre-tool-use.js +629 -104
- package/codex/bundle/session-start-setup.js +194 -57
- package/codex/bundle/session-start.js +25 -10
- package/codex/bundle/shell/deeplake-shell.js +679 -112
- package/codex/bundle/stop.js +476 -58
- package/codex/bundle/wiki-worker.js +312 -11
- package/cursor/bundle/capture.js +768 -57
- package/cursor/bundle/commands/auth-login.js +209 -66
- package/cursor/bundle/embeddings/embed-daemon.js +243 -0
- package/cursor/bundle/pre-tool-use.js +561 -70
- package/cursor/bundle/session-end.js +223 -2
- package/cursor/bundle/session-start.js +192 -54
- package/cursor/bundle/shell/deeplake-shell.js +679 -112
- package/cursor/bundle/wiki-worker.js +571 -0
- package/hermes/bundle/capture.js +771 -58
- package/hermes/bundle/commands/auth-login.js +209 -66
- package/hermes/bundle/embeddings/embed-daemon.js +243 -0
- package/hermes/bundle/pre-tool-use.js +560 -69
- package/hermes/bundle/session-end.js +224 -1
- package/hermes/bundle/session-start.js +195 -54
- package/hermes/bundle/shell/deeplake-shell.js +679 -112
- package/hermes/bundle/wiki-worker.js +572 -0
- package/mcp/bundle/server.js +253 -68
- package/openclaw/dist/chunks/auth-creds-AEKS6D3P.js +14 -0
- package/openclaw/dist/chunks/chunk-SRCBBT4H.js +37 -0
- package/openclaw/dist/chunks/config-G23NI5TV.js +33 -0
- package/openclaw/dist/chunks/index-marker-store-PGT5CW6T.js +33 -0
- package/openclaw/dist/chunks/setup-config-C35UK4LP.js +114 -0
- package/openclaw/dist/index.js +752 -702
- package/openclaw/openclaw.plugin.json +1 -1
- package/openclaw/package.json +1 -1
- package/package.json +2 -1
- package/pi/extension-source/hivemind.ts +473 -21
package/openclaw/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeplake/hivemind",
|
|
3
|
-
"version": "0.
|
|
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 {
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
if (
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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")
|
|
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
|
|
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,
|
|
353
|
-
|
|
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
|
}
|