@deeplake/hivemind 0.6.48 → 0.7.9
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 +244 -20
- package/bundle/cli.js +1369 -112
- package/codex/bundle/capture.js +546 -96
- package/codex/bundle/commands/auth-login.js +290 -81
- package/codex/bundle/embeddings/embed-daemon.js +243 -0
- package/codex/bundle/pre-tool-use.js +666 -111
- package/codex/bundle/session-start-setup.js +231 -64
- package/codex/bundle/session-start.js +52 -13
- package/codex/bundle/shell/deeplake-shell.js +716 -119
- package/codex/bundle/skilify-worker.js +907 -0
- package/codex/bundle/stop.js +819 -79
- package/codex/bundle/wiki-worker.js +312 -11
- package/cursor/bundle/capture.js +1116 -64
- package/cursor/bundle/commands/auth-login.js +290 -81
- package/cursor/bundle/embeddings/embed-daemon.js +243 -0
- package/cursor/bundle/pre-tool-use.js +598 -77
- package/cursor/bundle/session-end.js +520 -2
- package/cursor/bundle/session-start.js +257 -65
- package/cursor/bundle/shell/deeplake-shell.js +716 -119
- package/cursor/bundle/skilify-worker.js +907 -0
- package/cursor/bundle/wiki-worker.js +571 -0
- package/hermes/bundle/capture.js +1119 -65
- package/hermes/bundle/commands/auth-login.js +290 -81
- package/hermes/bundle/embeddings/embed-daemon.js +243 -0
- package/hermes/bundle/pre-tool-use.js +597 -76
- package/hermes/bundle/session-end.js +522 -1
- package/hermes/bundle/session-start.js +260 -65
- package/hermes/bundle/shell/deeplake-shell.js +716 -119
- package/hermes/bundle/skilify-worker.js +907 -0
- package/hermes/bundle/wiki-worker.js +572 -0
- package/mcp/bundle/server.js +290 -75
- package/openclaw/dist/chunks/auth-creds-AEKS6D3P.js +14 -0
- package/openclaw/dist/chunks/chunk-SRCBBT4H.js +37 -0
- package/openclaw/dist/chunks/config-ZLH6JFJS.js +34 -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 +929 -710
- package/openclaw/dist/skilify-worker.js +907 -0
- package/openclaw/openclaw.plugin.json +1 -1
- package/openclaw/package.json +1 -1
- package/openclaw/skills/SKILL.md +19 -0
- package/package.json +7 -1
- package/pi/extension-source/hivemind.ts +603 -22
|
@@ -24,9 +24,35 @@
|
|
|
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
|
+
import { createHash } from "node:crypto";
|
|
36
|
+
|
|
37
|
+
// ---------- diagnostic logging --------------------------------------------------
|
|
38
|
+
//
|
|
39
|
+
// The capture path is fully async + swallows errors (writeSessionRow's catch
|
|
40
|
+
// is intentionally non-fatal, so a transient deeplake outage never breaks pi).
|
|
41
|
+
// That means a buggy extension is silent: rows just don't appear, with no
|
|
42
|
+
// indication where things went wrong. When HIVEMIND_DEBUG=1 we dump a
|
|
43
|
+
// breadcrumb to ~/.deeplake/hivemind-pi.log at every meaningful step so the
|
|
44
|
+
// failure mode is observable. Off by default to keep `pi` quiet for normal
|
|
45
|
+
// users.
|
|
46
|
+
|
|
47
|
+
const LOG_PATH = join(homedir(), ".deeplake", "hivemind-pi.log");
|
|
48
|
+
|
|
49
|
+
function logHm(msg: string): void {
|
|
50
|
+
if (process.env.HIVEMIND_DEBUG !== "1") return;
|
|
51
|
+
try {
|
|
52
|
+
mkdirSync(dirname(LOG_PATH), { recursive: true });
|
|
53
|
+
appendFileSync(LOG_PATH, `${new Date().toISOString()} [pi] ${msg}\n`);
|
|
54
|
+
} catch { /* logging must never break the agent */ }
|
|
55
|
+
}
|
|
30
56
|
|
|
31
57
|
// ---------- credentials / config -----------------------------------------------
|
|
32
58
|
|
|
@@ -111,6 +137,417 @@ async function dlQuery(creds: Creds, sql: string): Promise<unknown[]> {
|
|
|
111
137
|
return json.rows.map((r) => Object.fromEntries(json.columns!.map((c, i) => [c, r[i]])));
|
|
112
138
|
}
|
|
113
139
|
|
|
140
|
+
// ---------- embedding client (inline; reuses the shared daemon) ----------------
|
|
141
|
+
//
|
|
142
|
+
// Pi avoids importing EmbedClient (which is bundled into other agents but
|
|
143
|
+
// here would break the "raw .ts, zero deps" promise of pi extensions).
|
|
144
|
+
// Instead we open a Unix socket directly to the daemon at the same well-known
|
|
145
|
+
// path EmbedClient uses. If the socket isn't there yet, we spawn the
|
|
146
|
+
// canonical daemon at ~/.hivemind/embed-deps/embed-daemon.js (deposited by
|
|
147
|
+
// `hivemind embeddings install`) and wait for it to listen, mirroring the
|
|
148
|
+
// auto-spawn-on-miss logic in src/embeddings/client.ts. Subsequent agents
|
|
149
|
+
// (codex, CC, cursor, hermes, …) connect to the SAME daemon — pi pays the
|
|
150
|
+
// cold-start cost only when it's the first user on the box.
|
|
151
|
+
//
|
|
152
|
+
// Graceful fallback: any failure → return null → caller writes NULL into
|
|
153
|
+
// message_embedding. Embedding is never on the critical path.
|
|
154
|
+
|
|
155
|
+
const EMBED_DAEMON_ENTRY = join(homedir(), ".hivemind", "embed-deps", "embed-daemon.js");
|
|
156
|
+
const EMBED_SOCKET_PATH = (() => {
|
|
157
|
+
const uid = typeof process.getuid === "function" ? String(process.getuid()) : (process.env.USER ?? "default");
|
|
158
|
+
return `/tmp/hivemind-embed-${uid}.sock`;
|
|
159
|
+
})();
|
|
160
|
+
|
|
161
|
+
function tryEmbedOverSocket(text: string, kind: "document" | "query"): Promise<number[] | null> {
|
|
162
|
+
return new Promise((resolve) => {
|
|
163
|
+
let resolved = false;
|
|
164
|
+
const settle = (v: number[] | null) => { if (!resolved) { resolved = true; resolve(v); } };
|
|
165
|
+
const sock = connect(EMBED_SOCKET_PATH);
|
|
166
|
+
let buf = "";
|
|
167
|
+
const timer = setTimeout(() => { sock.destroy(); settle(null); }, 5000);
|
|
168
|
+
sock.on("connect", () => {
|
|
169
|
+
// Protocol shape comes from src/embeddings/protocol.ts: {op, id, kind, text}.
|
|
170
|
+
// id is a string ("1"), not a number, and the verb field is "op" not "type".
|
|
171
|
+
sock.write(JSON.stringify({ op: "embed", id: "1", kind, text }) + "\n");
|
|
172
|
+
});
|
|
173
|
+
sock.on("data", (chunk: Buffer) => {
|
|
174
|
+
buf += chunk.toString("utf-8");
|
|
175
|
+
const nl = buf.indexOf("\n");
|
|
176
|
+
if (nl !== -1) {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
try {
|
|
179
|
+
const resp = JSON.parse(buf.slice(0, nl));
|
|
180
|
+
settle(Array.isArray(resp.embedding) ? resp.embedding : null);
|
|
181
|
+
} catch { settle(null); }
|
|
182
|
+
sock.destroy();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
sock.on("error", () => { clearTimeout(timer); settle(null); });
|
|
186
|
+
sock.on("close", () => { clearTimeout(timer); settle(null); });
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---------- summary state + wiki-worker spawn ---------------------------------
|
|
191
|
+
//
|
|
192
|
+
// Mirror of src/hooks/summary-state.ts (same dir, same JSON shape, shared
|
|
193
|
+
// across CC/codex/cursor/hermes — session ids are UUIDs so collisions are
|
|
194
|
+
// impossible). The pi extension increments totalCount on every captured
|
|
195
|
+
// event and spawns the bundled wiki-worker (see pi/bundle/wiki-worker.js)
|
|
196
|
+
// when the threshold is hit. The worker, after generating the summary,
|
|
197
|
+
// calls finalizeSummary() / releaseLock() against this same dir. So the
|
|
198
|
+
// extension and the worker share state.
|
|
199
|
+
|
|
200
|
+
const SUMMARY_STATE_DIR = join(homedir(), ".claude", "hooks", "summary-state");
|
|
201
|
+
const PI_WIKI_WORKER_PATH = join(homedir(), ".pi", "agent", "hivemind", "wiki-worker.js");
|
|
202
|
+
// Skilify worker installed alongside wiki-worker by `hivemind pi install`.
|
|
203
|
+
// Spawned on session_shutdown to mine reusable Claude skills from the just-
|
|
204
|
+
// finished session. Same shared bundle used by CC/Codex/Cursor/Hermes.
|
|
205
|
+
const PI_SKILIFY_WORKER_PATH = join(homedir(), ".pi", "agent", "hivemind", "skilify-worker.js");
|
|
206
|
+
const SKILIFY_STATE_DIR = join(homedir(), ".deeplake", "state", "skilify");
|
|
207
|
+
|
|
208
|
+
interface SummaryState {
|
|
209
|
+
lastSummaryAt: number;
|
|
210
|
+
lastSummaryCount: number;
|
|
211
|
+
totalCount: number;
|
|
212
|
+
}
|
|
213
|
+
interface SummaryConfig {
|
|
214
|
+
everyNMessages: number;
|
|
215
|
+
everyHours: number;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function summaryStatePath(sessionId: string): string {
|
|
219
|
+
return join(SUMMARY_STATE_DIR, `${sessionId}.json`);
|
|
220
|
+
}
|
|
221
|
+
function summaryLockPath(sessionId: string): string {
|
|
222
|
+
return join(SUMMARY_STATE_DIR, `${sessionId}.lock`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function loadSummaryConfig(): SummaryConfig {
|
|
226
|
+
const n = Number(process.env.HIVEMIND_SUMMARY_EVERY_N_MSGS ?? "");
|
|
227
|
+
const h = Number(process.env.HIVEMIND_SUMMARY_EVERY_HOURS ?? "");
|
|
228
|
+
return {
|
|
229
|
+
everyNMessages: Number.isInteger(n) && n > 0 ? n : 50,
|
|
230
|
+
everyHours: Number.isFinite(h) && h > 0 ? h : 2,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Mirrors src/hooks/summary-state.ts — the very first summary fires at
|
|
235
|
+
// totalCount=10 (vs the steady-state N=50) so a fresh chat gets indexed
|
|
236
|
+
// quickly without waiting for ~50 messages.
|
|
237
|
+
const FIRST_SUMMARY_AT = 10;
|
|
238
|
+
|
|
239
|
+
function readSummaryState(sessionId: string): SummaryState | null {
|
|
240
|
+
try {
|
|
241
|
+
const p = summaryStatePath(sessionId);
|
|
242
|
+
if (!existsSync(p)) return null;
|
|
243
|
+
const raw = JSON.parse(readFileSync(p, "utf-8"));
|
|
244
|
+
return {
|
|
245
|
+
lastSummaryAt: Number(raw.lastSummaryAt) || 0,
|
|
246
|
+
lastSummaryCount: Number(raw.lastSummaryCount) || 0,
|
|
247
|
+
totalCount: Number(raw.totalCount) || 0,
|
|
248
|
+
};
|
|
249
|
+
} catch { return null; }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function writeSummaryState(sessionId: string, state: SummaryState): void {
|
|
253
|
+
try {
|
|
254
|
+
mkdirSync(SUMMARY_STATE_DIR, { recursive: true });
|
|
255
|
+
writeFileSync(summaryStatePath(sessionId), JSON.stringify(state));
|
|
256
|
+
} catch { /* non-fatal */ }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function bumpCounter(sessionId: string): SummaryState {
|
|
260
|
+
const cur = readSummaryState(sessionId) ?? { lastSummaryAt: 0, lastSummaryCount: 0, totalCount: 0 };
|
|
261
|
+
cur.totalCount += 1;
|
|
262
|
+
writeSummaryState(sessionId, cur);
|
|
263
|
+
return cur;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function shouldTriggerNow(state: SummaryState, cfg: SummaryConfig): boolean {
|
|
267
|
+
const msgsSince = state.totalCount - state.lastSummaryCount;
|
|
268
|
+
// First-chat trigger: index a fresh session quickly (10 events) instead of
|
|
269
|
+
// waiting until N=50. Mirrors summary-state.ts in CC/codex.
|
|
270
|
+
if (state.lastSummaryCount === 0 && state.totalCount >= FIRST_SUMMARY_AT) return true;
|
|
271
|
+
if (msgsSince >= cfg.everyNMessages) return true;
|
|
272
|
+
if (msgsSince > 0 && state.lastSummaryAt > 0
|
|
273
|
+
&& Date.now() - state.lastSummaryAt >= cfg.everyHours * 3600 * 1000) return true;
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function tryAcquireSummaryLock(sessionId: string): boolean {
|
|
278
|
+
try {
|
|
279
|
+
mkdirSync(SUMMARY_STATE_DIR, { recursive: true });
|
|
280
|
+
const fd = openSync(summaryLockPath(sessionId),
|
|
281
|
+
fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY);
|
|
282
|
+
closeSync(fd);
|
|
283
|
+
return true;
|
|
284
|
+
} catch { return false; }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function findPiBin(): string {
|
|
288
|
+
try {
|
|
289
|
+
const out = execSync("which pi 2>/dev/null", { encoding: "utf-8" }).trim();
|
|
290
|
+
if (out) return out;
|
|
291
|
+
} catch { /* fall through */ }
|
|
292
|
+
return "pi";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Same template the CC/codex spawn-wiki-worker.ts ships. Inlined here
|
|
296
|
+
// because the pi extension is raw .ts and can't import it.
|
|
297
|
+
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.
|
|
298
|
+
|
|
299
|
+
SESSION JSONL path: __JSONL__
|
|
300
|
+
SUMMARY FILE to write: __SUMMARY__
|
|
301
|
+
SESSION ID: __SESSION_ID__
|
|
302
|
+
PROJECT: __PROJECT__
|
|
303
|
+
PREVIOUS JSONL OFFSET (lines already processed): __PREV_OFFSET__
|
|
304
|
+
CURRENT JSONL LINES: __JSONL_LINES__
|
|
305
|
+
|
|
306
|
+
Steps:
|
|
307
|
+
1. Read the session JSONL at the path above.
|
|
308
|
+
- If PREVIOUS JSONL OFFSET > 0, this is a resumed session. Read the existing summary file first,
|
|
309
|
+
then focus on lines AFTER the offset for new content. Merge new facts into the existing summary.
|
|
310
|
+
- If offset is 0, generate from scratch.
|
|
311
|
+
|
|
312
|
+
2. Write the summary file at the path above with this EXACT format:
|
|
313
|
+
|
|
314
|
+
# Session __SESSION_ID__
|
|
315
|
+
- **Source**: __JSONL_SERVER_PATH__
|
|
316
|
+
- **Started**: <extract from JSONL>
|
|
317
|
+
- **Ended**: <now>
|
|
318
|
+
- **Project**: __PROJECT__
|
|
319
|
+
- **JSONL offset**: __JSONL_LINES__
|
|
320
|
+
|
|
321
|
+
## What Happened
|
|
322
|
+
<2-3 dense sentences. What was the goal, what was accomplished, what's left.>
|
|
323
|
+
|
|
324
|
+
## People
|
|
325
|
+
<For each person mentioned: name, role, what they did/said. Format: **Name** — role — action>
|
|
326
|
+
|
|
327
|
+
## Entities
|
|
328
|
+
<Every named thing: repos, branches, files, APIs, tools, services, tables, features, bugs.
|
|
329
|
+
Format: **entity** (type) — what was done with it, its current state>
|
|
330
|
+
|
|
331
|
+
## Decisions & Reasoning
|
|
332
|
+
<Every decision made and WHY.>
|
|
333
|
+
|
|
334
|
+
## Key Facts
|
|
335
|
+
<Bullet list of atomic facts that could answer future questions.>
|
|
336
|
+
|
|
337
|
+
## Files Modified
|
|
338
|
+
<bullet list: path (new/modified/deleted) — what changed>
|
|
339
|
+
|
|
340
|
+
## Open Questions / TODO
|
|
341
|
+
<Anything unresolved, blocked, or explicitly deferred>
|
|
342
|
+
|
|
343
|
+
IMPORTANT: Be exhaustive. Extract EVERY entity, decision, and fact.
|
|
344
|
+
PRIVACY: Never include absolute filesystem paths in the summary.
|
|
345
|
+
LENGTH LIMIT: Keep the total summary under 4000 characters.`;
|
|
346
|
+
|
|
347
|
+
function spawnWikiWorker(
|
|
348
|
+
creds: Creds,
|
|
349
|
+
sessionId: string,
|
|
350
|
+
cwd: string,
|
|
351
|
+
reason: "periodic" | "final",
|
|
352
|
+
): void {
|
|
353
|
+
if (!existsSync(PI_WIKI_WORKER_PATH)) {
|
|
354
|
+
logHm(`spawnWikiWorker(${reason}): no worker at ${PI_WIKI_WORKER_PATH} — install via 'hivemind pi install' or rebuild`);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
// Periodic: only one in-flight; lock prevents races between events.
|
|
358
|
+
// Final: also takes the lock — if a periodic was mid-flight at session_shutdown,
|
|
359
|
+
// skip the final to avoid two concurrent workers writing back to the same row.
|
|
360
|
+
if (!tryAcquireSummaryLock(sessionId)) {
|
|
361
|
+
logHm(`spawnWikiWorker(${reason}): lock held — skipping (a worker is already running)`);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// tmp dir owned by the worker; it removes it on completion.
|
|
365
|
+
const tmpDir = join(tmpdir(), `deeplake-wiki-${sessionId}-${Date.now()}`);
|
|
366
|
+
try { mkdirSync(tmpDir, { recursive: true }); } catch { /* ignore */ }
|
|
367
|
+
const configPath = join(tmpDir, "config.json");
|
|
368
|
+
const project = (cwd ?? "").split("/").pop() || "unknown";
|
|
369
|
+
const config = {
|
|
370
|
+
apiUrl: creds.apiUrl,
|
|
371
|
+
token: creds.token,
|
|
372
|
+
orgId: creds.orgId,
|
|
373
|
+
workspaceId: creds.workspaceId,
|
|
374
|
+
memoryTable: MEMORY_TABLE,
|
|
375
|
+
sessionsTable: SESSIONS_TABLE,
|
|
376
|
+
sessionId,
|
|
377
|
+
userName: creds.userName,
|
|
378
|
+
project,
|
|
379
|
+
tmpDir,
|
|
380
|
+
piBin: findPiBin(),
|
|
381
|
+
piProvider: process.env.HIVEMIND_PI_PROVIDER ?? "google",
|
|
382
|
+
piModel: process.env.HIVEMIND_PI_MODEL ?? "gemini-2.5-flash",
|
|
383
|
+
wikiLog: join(homedir(), ".deeplake", "hivemind-pi.log"),
|
|
384
|
+
hooksDir: join(homedir(), ".pi", "agent", "hivemind"),
|
|
385
|
+
promptTemplate: WIKI_PROMPT_TEMPLATE,
|
|
386
|
+
};
|
|
387
|
+
try { writeFileSync(configPath, JSON.stringify(config)); }
|
|
388
|
+
catch (e: any) { logHm(`spawnWikiWorker(${reason}): writeFileSync failed: ${e?.message ?? e}`); return; }
|
|
389
|
+
logHm(`spawnWikiWorker(${reason}): spawning ${PI_WIKI_WORKER_PATH} session=${sessionId} provider=${config.piProvider} model=${config.piModel}`);
|
|
390
|
+
try {
|
|
391
|
+
spawn(process.execPath, [PI_WIKI_WORKER_PATH, configPath], {
|
|
392
|
+
detached: true,
|
|
393
|
+
stdio: "ignore",
|
|
394
|
+
env: { ...process.env, HIVEMIND_WIKI_WORKER: "1", HIVEMIND_CAPTURE: "false" },
|
|
395
|
+
}).unref();
|
|
396
|
+
} catch (e: any) {
|
|
397
|
+
logHm(`spawnWikiWorker(${reason}): spawn failed: ${e?.message ?? e}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ---------- skilify worker spawn ---------------------------------------------
|
|
402
|
+
//
|
|
403
|
+
// Mirror of src/skilify/spawn-skilify-worker.ts and src/skilify/triggers.ts —
|
|
404
|
+
// inlined here because pi/extension-source/hivemind.ts is shipped as raw .ts
|
|
405
|
+
// with zero non-builtin runtime dependencies (pi compiles + loads it at
|
|
406
|
+
// extension-load time). The shared TypeScript modules under src/skilify/
|
|
407
|
+
// can't be imported from this file.
|
|
408
|
+
//
|
|
409
|
+
// The skilify worker mines the just-finished session for reusable Claude
|
|
410
|
+
// skills, gates each cluster via a model call, and writes SKILL.md files +
|
|
411
|
+
// rows in the org's skills Deeplake table.
|
|
412
|
+
|
|
413
|
+
/** Stable project key — sha1(cwd) truncated, mirrors src/skilify/state.ts deriveProjectKey. */
|
|
414
|
+
function deriveSkilifyProjectKey(cwd: string): { key: string; project: string } {
|
|
415
|
+
const project = (cwd ?? "").split("/").pop() || "unknown";
|
|
416
|
+
// Pi's extension can't easily run `git config` synchronously here; use cwd
|
|
417
|
+
// as the signature. Two checkouts of the same repo at different paths get
|
|
418
|
+
// different project_keys, which is acceptable for pi (the other agents
|
|
419
|
+
// hash the git remote when available; pi falls back to cwd-only).
|
|
420
|
+
const key = createHash("sha1").update(cwd ?? "").digest("hex").slice(0, 16);
|
|
421
|
+
return { key, project };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function spawnPiSkilifyWorker(creds: Creds, sessionId: string, cwd: string): void {
|
|
425
|
+
if (!existsSync(PI_SKILIFY_WORKER_PATH)) {
|
|
426
|
+
logHm(`spawnPiSkilifyWorker: no worker at ${PI_SKILIFY_WORKER_PATH} — install via 'hivemind pi install' or rebuild`);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const { key: projectKey, project } = deriveSkilifyProjectKey(cwd);
|
|
430
|
+
|
|
431
|
+
// No spawn-side lock: the worker itself acquires `<projectKey>.lock` via
|
|
432
|
+
// src/skilify/state.ts:tryAcquireWorkerLock and releases it on exit (with
|
|
433
|
+
// a 10-min stale-lock fallback). A spawn-side lock here would create a
|
|
434
|
+
// SECOND lockfile (`<projectKey>.worker.lock`) that nobody releases,
|
|
435
|
+
// permanently blocking subsequent spawns from the same Pi runtime
|
|
436
|
+
// instance. Let the worker's own lock be the single source of truth;
|
|
437
|
+
// back-to-back spawns where a worker is in flight cost only one extra
|
|
438
|
+
// node cold-start (~50ms) before the worker self-skips on the lock.
|
|
439
|
+
|
|
440
|
+
const tmpDir = join(tmpdir(), `deeplake-skilify-${projectKey}-${Date.now()}`);
|
|
441
|
+
try { mkdirSync(tmpDir, { recursive: true, mode: 0o700 }); }
|
|
442
|
+
catch (e: any) { logHm(`spawnPiSkilifyWorker: mkdir failed: ${e?.message ?? e}`); return; }
|
|
443
|
+
const configPath = join(tmpDir, "config.json");
|
|
444
|
+
|
|
445
|
+
// Same shape the spawn-skilify-worker.ts module writes for the other agents.
|
|
446
|
+
// Defaults match scope-config.ts: scope=me, install=project, no team list.
|
|
447
|
+
// Pi-specific: no per-agent gate binary (`gateBin: null`) — the worker's
|
|
448
|
+
// gate-runner falls back to its agent dispatch which for `agent: "pi"`
|
|
449
|
+
// resolves to the `pi --print` invocation we'd want for consistency.
|
|
450
|
+
const config = {
|
|
451
|
+
apiUrl: creds.apiUrl,
|
|
452
|
+
token: creds.token,
|
|
453
|
+
orgId: creds.orgId,
|
|
454
|
+
workspaceId: creds.workspaceId,
|
|
455
|
+
sessionsTable: SESSIONS_TABLE,
|
|
456
|
+
skillsTable: process.env.HIVEMIND_SKILLS_TABLE || "skills",
|
|
457
|
+
userName: creds.userName,
|
|
458
|
+
cwd,
|
|
459
|
+
projectKey,
|
|
460
|
+
project,
|
|
461
|
+
agent: "pi",
|
|
462
|
+
scope: "me" as const,
|
|
463
|
+
team: [] as string[],
|
|
464
|
+
install: "project" as const,
|
|
465
|
+
tmpDir,
|
|
466
|
+
gateBin: findPiBin(),
|
|
467
|
+
cursorModel: process.env.HIVEMIND_CURSOR_MODEL,
|
|
468
|
+
hermesProvider: process.env.HIVEMIND_HERMES_PROVIDER,
|
|
469
|
+
hermesModel: process.env.HIVEMIND_HERMES_MODEL,
|
|
470
|
+
// pi-specific gate args — match wikiWorker config defaults (google + gemini-2.5-flash)
|
|
471
|
+
piProvider: process.env.HIVEMIND_PI_PROVIDER ?? "google",
|
|
472
|
+
piModel: process.env.HIVEMIND_PI_MODEL ?? "gemini-2.5-flash",
|
|
473
|
+
skilifyLog: join(homedir(), ".deeplake", "hivemind-pi-skilify.log"),
|
|
474
|
+
currentSessionId: sessionId,
|
|
475
|
+
};
|
|
476
|
+
try { writeFileSync(configPath, JSON.stringify(config), { mode: 0o600 }); }
|
|
477
|
+
catch (e: any) { logHm(`spawnPiSkilifyWorker: config write failed: ${e?.message ?? e}`); return; }
|
|
478
|
+
|
|
479
|
+
logHm(`spawnPiSkilifyWorker: spawning ${PI_SKILIFY_WORKER_PATH} project=${project} key=${projectKey} session=${sessionId}`);
|
|
480
|
+
try {
|
|
481
|
+
spawn(process.execPath, [PI_SKILIFY_WORKER_PATH, configPath], {
|
|
482
|
+
detached: true,
|
|
483
|
+
stdio: "ignore",
|
|
484
|
+
env: { ...process.env, HIVEMIND_SKILIFY_WORKER: "1", HIVEMIND_CAPTURE: "false" },
|
|
485
|
+
}).unref();
|
|
486
|
+
} catch (e: any) {
|
|
487
|
+
logHm(`spawnPiSkilifyWorker: spawn failed: ${e?.message ?? e}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function maybeTriggerPeriodicSummary(creds: Creds, sessionId: string, cwd: string): void {
|
|
492
|
+
if (process.env.HIVEMIND_CAPTURE === "false") return;
|
|
493
|
+
const state = bumpCounter(sessionId);
|
|
494
|
+
const cfg = loadSummaryConfig();
|
|
495
|
+
if (!shouldTriggerNow(state, cfg)) return;
|
|
496
|
+
logHm(`periodic threshold hit (total=${state.totalCount}, since=${state.totalCount - state.lastSummaryCount}, N=${cfg.everyNMessages}, hours=${cfg.everyHours})`);
|
|
497
|
+
spawnWikiWorker(creds, sessionId, cwd, "periodic");
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function embed(text: string): Promise<number[] | null> {
|
|
501
|
+
if (process.env.HIVEMIND_EMBEDDINGS === "false") {
|
|
502
|
+
logHm(`embed: skipped (HIVEMIND_EMBEDDINGS=false)`);
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
if (!text || text.length === 0) {
|
|
506
|
+
logHm(`embed: skipped (empty text)`);
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
// 1) socket already up (another agent or us in a previous turn) → fast path
|
|
510
|
+
let v = await tryEmbedOverSocket(text, "document");
|
|
511
|
+
if (v !== null) {
|
|
512
|
+
logHm(`embed: ok via existing socket (dims=${v.length})`);
|
|
513
|
+
return v;
|
|
514
|
+
}
|
|
515
|
+
// 2) no daemon binary deposited → fallback NULL
|
|
516
|
+
if (!existsSync(EMBED_DAEMON_ENTRY)) {
|
|
517
|
+
logHm(`embed: no daemon at ${EMBED_DAEMON_ENTRY} — run 'hivemind embeddings install'`);
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
// 3) spawn the canonical daemon detached; daemon's own pidfile lock guards
|
|
521
|
+
// against double-spawn if multiple pi turns race.
|
|
522
|
+
logHm(`embed: spawning daemon at ${EMBED_DAEMON_ENTRY}`);
|
|
523
|
+
try {
|
|
524
|
+
spawn(process.execPath, [EMBED_DAEMON_ENTRY], { detached: true, stdio: "ignore" }).unref();
|
|
525
|
+
} catch (e: any) {
|
|
526
|
+
logHm(`embed: spawn failed: ${e?.message ?? e}`);
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
// 4) poll for the socket up to ~5s, then retry the embed once
|
|
530
|
+
for (let i = 0; i < 25; i++) {
|
|
531
|
+
await new Promise(r => setTimeout(r, 200));
|
|
532
|
+
if (existsSync(EMBED_SOCKET_PATH)) {
|
|
533
|
+
v = await tryEmbedOverSocket(text, "document");
|
|
534
|
+
if (v !== null) {
|
|
535
|
+
logHm(`embed: ok after spawn (dims=${v.length}, polls=${i + 1})`);
|
|
536
|
+
return v;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
logHm(`embed: timed out after spawn (5s)`);
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function embedSqlLiteral(emb: number[] | null): string {
|
|
545
|
+
if (!emb || emb.length === 0) return "NULL";
|
|
546
|
+
// FLOAT4[] literal. Numbers serialize without quotes; emb is a plain
|
|
547
|
+
// number[] from the daemon so JSON-style join is safe.
|
|
548
|
+
return `ARRAY[${emb.join(",")}]::FLOAT4[]`;
|
|
549
|
+
}
|
|
550
|
+
|
|
114
551
|
// ---------- session-row writer -------------------------------------------------
|
|
115
552
|
|
|
116
553
|
function buildSessionPath(creds: Creds, sessionId: string): string {
|
|
@@ -118,6 +555,13 @@ function buildSessionPath(creds: Creds, sessionId: string): string {
|
|
|
118
555
|
return `/sessions/${creds.userName}/${filename}`;
|
|
119
556
|
}
|
|
120
557
|
|
|
558
|
+
// Deeplake quirk: CREATE TABLE IF NOT EXISTS returns 200 before the table
|
|
559
|
+
// is queryable for INSERTs (the propagation can take 30+ seconds on a fresh
|
|
560
|
+
// table). Other agents don't hit this in steady state because they reuse
|
|
561
|
+
// existing tables; pi's e2e tests use fresh timestamped tables every run.
|
|
562
|
+
// Fix: tolerate "Table does not exist" specifically and retry with backoff.
|
|
563
|
+
const INSERT_RETRY_BACKOFFS_MS = [1000, 3000, 8000, 15000];
|
|
564
|
+
|
|
121
565
|
async function writeSessionRow(
|
|
122
566
|
creds: Creds,
|
|
123
567
|
sessionId: string,
|
|
@@ -132,11 +576,33 @@ async function writeSessionRow(
|
|
|
132
576
|
const projectName = (cwd ?? "").split("/").pop() || "unknown";
|
|
133
577
|
const line = JSON.stringify(entry);
|
|
134
578
|
const jsonForSql = sqlJsonb(line);
|
|
579
|
+
logHm(`writeSessionRow: event=${event} session=${sessionId} bytes=${line.length} table=${SESSIONS_TABLE}`);
|
|
580
|
+
const emb = await embed(line);
|
|
581
|
+
logHm(`writeSessionRow: embed=${emb ? `dims=${emb.length}` : "null"}`);
|
|
135
582
|
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)}', ` +
|
|
583
|
+
`INSERT INTO "${SESSIONS_TABLE}" (id, path, filename, message, message_embedding, author, size_bytes, project, description, agent, creation_date, last_update_date) ` +
|
|
584
|
+
`VALUES ('${crypto.randomUUID()}', '${sqlStr(sessionPath)}', '${sqlStr(filename)}', '${jsonForSql}'::jsonb, ${embedSqlLiteral(emb)}, '${sqlStr(creds.userName)}', ` +
|
|
138
585
|
`${Buffer.byteLength(line, "utf-8")}, '${sqlStr(projectName)}', '${sqlStr(event)}', '${agent}', '${ts}', '${ts}')`;
|
|
139
|
-
|
|
586
|
+
let lastErr: any = null;
|
|
587
|
+
for (let attempt = 0; attempt <= INSERT_RETRY_BACKOFFS_MS.length; attempt++) {
|
|
588
|
+
try {
|
|
589
|
+
await dlQuery(creds, insertSql);
|
|
590
|
+
logHm(`writeSessionRow: INSERT ok (event=${event}, attempt=${attempt + 1})`);
|
|
591
|
+
return;
|
|
592
|
+
} catch (e: any) {
|
|
593
|
+
lastErr = e;
|
|
594
|
+
const msg = e?.message ?? String(e);
|
|
595
|
+
const isPropagationDelay = /table does not exist|relation .* does not exist/i.test(msg);
|
|
596
|
+
if (!isPropagationDelay || attempt === INSERT_RETRY_BACKOFFS_MS.length) {
|
|
597
|
+
logHm(`writeSessionRow: INSERT FAILED (event=${event}, attempt=${attempt + 1}): ${msg}`);
|
|
598
|
+
throw e;
|
|
599
|
+
}
|
|
600
|
+
const delay = INSERT_RETRY_BACKOFFS_MS[attempt];
|
|
601
|
+
logHm(`writeSessionRow: table not yet visible, retrying in ${delay}ms (attempt=${attempt + 1}/${INSERT_RETRY_BACKOFFS_MS.length + 1})`);
|
|
602
|
+
await new Promise(r => setTimeout(r, delay));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
throw lastErr;
|
|
140
606
|
}
|
|
141
607
|
|
|
142
608
|
// ---------- search primitive (used by hivemind_search) -------------------------
|
|
@@ -171,7 +637,32 @@ Three hivemind tools are registered:
|
|
|
171
637
|
hivemind_read { path } read full content at a memory path
|
|
172
638
|
hivemind_index { prefix?, limit? } list summary entries
|
|
173
639
|
|
|
174
|
-
Prefer these tools — one call returns ranked hits across all summaries and sessions in a single SQL query. Different paths under /summaries/<username>/ are different users; do NOT merge or alias them. Fall back to grep on ~/.deeplake/memory/ only if tools are unavailable
|
|
640
|
+
Prefer these tools — one call returns ranked hits across all summaries and sessions in a single SQL query. Different paths under /summaries/<username>/ are different users; do NOT merge or alias them. Fall back to grep on ~/.deeplake/memory/ only if tools are unavailable.
|
|
641
|
+
|
|
642
|
+
Organization management — each argument is SEPARATE (do NOT quote subcommands together):
|
|
643
|
+
- hivemind login — SSO login
|
|
644
|
+
- hivemind whoami — show current user/org
|
|
645
|
+
- hivemind org list — list organizations
|
|
646
|
+
- hivemind org switch <name-or-id> — switch organization
|
|
647
|
+
- hivemind workspaces — list workspaces
|
|
648
|
+
- hivemind workspace <id> — switch workspace
|
|
649
|
+
- hivemind invite <email> <ADMIN|WRITE|READ> — invite member (ALWAYS ask user which role before inviting)
|
|
650
|
+
- hivemind members — list members
|
|
651
|
+
- hivemind remove <user-id> — remove member
|
|
652
|
+
|
|
653
|
+
SKILLS (skilify) — mine + share reusable skills across the org. Run these in a terminal (or via shell if available):
|
|
654
|
+
- hivemind skilify — show scope/team/install + per-project state
|
|
655
|
+
- hivemind skilify pull — sync project skills from the org table
|
|
656
|
+
- hivemind skilify pull --user <email> — only that author's skills
|
|
657
|
+
- hivemind skilify pull --users a,b,c — multiple authors (CSV)
|
|
658
|
+
- hivemind skilify pull --all-users — explicit "no author filter"
|
|
659
|
+
- hivemind skilify pull --to project|global — install location
|
|
660
|
+
- hivemind skilify pull --dry-run — preview only
|
|
661
|
+
- hivemind skilify pull --force — overwrite local (creates .bak)
|
|
662
|
+
- hivemind skilify pull <skill-name> — pull only that skill (combines with --user)
|
|
663
|
+
- hivemind skilify scope <me|team|org> — sharing scope for new skills
|
|
664
|
+
- hivemind skilify install <project|global> — default install location
|
|
665
|
+
- hivemind skilify team add|remove|list <name> — manage team list`;
|
|
175
666
|
|
|
176
667
|
export default function hivemindExtension(pi: ExtensionAPI): void {
|
|
177
668
|
const captureEnabled = process.env.HIVEMIND_CAPTURE !== "false";
|
|
@@ -267,7 +758,62 @@ export default function hivemindExtension(pi: ExtensionAPI): void {
|
|
|
267
758
|
// themselves don't carry them.
|
|
268
759
|
|
|
269
760
|
pi.on("session_start", async (_event: any, _ctx: any) => {
|
|
761
|
+
logHm(`session_start: fired (capture=${captureEnabled}, embed=${process.env.HIVEMIND_EMBEDDINGS !== "false"}, table=${SESSIONS_TABLE})`);
|
|
270
762
|
const creds = loadCreds();
|
|
763
|
+
if (!creds) {
|
|
764
|
+
logHm(`session_start: no credentials at ~/.deeplake/credentials.json — capture disabled this session`);
|
|
765
|
+
} else {
|
|
766
|
+
logHm(`session_start: creds org=${creds.orgName ?? creds.orgId} ws=${creds.workspaceId}`);
|
|
767
|
+
}
|
|
768
|
+
if (creds && captureEnabled) {
|
|
769
|
+
// Other agents' session-start hooks create the memory + sessions tables
|
|
770
|
+
// via DeeplakeApi.ensureTable / ensureSessionsTable. The pi extension is
|
|
771
|
+
// standalone (no shared lib import to keep it raw-.ts), so we issue the
|
|
772
|
+
// CREATE TABLE IF NOT EXISTS directly. Schema matches the canonical one
|
|
773
|
+
// in src/deeplake-api.ts so all agents read/write the same shape.
|
|
774
|
+
const memCreate = `CREATE TABLE IF NOT EXISTS "${MEMORY_TABLE}" (` +
|
|
775
|
+
`id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', ` +
|
|
776
|
+
`filename TEXT NOT NULL DEFAULT '', summary TEXT NOT NULL DEFAULT '', ` +
|
|
777
|
+
`summary_embedding FLOAT4[], author TEXT NOT NULL DEFAULT '', ` +
|
|
778
|
+
`mime_type TEXT NOT NULL DEFAULT 'text/plain', size_bytes BIGINT NOT NULL DEFAULT 0, ` +
|
|
779
|
+
`project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', ` +
|
|
780
|
+
`agent TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', ` +
|
|
781
|
+
`last_update_date TEXT NOT NULL DEFAULT ''` +
|
|
782
|
+
`) USING deeplake`;
|
|
783
|
+
const sessCreate = `CREATE TABLE IF NOT EXISTS "${SESSIONS_TABLE}" (` +
|
|
784
|
+
`id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', ` +
|
|
785
|
+
`filename TEXT NOT NULL DEFAULT '', message JSONB, message_embedding FLOAT4[], ` +
|
|
786
|
+
`author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'application/json', ` +
|
|
787
|
+
`size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', ` +
|
|
788
|
+
`description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', ` +
|
|
789
|
+
`creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT ''` +
|
|
790
|
+
`) USING deeplake`;
|
|
791
|
+
try { await dlQuery(creds, memCreate); logHm(`session_start: memory CREATE TABLE ok (${MEMORY_TABLE})`); }
|
|
792
|
+
catch (e: any) { logHm(`session_start: memory CREATE failed: ${e?.message ?? e}`); }
|
|
793
|
+
try { await dlQuery(creds, sessCreate); logHm(`session_start: sessions CREATE TABLE ok (${SESSIONS_TABLE})`); }
|
|
794
|
+
catch (e: any) { logHm(`session_start: sessions CREATE failed: ${e?.message ?? e}`); }
|
|
795
|
+
// Proactively poll until the sessions table is queryable. CREATE TABLE
|
|
796
|
+
// returns 200 before propagation completes on Deeplake; the first INSERT
|
|
797
|
+
// can otherwise fail with "Table does not exist" for ~30s. Polling here
|
|
798
|
+
// amortises the delay before any event fires.
|
|
799
|
+
const probeSql = `SELECT 1 FROM "${SESSIONS_TABLE}" LIMIT 1`;
|
|
800
|
+
const start = Date.now();
|
|
801
|
+
let visible = false;
|
|
802
|
+
for (let i = 0; i < 30 && !visible; i++) {
|
|
803
|
+
try {
|
|
804
|
+
await dlQuery(creds, probeSql);
|
|
805
|
+
visible = true;
|
|
806
|
+
} catch (e: any) {
|
|
807
|
+
const msg = e?.message ?? String(e);
|
|
808
|
+
if (!/table does not exist|relation .* does not exist/i.test(msg)) {
|
|
809
|
+
logHm(`session_start: probe failed (non-propagation): ${msg}`);
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
logHm(`session_start: sessions table visible=${visible} (probe took ${Date.now() - start}ms)`);
|
|
816
|
+
}
|
|
271
817
|
const additional = creds
|
|
272
818
|
? `${CONTEXT_PREAMBLE}\nLogged in to Deeplake as org: ${creds.orgName ?? creds.orgId} (workspace: ${creds.workspaceId}).`
|
|
273
819
|
: `${CONTEXT_PREAMBLE}\nNot logged in to Deeplake. Run \`hivemind login\` to authenticate.`;
|
|
@@ -275,12 +821,13 @@ export default function hivemindExtension(pi: ExtensionAPI): void {
|
|
|
275
821
|
});
|
|
276
822
|
|
|
277
823
|
pi.on("input", async (event: any, ctx: any) => {
|
|
278
|
-
|
|
279
|
-
if (
|
|
824
|
+
logHm(`input: fired source=${event?.source ?? "?"}`);
|
|
825
|
+
if (!captureEnabled) { logHm(`input: capture disabled, skipping`); return; }
|
|
826
|
+
if (event.source === "extension") { logHm(`input: extension-injected, skipping`); return; }
|
|
280
827
|
const creds = loadCreds();
|
|
281
|
-
if (!creds) return;
|
|
828
|
+
if (!creds) { logHm(`input: no creds, skipping`); return; }
|
|
282
829
|
const text = typeof event.text === "string" ? event.text : "";
|
|
283
|
-
if (!text) return;
|
|
830
|
+
if (!text) { logHm(`input: empty text, skipping`); return; }
|
|
284
831
|
const sessionId = ctx?.sessionManager?.getSessionId?.() ?? `pi-${Date.now()}`;
|
|
285
832
|
const cwd = ctx?.cwd ?? ctx?.sessionManager?.getCwd?.() ?? process.cwd();
|
|
286
833
|
try {
|
|
@@ -291,13 +838,17 @@ export default function hivemindExtension(pi: ExtensionAPI): void {
|
|
|
291
838
|
content: text,
|
|
292
839
|
timestamp: new Date().toISOString(),
|
|
293
840
|
});
|
|
294
|
-
} catch
|
|
841
|
+
} catch (e: any) {
|
|
842
|
+
logHm(`input: writeSessionRow swallowed: ${e?.message ?? e}`);
|
|
843
|
+
}
|
|
844
|
+
maybeTriggerPeriodicSummary(creds, sessionId, cwd);
|
|
295
845
|
});
|
|
296
846
|
|
|
297
847
|
pi.on("tool_result", async (event: any, ctx: any) => {
|
|
298
|
-
|
|
848
|
+
logHm(`tool_result: fired tool=${event?.toolName ?? "?"} isError=${event?.isError === true}`);
|
|
849
|
+
if (!captureEnabled) { logHm(`tool_result: capture disabled, skipping`); return; }
|
|
299
850
|
const creds = loadCreds();
|
|
300
|
-
if (!creds) return;
|
|
851
|
+
if (!creds) { logHm(`tool_result: no creds, skipping`); return; }
|
|
301
852
|
const sessionId = ctx?.sessionManager?.getSessionId?.() ?? `pi-${Date.now()}`;
|
|
302
853
|
const cwd = ctx?.cwd ?? ctx?.sessionManager?.getCwd?.() ?? process.cwd();
|
|
303
854
|
// event.content is (TextContent | ImageContent)[]; extract text blocks.
|
|
@@ -318,24 +869,31 @@ export default function hivemindExtension(pi: ExtensionAPI): void {
|
|
|
318
869
|
is_error: event.isError === true,
|
|
319
870
|
timestamp: new Date().toISOString(),
|
|
320
871
|
});
|
|
321
|
-
} catch
|
|
872
|
+
} catch (e: any) {
|
|
873
|
+
logHm(`tool_result: writeSessionRow swallowed: ${e?.message ?? e}`);
|
|
874
|
+
}
|
|
875
|
+
maybeTriggerPeriodicSummary(creds, sessionId, cwd);
|
|
322
876
|
});
|
|
323
877
|
|
|
324
878
|
pi.on("message_end", async (event: any, ctx: any) => {
|
|
325
|
-
|
|
879
|
+
logHm(`message_end: fired role=${event?.message?.role ?? "?"}`);
|
|
880
|
+
if (!captureEnabled) { logHm(`message_end: capture disabled, skipping`); return; }
|
|
326
881
|
const creds = loadCreds();
|
|
327
|
-
if (!creds) return;
|
|
882
|
+
if (!creds) { logHm(`message_end: no creds, skipping`); return; }
|
|
328
883
|
const message = event.message ?? null;
|
|
329
884
|
// AgentMessage is UserMessage | AssistantMessage | ToolResultMessage.
|
|
330
885
|
// user is captured via `input`; toolResult via `tool_result`. Only assistant here.
|
|
331
|
-
if (!message || message.role !== "assistant")
|
|
886
|
+
if (!message || message.role !== "assistant") {
|
|
887
|
+
logHm(`message_end: skipping (role=${message?.role ?? "null"} — only assistant rows are written here)`);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
332
890
|
// AssistantMessage.content is (TextContent | ThinkingContent | ToolCall)[].
|
|
333
891
|
const blocks: any[] = Array.isArray(message.content) ? message.content : [];
|
|
334
892
|
const text = blocks
|
|
335
893
|
.filter((b: any) => b?.type === "text" && typeof b.text === "string")
|
|
336
894
|
.map((b: any) => b.text)
|
|
337
895
|
.join("\n");
|
|
338
|
-
if (!text) return;
|
|
896
|
+
if (!text) { logHm(`message_end: assistant message had no text blocks, skipping`); return; }
|
|
339
897
|
const sessionId = ctx?.sessionManager?.getSessionId?.() ?? `pi-${Date.now()}`;
|
|
340
898
|
const cwd = ctx?.cwd ?? ctx?.sessionManager?.getCwd?.() ?? process.cwd();
|
|
341
899
|
try {
|
|
@@ -346,10 +904,33 @@ export default function hivemindExtension(pi: ExtensionAPI): void {
|
|
|
346
904
|
content: text,
|
|
347
905
|
timestamp: new Date().toISOString(),
|
|
348
906
|
});
|
|
349
|
-
} catch
|
|
907
|
+
} catch (e: any) {
|
|
908
|
+
logHm(`message_end: writeSessionRow swallowed: ${e?.message ?? e}`);
|
|
909
|
+
}
|
|
910
|
+
maybeTriggerPeriodicSummary(creds, sessionId, cwd);
|
|
350
911
|
});
|
|
351
912
|
|
|
352
|
-
pi.on("session_shutdown", async (_event: any,
|
|
353
|
-
|
|
913
|
+
pi.on("session_shutdown", async (_event: any, ctx: any) => {
|
|
914
|
+
logHm(`session_shutdown: fired`);
|
|
915
|
+
if (process.env.HIVEMIND_CAPTURE === "false") return;
|
|
916
|
+
const creds = loadCreds();
|
|
917
|
+
if (!creds) { logHm(`session_shutdown: no creds, skipping final summary`); return; }
|
|
918
|
+
const sessionId = ctx?.sessionManager?.getSessionId?.() ?? null;
|
|
919
|
+
if (!sessionId) { logHm(`session_shutdown: no sessionId, skipping final summary`); return; }
|
|
920
|
+
const cwd = ctx?.cwd ?? ctx?.sessionManager?.getCwd?.() ?? process.cwd();
|
|
921
|
+
// Always spawn for "final" — but the lock check inside spawnWikiWorker
|
|
922
|
+
// skips if a periodic worker is mid-flight. Non-fatal either way.
|
|
923
|
+
spawnWikiWorker(creds, sessionId, cwd, "final");
|
|
924
|
+
|
|
925
|
+
// Also kick off the skilify worker so this session's prompt+answer
|
|
926
|
+
// pairs become candidates for reusable skills. Lock keyed on
|
|
927
|
+
// projectKey, not sessionId — multiple sessions in the same project
|
|
928
|
+
// shouldn't race the gate. Non-fatal: failure here only loses the
|
|
929
|
+
// mining for this one session, never breaks the wiki summary above.
|
|
930
|
+
try { spawnPiSkilifyWorker(creds, sessionId, cwd); }
|
|
931
|
+
catch (e: any) { logHm(`session_shutdown: skilify spawn threw: ${e?.message ?? e}`); }
|
|
354
932
|
});
|
|
933
|
+
|
|
934
|
+
// Module-load breadcrumb so we know the extension's default export ran at all.
|
|
935
|
+
logHm(`extension loaded (table=${SESSIONS_TABLE}, mem=${MEMORY_TABLE})`);
|
|
355
936
|
}
|