@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
package/codex/bundle/capture.js
CHANGED
|
@@ -1,4 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// dist/src/index-marker-store.js
|
|
13
|
+
var index_marker_store_exports = {};
|
|
14
|
+
__export(index_marker_store_exports, {
|
|
15
|
+
buildIndexMarkerPath: () => buildIndexMarkerPath,
|
|
16
|
+
getIndexMarkerDir: () => getIndexMarkerDir,
|
|
17
|
+
hasFreshIndexMarker: () => hasFreshIndexMarker,
|
|
18
|
+
writeIndexMarker: () => writeIndexMarker
|
|
19
|
+
});
|
|
20
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
21
|
+
import { join as join3 } from "node:path";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
function getIndexMarkerDir() {
|
|
24
|
+
return process.env.HIVEMIND_INDEX_MARKER_DIR ?? join3(tmpdir(), "hivemind-deeplake-indexes");
|
|
25
|
+
}
|
|
26
|
+
function buildIndexMarkerPath(workspaceId, orgId, table, suffix) {
|
|
27
|
+
const markerKey = [workspaceId, orgId, table, suffix].join("__").replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
28
|
+
return join3(getIndexMarkerDir(), `${markerKey}.json`);
|
|
29
|
+
}
|
|
30
|
+
function hasFreshIndexMarker(markerPath) {
|
|
31
|
+
if (!existsSync2(markerPath))
|
|
32
|
+
return false;
|
|
33
|
+
try {
|
|
34
|
+
const raw = JSON.parse(readFileSync2(markerPath, "utf-8"));
|
|
35
|
+
const updatedAt = raw.updatedAt ? new Date(raw.updatedAt).getTime() : NaN;
|
|
36
|
+
if (!Number.isFinite(updatedAt) || Date.now() - updatedAt > INDEX_MARKER_TTL_MS)
|
|
37
|
+
return false;
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function writeIndexMarker(markerPath) {
|
|
44
|
+
mkdirSync(getIndexMarkerDir(), { recursive: true });
|
|
45
|
+
writeFileSync(markerPath, JSON.stringify({ updatedAt: (/* @__PURE__ */ new Date()).toISOString() }), "utf-8");
|
|
46
|
+
}
|
|
47
|
+
var INDEX_MARKER_TTL_MS;
|
|
48
|
+
var init_index_marker_store = __esm({
|
|
49
|
+
"dist/src/index-marker-store.js"() {
|
|
50
|
+
"use strict";
|
|
51
|
+
INDEX_MARKER_TTL_MS = Number(process.env.HIVEMIND_INDEX_MARKER_TTL_MS ?? 6 * 60 * 6e4);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
2
54
|
|
|
3
55
|
// dist/src/utils/stdin.js
|
|
4
56
|
function readStdin() {
|
|
@@ -45,15 +97,13 @@ function loadConfig() {
|
|
|
45
97
|
apiUrl: process.env.HIVEMIND_API_URL ?? creds?.apiUrl ?? "https://api.deeplake.ai",
|
|
46
98
|
tableName: process.env.HIVEMIND_TABLE ?? "memory",
|
|
47
99
|
sessionsTableName: process.env.HIVEMIND_SESSIONS_TABLE ?? "sessions",
|
|
100
|
+
skillsTableName: process.env.HIVEMIND_SKILLS_TABLE ?? "skills",
|
|
48
101
|
memoryPath: process.env.HIVEMIND_MEMORY_PATH ?? join(home, ".deeplake", "memory")
|
|
49
102
|
};
|
|
50
103
|
}
|
|
51
104
|
|
|
52
105
|
// dist/src/deeplake-api.js
|
|
53
106
|
import { randomUUID } from "node:crypto";
|
|
54
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
55
|
-
import { join as join3 } from "node:path";
|
|
56
|
-
import { tmpdir } from "node:os";
|
|
57
107
|
|
|
58
108
|
// dist/src/utils/debug.js
|
|
59
109
|
import { appendFileSync } from "node:fs";
|
|
@@ -75,8 +125,33 @@ function log(tag, msg) {
|
|
|
75
125
|
function sqlStr(value) {
|
|
76
126
|
return value.replace(/\\/g, "\\\\").replace(/'/g, "''").replace(/\0/g, "").replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
|
|
77
127
|
}
|
|
128
|
+
function sqlIdent(name) {
|
|
129
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
130
|
+
throw new Error(`Invalid SQL identifier: ${JSON.stringify(name)}`);
|
|
131
|
+
}
|
|
132
|
+
return name;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// dist/src/embeddings/columns.js
|
|
136
|
+
var SUMMARY_EMBEDDING_COL = "summary_embedding";
|
|
137
|
+
var MESSAGE_EMBEDDING_COL = "message_embedding";
|
|
138
|
+
|
|
139
|
+
// dist/src/utils/client-header.js
|
|
140
|
+
var DEEPLAKE_CLIENT_HEADER = "X-Deeplake-Client";
|
|
141
|
+
function deeplakeClientValue() {
|
|
142
|
+
return "hivemind";
|
|
143
|
+
}
|
|
144
|
+
function deeplakeClientHeader() {
|
|
145
|
+
return { [DEEPLAKE_CLIENT_HEADER]: deeplakeClientValue() };
|
|
146
|
+
}
|
|
78
147
|
|
|
79
148
|
// dist/src/deeplake-api.js
|
|
149
|
+
var indexMarkerStorePromise = null;
|
|
150
|
+
function getIndexMarkerStore() {
|
|
151
|
+
if (!indexMarkerStorePromise)
|
|
152
|
+
indexMarkerStorePromise = Promise.resolve().then(() => (init_index_marker_store(), index_marker_store_exports));
|
|
153
|
+
return indexMarkerStorePromise;
|
|
154
|
+
}
|
|
80
155
|
var log2 = (msg) => log("sdk", msg);
|
|
81
156
|
function summarizeSql(sql, maxLen = 220) {
|
|
82
157
|
const compact = sql.replace(/\s+/g, " ").trim();
|
|
@@ -96,7 +171,6 @@ var MAX_RETRIES = 3;
|
|
|
96
171
|
var BASE_DELAY_MS = 500;
|
|
97
172
|
var MAX_CONCURRENCY = 5;
|
|
98
173
|
var QUERY_TIMEOUT_MS = Number(process.env.HIVEMIND_QUERY_TIMEOUT_MS ?? 1e4);
|
|
99
|
-
var INDEX_MARKER_TTL_MS = Number(process.env.HIVEMIND_INDEX_MARKER_TTL_MS ?? 6 * 60 * 6e4);
|
|
100
174
|
function sleep(ms) {
|
|
101
175
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
102
176
|
}
|
|
@@ -116,9 +190,6 @@ function isTransientHtml403(text) {
|
|
|
116
190
|
const body = text.toLowerCase();
|
|
117
191
|
return body.includes("<html") || body.includes("403 forbidden") || body.includes("cloudflare") || body.includes("nginx");
|
|
118
192
|
}
|
|
119
|
-
function getIndexMarkerDir() {
|
|
120
|
-
return process.env.HIVEMIND_INDEX_MARKER_DIR ?? join3(tmpdir(), "hivemind-deeplake-indexes");
|
|
121
|
-
}
|
|
122
193
|
var Semaphore = class {
|
|
123
194
|
max;
|
|
124
195
|
waiting = [];
|
|
@@ -187,7 +258,8 @@ var DeeplakeApi = class {
|
|
|
187
258
|
headers: {
|
|
188
259
|
Authorization: `Bearer ${this.token}`,
|
|
189
260
|
"Content-Type": "application/json",
|
|
190
|
-
"X-Activeloop-Org-Id": this.orgId
|
|
261
|
+
"X-Activeloop-Org-Id": this.orgId,
|
|
262
|
+
...deeplakeClientHeader()
|
|
191
263
|
},
|
|
192
264
|
signal,
|
|
193
265
|
body: JSON.stringify({ query: sql })
|
|
@@ -214,7 +286,8 @@ var DeeplakeApi = class {
|
|
|
214
286
|
}
|
|
215
287
|
const text = await resp.text().catch(() => "");
|
|
216
288
|
const retryable403 = isSessionInsertQuery(sql) && (resp.status === 401 || resp.status === 403 && (text.length === 0 || isTransientHtml403(text)));
|
|
217
|
-
|
|
289
|
+
const alreadyExists = resp.status === 500 && isDuplicateIndexError(text);
|
|
290
|
+
if (!alreadyExists && attempt < MAX_RETRIES && (RETRYABLE_CODES.has(resp.status) || retryable403)) {
|
|
218
291
|
const delay = BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 200;
|
|
219
292
|
log2(`query retry ${attempt + 1}/${MAX_RETRIES} (${resp.status}) in ${delay.toFixed(0)}ms`);
|
|
220
293
|
await sleep(delay);
|
|
@@ -248,7 +321,7 @@ var DeeplakeApi = class {
|
|
|
248
321
|
const lud = row.lastUpdateDate ?? ts;
|
|
249
322
|
const exists = await this.query(`SELECT path FROM "${this.tableName}" WHERE path = '${sqlStr(row.path)}' LIMIT 1`);
|
|
250
323
|
if (exists.length > 0) {
|
|
251
|
-
let setClauses = `summary = E'${sqlStr(row.contentText)}', mime_type = '${sqlStr(row.mimeType)}', size_bytes = ${row.sizeBytes}, last_update_date = '${lud}'`;
|
|
324
|
+
let setClauses = `summary = E'${sqlStr(row.contentText)}', ${SUMMARY_EMBEDDING_COL} = NULL, mime_type = '${sqlStr(row.mimeType)}', size_bytes = ${row.sizeBytes}, last_update_date = '${lud}'`;
|
|
252
325
|
if (row.project !== void 0)
|
|
253
326
|
setClauses += `, project = '${sqlStr(row.project)}'`;
|
|
254
327
|
if (row.description !== void 0)
|
|
@@ -256,8 +329,8 @@ var DeeplakeApi = class {
|
|
|
256
329
|
await this.query(`UPDATE "${this.tableName}" SET ${setClauses} WHERE path = '${sqlStr(row.path)}'`);
|
|
257
330
|
} else {
|
|
258
331
|
const id = randomUUID();
|
|
259
|
-
let cols =
|
|
260
|
-
let vals = `'${id}', '${sqlStr(row.path)}', '${sqlStr(row.filename)}', E'${sqlStr(row.contentText)}', '${sqlStr(row.mimeType)}', ${row.sizeBytes}, '${cd}', '${lud}'`;
|
|
332
|
+
let cols = `id, path, filename, summary, ${SUMMARY_EMBEDDING_COL}, mime_type, size_bytes, creation_date, last_update_date`;
|
|
333
|
+
let vals = `'${id}', '${sqlStr(row.path)}', '${sqlStr(row.filename)}', E'${sqlStr(row.contentText)}', NULL, '${sqlStr(row.mimeType)}', ${row.sizeBytes}, '${cd}', '${lud}'`;
|
|
261
334
|
if (row.project !== void 0) {
|
|
262
335
|
cols += ", project";
|
|
263
336
|
vals += `, '${sqlStr(row.project)}'`;
|
|
@@ -282,48 +355,83 @@ var DeeplakeApi = class {
|
|
|
282
355
|
buildLookupIndexName(table, suffix) {
|
|
283
356
|
return `idx_${table}_${suffix}`.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
284
357
|
}
|
|
285
|
-
getLookupIndexMarkerPath(table, suffix) {
|
|
286
|
-
const markerKey = [
|
|
287
|
-
this.workspaceId,
|
|
288
|
-
this.orgId,
|
|
289
|
-
table,
|
|
290
|
-
suffix
|
|
291
|
-
].join("__").replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
292
|
-
return join3(getIndexMarkerDir(), `${markerKey}.json`);
|
|
293
|
-
}
|
|
294
|
-
hasFreshLookupIndexMarker(table, suffix) {
|
|
295
|
-
const markerPath = this.getLookupIndexMarkerPath(table, suffix);
|
|
296
|
-
if (!existsSync2(markerPath))
|
|
297
|
-
return false;
|
|
298
|
-
try {
|
|
299
|
-
const raw = JSON.parse(readFileSync2(markerPath, "utf-8"));
|
|
300
|
-
const updatedAt = raw.updatedAt ? new Date(raw.updatedAt).getTime() : NaN;
|
|
301
|
-
if (!Number.isFinite(updatedAt) || Date.now() - updatedAt > INDEX_MARKER_TTL_MS)
|
|
302
|
-
return false;
|
|
303
|
-
return true;
|
|
304
|
-
} catch {
|
|
305
|
-
return false;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
markLookupIndexReady(table, suffix) {
|
|
309
|
-
mkdirSync(getIndexMarkerDir(), { recursive: true });
|
|
310
|
-
writeFileSync(this.getLookupIndexMarkerPath(table, suffix), JSON.stringify({ updatedAt: (/* @__PURE__ */ new Date()).toISOString() }), "utf-8");
|
|
311
|
-
}
|
|
312
358
|
async ensureLookupIndex(table, suffix, columnsSql) {
|
|
313
|
-
|
|
359
|
+
const markers = await getIndexMarkerStore();
|
|
360
|
+
const markerPath = markers.buildIndexMarkerPath(this.workspaceId, this.orgId, table, suffix);
|
|
361
|
+
if (markers.hasFreshIndexMarker(markerPath))
|
|
314
362
|
return;
|
|
315
363
|
const indexName = this.buildLookupIndexName(table, suffix);
|
|
316
364
|
try {
|
|
317
365
|
await this.query(`CREATE INDEX IF NOT EXISTS "${indexName}" ON "${table}" ${columnsSql}`);
|
|
318
|
-
|
|
366
|
+
markers.writeIndexMarker(markerPath);
|
|
319
367
|
} catch (e) {
|
|
320
368
|
if (isDuplicateIndexError(e)) {
|
|
321
|
-
|
|
369
|
+
markers.writeIndexMarker(markerPath);
|
|
322
370
|
return;
|
|
323
371
|
}
|
|
324
372
|
log2(`index "${indexName}" skipped: ${e.message}`);
|
|
325
373
|
}
|
|
326
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* Ensure a vector column exists on the given table.
|
|
377
|
+
*
|
|
378
|
+
* The previous implementation always issued `ALTER TABLE ADD COLUMN IF NOT
|
|
379
|
+
* EXISTS …` on every SessionStart. On a long-running workspace that's
|
|
380
|
+
* already migrated, every call returns 500 "Column already exists" — noisy
|
|
381
|
+
* in the log and a wasted round-trip. Worse, the very first call after the
|
|
382
|
+
* column is genuinely added triggers Deeplake's post-ALTER `vector::at`
|
|
383
|
+
* window (~30s) during which subsequent INSERTs fail; minimising the
|
|
384
|
+
* number of ALTER calls minimises exposure to that window.
|
|
385
|
+
*
|
|
386
|
+
* New flow:
|
|
387
|
+
* 1. Check the local marker file (mirrors ensureLookupIndex). If fresh,
|
|
388
|
+
* return — zero network calls.
|
|
389
|
+
* 2. SELECT 1 FROM information_schema.columns WHERE table_name = T AND
|
|
390
|
+
* column_name = C. Read-only, idempotent, can't tickle the post-ALTER
|
|
391
|
+
* bug. If the column is present → mark + return.
|
|
392
|
+
* 3. Only if step 2 says the column is missing, fall back to ALTER ADD
|
|
393
|
+
* COLUMN IF NOT EXISTS. Mark on success, also mark if Deeplake reports
|
|
394
|
+
* "already exists" (race: another client added it between our SELECT
|
|
395
|
+
* and ALTER).
|
|
396
|
+
*
|
|
397
|
+
* Marker uses the same dir / TTL as ensureLookupIndex so both schema
|
|
398
|
+
* caches share an opt-out (HIVEMIND_INDEX_MARKER_DIR) and a TTL knob.
|
|
399
|
+
*/
|
|
400
|
+
async ensureEmbeddingColumn(table, column) {
|
|
401
|
+
await this.ensureColumn(table, column, "FLOAT4[]");
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Generic marker-gated column migration. Same SELECT-then-ALTER flow as
|
|
405
|
+
* ensureEmbeddingColumn, parameterized by SQL type so it can patch up any
|
|
406
|
+
* column that was added to the schema after the table was originally
|
|
407
|
+
* created. Used today for `summary_embedding`, `message_embedding`, and
|
|
408
|
+
* the `agent` column (added 2026-04-11) — the latter has no fallback if
|
|
409
|
+
* a user upgraded over a pre-2026-04-11 table, so every INSERT fails
|
|
410
|
+
* with `column "agent" does not exist`.
|
|
411
|
+
*/
|
|
412
|
+
async ensureColumn(table, column, sqlType) {
|
|
413
|
+
const markers = await getIndexMarkerStore();
|
|
414
|
+
const markerPath = markers.buildIndexMarkerPath(this.workspaceId, this.orgId, table, `col_${column}`);
|
|
415
|
+
if (markers.hasFreshIndexMarker(markerPath))
|
|
416
|
+
return;
|
|
417
|
+
const colCheck = `SELECT 1 FROM information_schema.columns WHERE table_name = '${sqlStr(table)}' AND column_name = '${sqlStr(column)}' AND table_schema = '${sqlStr(this.workspaceId)}' LIMIT 1`;
|
|
418
|
+
const rows = await this.query(colCheck);
|
|
419
|
+
if (rows.length > 0) {
|
|
420
|
+
markers.writeIndexMarker(markerPath);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
await this.query(`ALTER TABLE "${table}" ADD COLUMN ${column} ${sqlType}`);
|
|
425
|
+
} catch (e) {
|
|
426
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
427
|
+
if (!/already exists/i.test(msg))
|
|
428
|
+
throw e;
|
|
429
|
+
const recheck = await this.query(colCheck);
|
|
430
|
+
if (recheck.length === 0)
|
|
431
|
+
throw e;
|
|
432
|
+
}
|
|
433
|
+
markers.writeIndexMarker(markerPath);
|
|
434
|
+
}
|
|
327
435
|
/** List all tables in the workspace (with retry). */
|
|
328
436
|
async listTables(forceRefresh = false) {
|
|
329
437
|
if (!forceRefresh && this._tablesCache)
|
|
@@ -339,7 +447,8 @@ var DeeplakeApi = class {
|
|
|
339
447
|
const resp = await fetch(`${this.apiUrl}/workspaces/${this.workspaceId}/tables`, {
|
|
340
448
|
headers: {
|
|
341
449
|
Authorization: `Bearer ${this.token}`,
|
|
342
|
-
"X-Activeloop-Org-Id": this.orgId
|
|
450
|
+
"X-Activeloop-Org-Id": this.orgId,
|
|
451
|
+
...deeplakeClientHeader()
|
|
343
452
|
}
|
|
344
453
|
});
|
|
345
454
|
if (resp.ok) {
|
|
@@ -364,29 +473,84 @@ var DeeplakeApi = class {
|
|
|
364
473
|
}
|
|
365
474
|
return { tables: [], cacheable: false };
|
|
366
475
|
}
|
|
476
|
+
/**
|
|
477
|
+
* Run a `CREATE TABLE` with an extra outer retry budget. The base
|
|
478
|
+
* `query()` already retries 3 times on fetch errors (~3.5s total), but a
|
|
479
|
+
* failed CREATE is permanent corruption — every subsequent SELECT against
|
|
480
|
+
* the missing table fails. Wrapping in an outer loop with longer backoff
|
|
481
|
+
* (2s, 5s, then 10s) gives us ~17s of reach across transient network
|
|
482
|
+
* blips before giving up. Failures still propagate; getApi() resets its
|
|
483
|
+
* cache on init failure (openclaw plugin) so the next call retries the
|
|
484
|
+
* whole init flow.
|
|
485
|
+
*/
|
|
486
|
+
async createTableWithRetry(sql, label) {
|
|
487
|
+
const OUTER_BACKOFFS_MS = [2e3, 5e3, 1e4];
|
|
488
|
+
let lastErr = null;
|
|
489
|
+
for (let attempt = 0; attempt <= OUTER_BACKOFFS_MS.length; attempt++) {
|
|
490
|
+
try {
|
|
491
|
+
await this.query(sql);
|
|
492
|
+
return;
|
|
493
|
+
} catch (err) {
|
|
494
|
+
lastErr = err;
|
|
495
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
496
|
+
log2(`CREATE TABLE "${label}" attempt ${attempt + 1}/${OUTER_BACKOFFS_MS.length + 1} failed: ${msg}`);
|
|
497
|
+
if (attempt < OUTER_BACKOFFS_MS.length) {
|
|
498
|
+
await sleep(OUTER_BACKOFFS_MS[attempt]);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
throw lastErr;
|
|
503
|
+
}
|
|
367
504
|
/** Create the memory table if it doesn't already exist. Migrate columns on existing tables. */
|
|
368
505
|
async ensureTable(name) {
|
|
369
|
-
const tbl = name ?? this.tableName;
|
|
506
|
+
const tbl = sqlIdent(name ?? this.tableName);
|
|
370
507
|
const tables = await this.listTables();
|
|
371
508
|
if (!tables.includes(tbl)) {
|
|
372
509
|
log2(`table "${tbl}" not found, creating`);
|
|
373
|
-
await this.
|
|
510
|
+
await this.createTableWithRetry(`CREATE TABLE IF NOT EXISTS "${tbl}" (id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', filename TEXT NOT NULL DEFAULT '', summary TEXT NOT NULL DEFAULT '', summary_embedding FLOAT4[], author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'text/plain', size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT '') USING deeplake`, tbl);
|
|
374
511
|
log2(`table "${tbl}" created`);
|
|
375
512
|
if (!tables.includes(tbl))
|
|
376
513
|
this._tablesCache = [...tables, tbl];
|
|
377
514
|
}
|
|
515
|
+
await this.ensureEmbeddingColumn(tbl, SUMMARY_EMBEDDING_COL);
|
|
516
|
+
await this.ensureColumn(tbl, "agent", "TEXT NOT NULL DEFAULT ''");
|
|
378
517
|
}
|
|
379
518
|
/** Create the sessions table (uses JSONB for message since every row is a JSON event). */
|
|
380
519
|
async ensureSessionsTable(name) {
|
|
520
|
+
const safe = sqlIdent(name);
|
|
521
|
+
const tables = await this.listTables();
|
|
522
|
+
if (!tables.includes(safe)) {
|
|
523
|
+
log2(`table "${safe}" not found, creating`);
|
|
524
|
+
await this.createTableWithRetry(`CREATE TABLE IF NOT EXISTS "${safe}" (id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', filename TEXT NOT NULL DEFAULT '', message JSONB, message_embedding FLOAT4[], author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'application/json', size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT '') USING deeplake`, safe);
|
|
525
|
+
log2(`table "${safe}" created`);
|
|
526
|
+
if (!tables.includes(safe))
|
|
527
|
+
this._tablesCache = [...tables, safe];
|
|
528
|
+
}
|
|
529
|
+
await this.ensureEmbeddingColumn(safe, MESSAGE_EMBEDDING_COL);
|
|
530
|
+
await this.ensureColumn(safe, "agent", "TEXT NOT NULL DEFAULT ''");
|
|
531
|
+
await this.ensureLookupIndex(safe, "path_creation_date", `("path", "creation_date")`);
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Create the skills table.
|
|
535
|
+
*
|
|
536
|
+
* One row per skill version. Workers INSERT a fresh row on every KEEP /
|
|
537
|
+
* MERGE rather than UPDATE-ing in place, so the full version history is
|
|
538
|
+
* recoverable. Uniqueness in the *current* state is by (project_key, name)
|
|
539
|
+
* — newer rows shadow older ones at read time (ORDER BY version DESC).
|
|
540
|
+
* This sidesteps the Deeplake UPDATE-coalescing quirk that bit the wiki
|
|
541
|
+
* worker.
|
|
542
|
+
*/
|
|
543
|
+
async ensureSkillsTable(name) {
|
|
544
|
+
const safe = sqlIdent(name);
|
|
381
545
|
const tables = await this.listTables();
|
|
382
|
-
if (!tables.includes(
|
|
383
|
-
log2(`table "${
|
|
384
|
-
await this.
|
|
385
|
-
log2(`table "${
|
|
386
|
-
if (!tables.includes(
|
|
387
|
-
this._tablesCache = [...tables,
|
|
546
|
+
if (!tables.includes(safe)) {
|
|
547
|
+
log2(`table "${safe}" not found, creating`);
|
|
548
|
+
await this.createTableWithRetry(`CREATE TABLE IF NOT EXISTS "${safe}" (id TEXT NOT NULL DEFAULT '', name TEXT NOT NULL DEFAULT '', project TEXT NOT NULL DEFAULT '', project_key TEXT NOT NULL DEFAULT '', local_path TEXT NOT NULL DEFAULT '', install TEXT NOT NULL DEFAULT 'project', source_sessions TEXT NOT NULL DEFAULT '[]', source_agent TEXT NOT NULL DEFAULT '', scope TEXT NOT NULL DEFAULT 'me', author TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', trigger_text TEXT NOT NULL DEFAULT '', body TEXT NOT NULL DEFAULT '', version BIGINT NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT '', updated_at TEXT NOT NULL DEFAULT '') USING deeplake`, safe);
|
|
549
|
+
log2(`table "${safe}" created`);
|
|
550
|
+
if (!tables.includes(safe))
|
|
551
|
+
this._tablesCache = [...tables, safe];
|
|
388
552
|
}
|
|
389
|
-
await this.ensureLookupIndex(
|
|
553
|
+
await this.ensureLookupIndex(safe, "project_key_name", `("project_key", "name")`);
|
|
390
554
|
}
|
|
391
555
|
};
|
|
392
556
|
|
|
@@ -396,25 +560,306 @@ function buildSessionPath(config, sessionId) {
|
|
|
396
560
|
return `/sessions/${config.userName}/${config.userName}_${config.orgName}_${workspace}_${sessionId}.jsonl`;
|
|
397
561
|
}
|
|
398
562
|
|
|
399
|
-
// dist/src/
|
|
400
|
-
import {
|
|
563
|
+
// dist/src/embeddings/client.js
|
|
564
|
+
import { connect } from "node:net";
|
|
565
|
+
import { spawn } from "node:child_process";
|
|
566
|
+
import { openSync, closeSync, writeSync, unlinkSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
401
567
|
import { homedir as homedir3 } from "node:os";
|
|
402
568
|
import { join as join4 } from "node:path";
|
|
569
|
+
|
|
570
|
+
// dist/src/embeddings/protocol.js
|
|
571
|
+
var DEFAULT_SOCKET_DIR = "/tmp";
|
|
572
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
573
|
+
var DEFAULT_CLIENT_TIMEOUT_MS = 2e3;
|
|
574
|
+
function socketPathFor(uid, dir = DEFAULT_SOCKET_DIR) {
|
|
575
|
+
return `${dir}/hivemind-embed-${uid}.sock`;
|
|
576
|
+
}
|
|
577
|
+
function pidPathFor(uid, dir = DEFAULT_SOCKET_DIR) {
|
|
578
|
+
return `${dir}/hivemind-embed-${uid}.pid`;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// dist/src/embeddings/client.js
|
|
582
|
+
var SHARED_DAEMON_PATH = join4(homedir3(), ".hivemind", "embed-deps", "embed-daemon.js");
|
|
583
|
+
var log3 = (m) => log("embed-client", m);
|
|
584
|
+
function getUid() {
|
|
585
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
586
|
+
return uid !== void 0 ? String(uid) : process.env.USER ?? "default";
|
|
587
|
+
}
|
|
588
|
+
var EmbedClient = class {
|
|
589
|
+
socketPath;
|
|
590
|
+
pidPath;
|
|
591
|
+
timeoutMs;
|
|
592
|
+
daemonEntry;
|
|
593
|
+
autoSpawn;
|
|
594
|
+
spawnWaitMs;
|
|
595
|
+
nextId = 0;
|
|
596
|
+
constructor(opts = {}) {
|
|
597
|
+
const uid = getUid();
|
|
598
|
+
const dir = opts.socketDir ?? "/tmp";
|
|
599
|
+
this.socketPath = socketPathFor(uid, dir);
|
|
600
|
+
this.pidPath = pidPathFor(uid, dir);
|
|
601
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_CLIENT_TIMEOUT_MS;
|
|
602
|
+
this.daemonEntry = opts.daemonEntry ?? process.env.HIVEMIND_EMBED_DAEMON ?? (existsSync3(SHARED_DAEMON_PATH) ? SHARED_DAEMON_PATH : void 0);
|
|
603
|
+
this.autoSpawn = opts.autoSpawn ?? true;
|
|
604
|
+
this.spawnWaitMs = opts.spawnWaitMs ?? 5e3;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Returns an embedding vector, or null on timeout/failure. Hooks MUST treat
|
|
608
|
+
* null as "skip embedding column" — never block the write path on us.
|
|
609
|
+
*
|
|
610
|
+
* Fire-and-forget spawn on miss: if the daemon isn't up, this call returns
|
|
611
|
+
* null AND kicks off a background spawn. The next call finds a ready daemon.
|
|
612
|
+
*/
|
|
613
|
+
async embed(text, kind = "document") {
|
|
614
|
+
let sock;
|
|
615
|
+
try {
|
|
616
|
+
sock = await this.connectOnce();
|
|
617
|
+
} catch {
|
|
618
|
+
if (this.autoSpawn)
|
|
619
|
+
this.trySpawnDaemon();
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
const id = String(++this.nextId);
|
|
624
|
+
const req = { op: "embed", id, kind, text };
|
|
625
|
+
const resp = await this.sendAndWait(sock, req);
|
|
626
|
+
if (resp.error || !("embedding" in resp) || !resp.embedding) {
|
|
627
|
+
log3(`embed err: ${resp.error ?? "no embedding"}`);
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
return resp.embedding;
|
|
631
|
+
} catch (e) {
|
|
632
|
+
const err = e instanceof Error ? e.message : String(e);
|
|
633
|
+
log3(`embed failed: ${err}`);
|
|
634
|
+
return null;
|
|
635
|
+
} finally {
|
|
636
|
+
try {
|
|
637
|
+
sock.end();
|
|
638
|
+
} catch {
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Wait up to spawnWaitMs for the daemon to accept connections, spawning if
|
|
644
|
+
* necessary. Meant for SessionStart / long-running batches — not the hot path.
|
|
645
|
+
*/
|
|
646
|
+
async warmup() {
|
|
647
|
+
try {
|
|
648
|
+
const s = await this.connectOnce();
|
|
649
|
+
s.end();
|
|
650
|
+
return true;
|
|
651
|
+
} catch {
|
|
652
|
+
if (!this.autoSpawn)
|
|
653
|
+
return false;
|
|
654
|
+
this.trySpawnDaemon();
|
|
655
|
+
try {
|
|
656
|
+
const s = await this.waitForSocket();
|
|
657
|
+
s.end();
|
|
658
|
+
return true;
|
|
659
|
+
} catch {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
connectOnce() {
|
|
665
|
+
return new Promise((resolve, reject) => {
|
|
666
|
+
const sock = connect(this.socketPath);
|
|
667
|
+
const to = setTimeout(() => {
|
|
668
|
+
sock.destroy();
|
|
669
|
+
reject(new Error("connect timeout"));
|
|
670
|
+
}, this.timeoutMs);
|
|
671
|
+
sock.once("connect", () => {
|
|
672
|
+
clearTimeout(to);
|
|
673
|
+
resolve(sock);
|
|
674
|
+
});
|
|
675
|
+
sock.once("error", (e) => {
|
|
676
|
+
clearTimeout(to);
|
|
677
|
+
reject(e);
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
trySpawnDaemon() {
|
|
682
|
+
let fd;
|
|
683
|
+
try {
|
|
684
|
+
fd = openSync(this.pidPath, "wx", 384);
|
|
685
|
+
writeSync(fd, String(process.pid));
|
|
686
|
+
} catch (e) {
|
|
687
|
+
if (this.isPidFileStale()) {
|
|
688
|
+
try {
|
|
689
|
+
unlinkSync(this.pidPath);
|
|
690
|
+
} catch {
|
|
691
|
+
}
|
|
692
|
+
try {
|
|
693
|
+
fd = openSync(this.pidPath, "wx", 384);
|
|
694
|
+
writeSync(fd, String(process.pid));
|
|
695
|
+
} catch {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
} else {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (!this.daemonEntry || !existsSync3(this.daemonEntry)) {
|
|
703
|
+
log3(`daemonEntry not configured or missing: ${this.daemonEntry}`);
|
|
704
|
+
try {
|
|
705
|
+
closeSync(fd);
|
|
706
|
+
unlinkSync(this.pidPath);
|
|
707
|
+
} catch {
|
|
708
|
+
}
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
try {
|
|
712
|
+
const child = spawn(process.execPath, [this.daemonEntry], {
|
|
713
|
+
detached: true,
|
|
714
|
+
stdio: "ignore",
|
|
715
|
+
env: process.env
|
|
716
|
+
});
|
|
717
|
+
child.unref();
|
|
718
|
+
log3(`spawned daemon pid=${child.pid}`);
|
|
719
|
+
} finally {
|
|
720
|
+
closeSync(fd);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
isPidFileStale() {
|
|
724
|
+
try {
|
|
725
|
+
const raw = readFileSync3(this.pidPath, "utf-8").trim();
|
|
726
|
+
const pid = Number(raw);
|
|
727
|
+
if (!pid || Number.isNaN(pid))
|
|
728
|
+
return true;
|
|
729
|
+
try {
|
|
730
|
+
process.kill(pid, 0);
|
|
731
|
+
return false;
|
|
732
|
+
} catch {
|
|
733
|
+
return true;
|
|
734
|
+
}
|
|
735
|
+
} catch {
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
async waitForSocket() {
|
|
740
|
+
const deadline = Date.now() + this.spawnWaitMs;
|
|
741
|
+
let delay = 30;
|
|
742
|
+
while (Date.now() < deadline) {
|
|
743
|
+
await sleep2(delay);
|
|
744
|
+
delay = Math.min(delay * 1.5, 300);
|
|
745
|
+
if (!existsSync3(this.socketPath))
|
|
746
|
+
continue;
|
|
747
|
+
try {
|
|
748
|
+
return await this.connectOnce();
|
|
749
|
+
} catch {
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
throw new Error("daemon did not become ready within spawnWaitMs");
|
|
753
|
+
}
|
|
754
|
+
sendAndWait(sock, req) {
|
|
755
|
+
return new Promise((resolve, reject) => {
|
|
756
|
+
let buf = "";
|
|
757
|
+
const to = setTimeout(() => {
|
|
758
|
+
sock.destroy();
|
|
759
|
+
reject(new Error("request timeout"));
|
|
760
|
+
}, this.timeoutMs);
|
|
761
|
+
sock.setEncoding("utf-8");
|
|
762
|
+
sock.on("data", (chunk) => {
|
|
763
|
+
buf += chunk;
|
|
764
|
+
const nl = buf.indexOf("\n");
|
|
765
|
+
if (nl === -1)
|
|
766
|
+
return;
|
|
767
|
+
const line = buf.slice(0, nl);
|
|
768
|
+
clearTimeout(to);
|
|
769
|
+
try {
|
|
770
|
+
resolve(JSON.parse(line));
|
|
771
|
+
} catch (e) {
|
|
772
|
+
reject(e);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
sock.on("error", (e) => {
|
|
776
|
+
clearTimeout(to);
|
|
777
|
+
reject(e);
|
|
778
|
+
});
|
|
779
|
+
sock.on("end", () => {
|
|
780
|
+
clearTimeout(to);
|
|
781
|
+
reject(new Error("connection closed without response"));
|
|
782
|
+
});
|
|
783
|
+
sock.write(JSON.stringify(req) + "\n");
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
function sleep2(ms) {
|
|
788
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// dist/src/embeddings/sql.js
|
|
792
|
+
function embeddingSqlLiteral(vec) {
|
|
793
|
+
if (!vec || vec.length === 0)
|
|
794
|
+
return "NULL";
|
|
795
|
+
const parts = [];
|
|
796
|
+
for (const v of vec) {
|
|
797
|
+
if (!Number.isFinite(v))
|
|
798
|
+
return "NULL";
|
|
799
|
+
parts.push(String(v));
|
|
800
|
+
}
|
|
801
|
+
return `ARRAY[${parts.join(",")}]::float4[]`;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// dist/src/embeddings/disable.js
|
|
805
|
+
import { createRequire } from "node:module";
|
|
806
|
+
import { homedir as homedir4 } from "node:os";
|
|
807
|
+
import { join as join5 } from "node:path";
|
|
808
|
+
import { pathToFileURL } from "node:url";
|
|
809
|
+
var cachedStatus = null;
|
|
810
|
+
function defaultResolveTransformers() {
|
|
811
|
+
try {
|
|
812
|
+
createRequire(import.meta.url).resolve("@huggingface/transformers");
|
|
813
|
+
return;
|
|
814
|
+
} catch {
|
|
815
|
+
}
|
|
816
|
+
const sharedDir = join5(homedir4(), ".hivemind", "embed-deps");
|
|
817
|
+
createRequire(pathToFileURL(`${sharedDir}/`).href).resolve("@huggingface/transformers");
|
|
818
|
+
}
|
|
819
|
+
var _resolve = defaultResolveTransformers;
|
|
820
|
+
function detectStatus() {
|
|
821
|
+
if (process.env.HIVEMIND_EMBEDDINGS === "false")
|
|
822
|
+
return "env-disabled";
|
|
823
|
+
try {
|
|
824
|
+
_resolve();
|
|
825
|
+
return "enabled";
|
|
826
|
+
} catch {
|
|
827
|
+
return "no-transformers";
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function embeddingsStatus() {
|
|
831
|
+
if (cachedStatus !== null)
|
|
832
|
+
return cachedStatus;
|
|
833
|
+
cachedStatus = detectStatus();
|
|
834
|
+
return cachedStatus;
|
|
835
|
+
}
|
|
836
|
+
function embeddingsDisabled() {
|
|
837
|
+
return embeddingsStatus() !== "enabled";
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// dist/src/hooks/codex/capture.js
|
|
841
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
842
|
+
import { dirname as dirname2, join as join9 } from "node:path";
|
|
843
|
+
|
|
844
|
+
// dist/src/hooks/summary-state.js
|
|
845
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, writeSync as writeSync2, mkdirSync as mkdirSync2, renameSync, existsSync as existsSync4, unlinkSync as unlinkSync2, openSync as openSync2, closeSync as closeSync2 } from "node:fs";
|
|
846
|
+
import { homedir as homedir5 } from "node:os";
|
|
847
|
+
import { join as join6 } from "node:path";
|
|
403
848
|
var dlog = (msg) => log("summary-state", msg);
|
|
404
|
-
var STATE_DIR =
|
|
849
|
+
var STATE_DIR = join6(homedir5(), ".claude", "hooks", "summary-state");
|
|
405
850
|
var YIELD_BUF = new Int32Array(new SharedArrayBuffer(4));
|
|
406
851
|
function statePath(sessionId) {
|
|
407
|
-
return
|
|
852
|
+
return join6(STATE_DIR, `${sessionId}.json`);
|
|
408
853
|
}
|
|
409
854
|
function lockPath(sessionId) {
|
|
410
|
-
return
|
|
855
|
+
return join6(STATE_DIR, `${sessionId}.lock`);
|
|
411
856
|
}
|
|
412
857
|
function readState(sessionId) {
|
|
413
858
|
const p = statePath(sessionId);
|
|
414
|
-
if (!
|
|
859
|
+
if (!existsSync4(p))
|
|
415
860
|
return null;
|
|
416
861
|
try {
|
|
417
|
-
return JSON.parse(
|
|
862
|
+
return JSON.parse(readFileSync4(p, "utf-8"));
|
|
418
863
|
} catch {
|
|
419
864
|
return null;
|
|
420
865
|
}
|
|
@@ -433,14 +878,14 @@ function withRmwLock(sessionId, fn) {
|
|
|
433
878
|
let fd = null;
|
|
434
879
|
while (fd === null) {
|
|
435
880
|
try {
|
|
436
|
-
fd =
|
|
881
|
+
fd = openSync2(rmwLock, "wx");
|
|
437
882
|
} catch (e) {
|
|
438
883
|
if (e.code !== "EEXIST")
|
|
439
884
|
throw e;
|
|
440
885
|
if (Date.now() > deadline) {
|
|
441
886
|
dlog(`rmw lock deadline exceeded for ${sessionId}, reclaiming stale lock`);
|
|
442
887
|
try {
|
|
443
|
-
|
|
888
|
+
unlinkSync2(rmwLock);
|
|
444
889
|
} catch (unlinkErr) {
|
|
445
890
|
dlog(`stale rmw lock unlink failed for ${sessionId}: ${unlinkErr.message}`);
|
|
446
891
|
}
|
|
@@ -452,9 +897,9 @@ function withRmwLock(sessionId, fn) {
|
|
|
452
897
|
try {
|
|
453
898
|
return fn();
|
|
454
899
|
} finally {
|
|
455
|
-
|
|
900
|
+
closeSync2(fd);
|
|
456
901
|
try {
|
|
457
|
-
|
|
902
|
+
unlinkSync2(rmwLock);
|
|
458
903
|
} catch (unlinkErr) {
|
|
459
904
|
dlog(`rmw lock cleanup failed for ${sessionId}: ${unlinkErr.message}`);
|
|
460
905
|
}
|
|
@@ -491,27 +936,27 @@ function shouldTrigger(state, cfg, now = Date.now()) {
|
|
|
491
936
|
function tryAcquireLock(sessionId, maxAgeMs = 10 * 60 * 1e3) {
|
|
492
937
|
mkdirSync2(STATE_DIR, { recursive: true });
|
|
493
938
|
const p = lockPath(sessionId);
|
|
494
|
-
if (
|
|
939
|
+
if (existsSync4(p)) {
|
|
495
940
|
try {
|
|
496
|
-
const ageMs = Date.now() - parseInt(
|
|
941
|
+
const ageMs = Date.now() - parseInt(readFileSync4(p, "utf-8"), 10);
|
|
497
942
|
if (Number.isFinite(ageMs) && ageMs < maxAgeMs)
|
|
498
943
|
return false;
|
|
499
944
|
} catch (readErr) {
|
|
500
945
|
dlog(`lock file unreadable for ${sessionId}, treating as stale: ${readErr.message}`);
|
|
501
946
|
}
|
|
502
947
|
try {
|
|
503
|
-
|
|
948
|
+
unlinkSync2(p);
|
|
504
949
|
} catch (unlinkErr) {
|
|
505
950
|
dlog(`could not unlink stale lock for ${sessionId}: ${unlinkErr.message}`);
|
|
506
951
|
return false;
|
|
507
952
|
}
|
|
508
953
|
}
|
|
509
954
|
try {
|
|
510
|
-
const fd =
|
|
955
|
+
const fd = openSync2(p, "wx");
|
|
511
956
|
try {
|
|
512
|
-
|
|
957
|
+
writeSync2(fd, String(Date.now()));
|
|
513
958
|
} finally {
|
|
514
|
-
|
|
959
|
+
closeSync2(fd);
|
|
515
960
|
}
|
|
516
961
|
return true;
|
|
517
962
|
} catch (e) {
|
|
@@ -522,7 +967,7 @@ function tryAcquireLock(sessionId, maxAgeMs = 10 * 60 * 1e3) {
|
|
|
522
967
|
}
|
|
523
968
|
function releaseLock(sessionId) {
|
|
524
969
|
try {
|
|
525
|
-
|
|
970
|
+
unlinkSync2(lockPath(sessionId));
|
|
526
971
|
} catch (e) {
|
|
527
972
|
if (e?.code !== "ENOENT") {
|
|
528
973
|
dlog(`releaseLock unlink failed for ${sessionId}: ${e.message}`);
|
|
@@ -531,17 +976,17 @@ function releaseLock(sessionId) {
|
|
|
531
976
|
}
|
|
532
977
|
|
|
533
978
|
// dist/src/hooks/codex/spawn-wiki-worker.js
|
|
534
|
-
import { spawn, execSync } from "node:child_process";
|
|
979
|
+
import { spawn as spawn2, execSync } from "node:child_process";
|
|
535
980
|
import { fileURLToPath } from "node:url";
|
|
536
|
-
import { dirname, join as
|
|
981
|
+
import { dirname, join as join8 } from "node:path";
|
|
537
982
|
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
|
|
538
|
-
import { homedir as
|
|
983
|
+
import { homedir as homedir6, tmpdir as tmpdir2 } from "node:os";
|
|
539
984
|
|
|
540
985
|
// dist/src/utils/wiki-log.js
|
|
541
986
|
import { mkdirSync as mkdirSync3, appendFileSync as appendFileSync2 } from "node:fs";
|
|
542
|
-
import { join as
|
|
987
|
+
import { join as join7 } from "node:path";
|
|
543
988
|
function makeWikiLogger(hooksDir, filename = "deeplake-wiki.log") {
|
|
544
|
-
const path =
|
|
989
|
+
const path = join7(hooksDir, filename);
|
|
545
990
|
return {
|
|
546
991
|
path,
|
|
547
992
|
log(msg) {
|
|
@@ -556,8 +1001,8 @@ function makeWikiLogger(hooksDir, filename = "deeplake-wiki.log") {
|
|
|
556
1001
|
}
|
|
557
1002
|
|
|
558
1003
|
// dist/src/hooks/codex/spawn-wiki-worker.js
|
|
559
|
-
var HOME =
|
|
560
|
-
var wikiLogger = makeWikiLogger(
|
|
1004
|
+
var HOME = homedir6();
|
|
1005
|
+
var wikiLogger = makeWikiLogger(join8(HOME, ".codex", "hooks"));
|
|
561
1006
|
var WIKI_LOG = wikiLogger.path;
|
|
562
1007
|
var WIKI_PROMPT_TEMPLATE = `You are building a personal wiki from a coding session. Your goal is to extract every piece of knowledge \u2014 entities, decisions, relationships, and facts \u2014 into a structured, searchable wiki entry.
|
|
563
1008
|
|
|
@@ -619,9 +1064,9 @@ function findCodexBin() {
|
|
|
619
1064
|
function spawnCodexWikiWorker(opts) {
|
|
620
1065
|
const { config, sessionId, cwd, bundleDir, reason } = opts;
|
|
621
1066
|
const projectName = cwd.split("/").pop() || "unknown";
|
|
622
|
-
const tmpDir =
|
|
1067
|
+
const tmpDir = join8(tmpdir2(), `deeplake-wiki-${sessionId}-${Date.now()}`);
|
|
623
1068
|
mkdirSync4(tmpDir, { recursive: true });
|
|
624
|
-
const configFile =
|
|
1069
|
+
const configFile = join8(tmpDir, "config.json");
|
|
625
1070
|
writeFileSync3(configFile, JSON.stringify({
|
|
626
1071
|
apiUrl: config.apiUrl,
|
|
627
1072
|
token: config.token,
|
|
@@ -635,12 +1080,12 @@ function spawnCodexWikiWorker(opts) {
|
|
|
635
1080
|
tmpDir,
|
|
636
1081
|
codexBin: findCodexBin(),
|
|
637
1082
|
wikiLog: WIKI_LOG,
|
|
638
|
-
hooksDir:
|
|
1083
|
+
hooksDir: join8(HOME, ".codex", "hooks"),
|
|
639
1084
|
promptTemplate: WIKI_PROMPT_TEMPLATE
|
|
640
1085
|
}));
|
|
641
1086
|
wikiLog(`${reason}: spawning summary worker for ${sessionId}`);
|
|
642
|
-
const workerPath =
|
|
643
|
-
|
|
1087
|
+
const workerPath = join8(bundleDir, "wiki-worker.js");
|
|
1088
|
+
spawn2("nohup", ["node", workerPath, configFile], {
|
|
644
1089
|
detached: true,
|
|
645
1090
|
stdio: ["ignore", "ignore", "ignore"]
|
|
646
1091
|
}).unref();
|
|
@@ -651,7 +1096,10 @@ function bundleDirFromImportMeta(importMetaUrl) {
|
|
|
651
1096
|
}
|
|
652
1097
|
|
|
653
1098
|
// dist/src/hooks/codex/capture.js
|
|
654
|
-
var
|
|
1099
|
+
var log4 = (msg) => log("codex-capture", msg);
|
|
1100
|
+
function resolveEmbedDaemonPath() {
|
|
1101
|
+
return join9(dirname2(fileURLToPath2(import.meta.url)), "embeddings", "embed-daemon.js");
|
|
1102
|
+
}
|
|
655
1103
|
var CAPTURE = process.env.HIVEMIND_CAPTURE !== "false";
|
|
656
1104
|
async function main() {
|
|
657
1105
|
if (!CAPTURE)
|
|
@@ -659,7 +1107,7 @@ async function main() {
|
|
|
659
1107
|
const input = await readStdin();
|
|
660
1108
|
const config = loadConfig();
|
|
661
1109
|
if (!config) {
|
|
662
|
-
|
|
1110
|
+
log4("no config");
|
|
663
1111
|
return;
|
|
664
1112
|
}
|
|
665
1113
|
const sessionsTable = config.sessionsTableName;
|
|
@@ -676,7 +1124,7 @@ async function main() {
|
|
|
676
1124
|
};
|
|
677
1125
|
let entry;
|
|
678
1126
|
if (input.hook_event_name === "UserPromptSubmit" && input.prompt !== void 0) {
|
|
679
|
-
|
|
1127
|
+
log4(`user session=${input.session_id}`);
|
|
680
1128
|
entry = {
|
|
681
1129
|
id: crypto.randomUUID(),
|
|
682
1130
|
...meta,
|
|
@@ -684,7 +1132,7 @@ async function main() {
|
|
|
684
1132
|
content: input.prompt
|
|
685
1133
|
};
|
|
686
1134
|
} else if (input.hook_event_name === "PostToolUse" && input.tool_name !== void 0) {
|
|
687
|
-
|
|
1135
|
+
log4(`tool=${input.tool_name} session=${input.session_id}`);
|
|
688
1136
|
entry = {
|
|
689
1137
|
id: crypto.randomUUID(),
|
|
690
1138
|
...meta,
|
|
@@ -695,28 +1143,30 @@ async function main() {
|
|
|
695
1143
|
tool_response: JSON.stringify(input.tool_response)
|
|
696
1144
|
};
|
|
697
1145
|
} else {
|
|
698
|
-
|
|
1146
|
+
log4(`unknown event: ${input.hook_event_name}, skipping`);
|
|
699
1147
|
return;
|
|
700
1148
|
}
|
|
701
1149
|
const sessionPath = buildSessionPath(config, input.session_id);
|
|
702
1150
|
const line = JSON.stringify(entry);
|
|
703
|
-
|
|
1151
|
+
log4(`writing to ${sessionPath}`);
|
|
704
1152
|
const projectName = (input.cwd ?? "").split("/").pop() || "unknown";
|
|
705
1153
|
const filename = sessionPath.split("/").pop() ?? "";
|
|
706
1154
|
const jsonForSql = line.replace(/'/g, "''");
|
|
707
|
-
const
|
|
1155
|
+
const embedding = embeddingsDisabled() ? null : await new EmbedClient({ daemonEntry: resolveEmbedDaemonPath() }).embed(line, "document");
|
|
1156
|
+
const embeddingSql = embeddingSqlLiteral(embedding);
|
|
1157
|
+
const insertSql = `INSERT INTO "${sessionsTable}" (id, path, filename, message, message_embedding, author, size_bytes, project, description, agent, creation_date, last_update_date) VALUES ('${crypto.randomUUID()}', '${sqlStr(sessionPath)}', '${sqlStr(filename)}', '${jsonForSql}'::jsonb, ${embeddingSql}, '${sqlStr(config.userName)}', ${Buffer.byteLength(line, "utf-8")}, '${sqlStr(projectName)}', '${sqlStr(input.hook_event_name ?? "")}', 'codex', '${ts}', '${ts}')`;
|
|
708
1158
|
try {
|
|
709
1159
|
await api.query(insertSql);
|
|
710
1160
|
} catch (e) {
|
|
711
1161
|
if (e.message?.includes("permission denied") || e.message?.includes("does not exist")) {
|
|
712
|
-
|
|
1162
|
+
log4("table missing, creating and retrying");
|
|
713
1163
|
await api.ensureSessionsTable(sessionsTable);
|
|
714
1164
|
await api.query(insertSql);
|
|
715
1165
|
} else {
|
|
716
1166
|
throw e;
|
|
717
1167
|
}
|
|
718
1168
|
}
|
|
719
|
-
|
|
1169
|
+
log4("capture ok");
|
|
720
1170
|
maybeTriggerPeriodicSummary(input.session_id, input.cwd ?? "", config);
|
|
721
1171
|
}
|
|
722
1172
|
function maybeTriggerPeriodicSummary(sessionId, cwd, config) {
|
|
@@ -728,7 +1178,7 @@ function maybeTriggerPeriodicSummary(sessionId, cwd, config) {
|
|
|
728
1178
|
if (!shouldTrigger(state, cfg))
|
|
729
1179
|
return;
|
|
730
1180
|
if (!tryAcquireLock(sessionId)) {
|
|
731
|
-
|
|
1181
|
+
log4(`periodic trigger suppressed (lock held) session=${sessionId}`);
|
|
732
1182
|
return;
|
|
733
1183
|
}
|
|
734
1184
|
wikiLog(`Periodic: threshold hit (total=${state.totalCount}, since=${state.totalCount - state.lastSummaryCount}, N=${cfg.everyNMessages}, hours=${cfg.everyHours})`);
|
|
@@ -741,19 +1191,19 @@ function maybeTriggerPeriodicSummary(sessionId, cwd, config) {
|
|
|
741
1191
|
reason: "Periodic"
|
|
742
1192
|
});
|
|
743
1193
|
} catch (e) {
|
|
744
|
-
|
|
1194
|
+
log4(`periodic spawn failed: ${e.message}`);
|
|
745
1195
|
try {
|
|
746
1196
|
releaseLock(sessionId);
|
|
747
1197
|
} catch (releaseErr) {
|
|
748
|
-
|
|
1198
|
+
log4(`releaseLock after periodic spawn failure also failed: ${releaseErr.message}`);
|
|
749
1199
|
}
|
|
750
1200
|
throw e;
|
|
751
1201
|
}
|
|
752
1202
|
} catch (e) {
|
|
753
|
-
|
|
1203
|
+
log4(`periodic trigger error: ${e.message}`);
|
|
754
1204
|
}
|
|
755
1205
|
}
|
|
756
1206
|
main().catch((e) => {
|
|
757
|
-
|
|
1207
|
+
log4(`fatal: ${e.message}`);
|
|
758
1208
|
process.exit(0);
|
|
759
1209
|
});
|