@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/stop.js
CHANGED
|
@@ -1,7 +1,61 @@
|
|
|
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/hooks/codex/stop.js
|
|
4
|
-
import { readFileSync as
|
|
56
|
+
import { readFileSync as readFileSync7, existsSync as existsSync8 } from "node:fs";
|
|
57
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
58
|
+
import { dirname as dirname3, join as join13 } from "node:path";
|
|
5
59
|
|
|
6
60
|
// dist/src/utils/stdin.js
|
|
7
61
|
function readStdin() {
|
|
@@ -48,15 +102,13 @@ function loadConfig() {
|
|
|
48
102
|
apiUrl: process.env.HIVEMIND_API_URL ?? creds?.apiUrl ?? "https://api.deeplake.ai",
|
|
49
103
|
tableName: process.env.HIVEMIND_TABLE ?? "memory",
|
|
50
104
|
sessionsTableName: process.env.HIVEMIND_SESSIONS_TABLE ?? "sessions",
|
|
105
|
+
skillsTableName: process.env.HIVEMIND_SKILLS_TABLE ?? "skills",
|
|
51
106
|
memoryPath: process.env.HIVEMIND_MEMORY_PATH ?? join(home, ".deeplake", "memory")
|
|
52
107
|
};
|
|
53
108
|
}
|
|
54
109
|
|
|
55
110
|
// dist/src/deeplake-api.js
|
|
56
111
|
import { randomUUID } from "node:crypto";
|
|
57
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
58
|
-
import { join as join3 } from "node:path";
|
|
59
|
-
import { tmpdir } from "node:os";
|
|
60
112
|
|
|
61
113
|
// dist/src/utils/debug.js
|
|
62
114
|
import { appendFileSync } from "node:fs";
|
|
@@ -78,8 +130,33 @@ function log(tag, msg) {
|
|
|
78
130
|
function sqlStr(value) {
|
|
79
131
|
return value.replace(/\\/g, "\\\\").replace(/'/g, "''").replace(/\0/g, "").replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
|
|
80
132
|
}
|
|
133
|
+
function sqlIdent(name) {
|
|
134
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
135
|
+
throw new Error(`Invalid SQL identifier: ${JSON.stringify(name)}`);
|
|
136
|
+
}
|
|
137
|
+
return name;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// dist/src/embeddings/columns.js
|
|
141
|
+
var SUMMARY_EMBEDDING_COL = "summary_embedding";
|
|
142
|
+
var MESSAGE_EMBEDDING_COL = "message_embedding";
|
|
143
|
+
|
|
144
|
+
// dist/src/utils/client-header.js
|
|
145
|
+
var DEEPLAKE_CLIENT_HEADER = "X-Deeplake-Client";
|
|
146
|
+
function deeplakeClientValue() {
|
|
147
|
+
return "hivemind";
|
|
148
|
+
}
|
|
149
|
+
function deeplakeClientHeader() {
|
|
150
|
+
return { [DEEPLAKE_CLIENT_HEADER]: deeplakeClientValue() };
|
|
151
|
+
}
|
|
81
152
|
|
|
82
153
|
// dist/src/deeplake-api.js
|
|
154
|
+
var indexMarkerStorePromise = null;
|
|
155
|
+
function getIndexMarkerStore() {
|
|
156
|
+
if (!indexMarkerStorePromise)
|
|
157
|
+
indexMarkerStorePromise = Promise.resolve().then(() => (init_index_marker_store(), index_marker_store_exports));
|
|
158
|
+
return indexMarkerStorePromise;
|
|
159
|
+
}
|
|
83
160
|
var log2 = (msg) => log("sdk", msg);
|
|
84
161
|
function summarizeSql(sql, maxLen = 220) {
|
|
85
162
|
const compact = sql.replace(/\s+/g, " ").trim();
|
|
@@ -99,7 +176,6 @@ var MAX_RETRIES = 3;
|
|
|
99
176
|
var BASE_DELAY_MS = 500;
|
|
100
177
|
var MAX_CONCURRENCY = 5;
|
|
101
178
|
var QUERY_TIMEOUT_MS = Number(process.env.HIVEMIND_QUERY_TIMEOUT_MS ?? 1e4);
|
|
102
|
-
var INDEX_MARKER_TTL_MS = Number(process.env.HIVEMIND_INDEX_MARKER_TTL_MS ?? 6 * 60 * 6e4);
|
|
103
179
|
function sleep(ms) {
|
|
104
180
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
105
181
|
}
|
|
@@ -119,9 +195,6 @@ function isTransientHtml403(text) {
|
|
|
119
195
|
const body = text.toLowerCase();
|
|
120
196
|
return body.includes("<html") || body.includes("403 forbidden") || body.includes("cloudflare") || body.includes("nginx");
|
|
121
197
|
}
|
|
122
|
-
function getIndexMarkerDir() {
|
|
123
|
-
return process.env.HIVEMIND_INDEX_MARKER_DIR ?? join3(tmpdir(), "hivemind-deeplake-indexes");
|
|
124
|
-
}
|
|
125
198
|
var Semaphore = class {
|
|
126
199
|
max;
|
|
127
200
|
waiting = [];
|
|
@@ -190,7 +263,8 @@ var DeeplakeApi = class {
|
|
|
190
263
|
headers: {
|
|
191
264
|
Authorization: `Bearer ${this.token}`,
|
|
192
265
|
"Content-Type": "application/json",
|
|
193
|
-
"X-Activeloop-Org-Id": this.orgId
|
|
266
|
+
"X-Activeloop-Org-Id": this.orgId,
|
|
267
|
+
...deeplakeClientHeader()
|
|
194
268
|
},
|
|
195
269
|
signal,
|
|
196
270
|
body: JSON.stringify({ query: sql })
|
|
@@ -217,7 +291,8 @@ var DeeplakeApi = class {
|
|
|
217
291
|
}
|
|
218
292
|
const text = await resp.text().catch(() => "");
|
|
219
293
|
const retryable403 = isSessionInsertQuery(sql) && (resp.status === 401 || resp.status === 403 && (text.length === 0 || isTransientHtml403(text)));
|
|
220
|
-
|
|
294
|
+
const alreadyExists = resp.status === 500 && isDuplicateIndexError(text);
|
|
295
|
+
if (!alreadyExists && attempt < MAX_RETRIES && (RETRYABLE_CODES.has(resp.status) || retryable403)) {
|
|
221
296
|
const delay = BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 200;
|
|
222
297
|
log2(`query retry ${attempt + 1}/${MAX_RETRIES} (${resp.status}) in ${delay.toFixed(0)}ms`);
|
|
223
298
|
await sleep(delay);
|
|
@@ -251,7 +326,7 @@ var DeeplakeApi = class {
|
|
|
251
326
|
const lud = row.lastUpdateDate ?? ts;
|
|
252
327
|
const exists = await this.query(`SELECT path FROM "${this.tableName}" WHERE path = '${sqlStr(row.path)}' LIMIT 1`);
|
|
253
328
|
if (exists.length > 0) {
|
|
254
|
-
let setClauses = `summary = E'${sqlStr(row.contentText)}', mime_type = '${sqlStr(row.mimeType)}', size_bytes = ${row.sizeBytes}, last_update_date = '${lud}'`;
|
|
329
|
+
let setClauses = `summary = E'${sqlStr(row.contentText)}', ${SUMMARY_EMBEDDING_COL} = NULL, mime_type = '${sqlStr(row.mimeType)}', size_bytes = ${row.sizeBytes}, last_update_date = '${lud}'`;
|
|
255
330
|
if (row.project !== void 0)
|
|
256
331
|
setClauses += `, project = '${sqlStr(row.project)}'`;
|
|
257
332
|
if (row.description !== void 0)
|
|
@@ -259,8 +334,8 @@ var DeeplakeApi = class {
|
|
|
259
334
|
await this.query(`UPDATE "${this.tableName}" SET ${setClauses} WHERE path = '${sqlStr(row.path)}'`);
|
|
260
335
|
} else {
|
|
261
336
|
const id = randomUUID();
|
|
262
|
-
let cols =
|
|
263
|
-
let vals = `'${id}', '${sqlStr(row.path)}', '${sqlStr(row.filename)}', E'${sqlStr(row.contentText)}', '${sqlStr(row.mimeType)}', ${row.sizeBytes}, '${cd}', '${lud}'`;
|
|
337
|
+
let cols = `id, path, filename, summary, ${SUMMARY_EMBEDDING_COL}, mime_type, size_bytes, creation_date, last_update_date`;
|
|
338
|
+
let vals = `'${id}', '${sqlStr(row.path)}', '${sqlStr(row.filename)}', E'${sqlStr(row.contentText)}', NULL, '${sqlStr(row.mimeType)}', ${row.sizeBytes}, '${cd}', '${lud}'`;
|
|
264
339
|
if (row.project !== void 0) {
|
|
265
340
|
cols += ", project";
|
|
266
341
|
vals += `, '${sqlStr(row.project)}'`;
|
|
@@ -285,48 +360,83 @@ var DeeplakeApi = class {
|
|
|
285
360
|
buildLookupIndexName(table, suffix) {
|
|
286
361
|
return `idx_${table}_${suffix}`.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
287
362
|
}
|
|
288
|
-
getLookupIndexMarkerPath(table, suffix) {
|
|
289
|
-
const markerKey = [
|
|
290
|
-
this.workspaceId,
|
|
291
|
-
this.orgId,
|
|
292
|
-
table,
|
|
293
|
-
suffix
|
|
294
|
-
].join("__").replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
295
|
-
return join3(getIndexMarkerDir(), `${markerKey}.json`);
|
|
296
|
-
}
|
|
297
|
-
hasFreshLookupIndexMarker(table, suffix) {
|
|
298
|
-
const markerPath = this.getLookupIndexMarkerPath(table, suffix);
|
|
299
|
-
if (!existsSync2(markerPath))
|
|
300
|
-
return false;
|
|
301
|
-
try {
|
|
302
|
-
const raw = JSON.parse(readFileSync2(markerPath, "utf-8"));
|
|
303
|
-
const updatedAt = raw.updatedAt ? new Date(raw.updatedAt).getTime() : NaN;
|
|
304
|
-
if (!Number.isFinite(updatedAt) || Date.now() - updatedAt > INDEX_MARKER_TTL_MS)
|
|
305
|
-
return false;
|
|
306
|
-
return true;
|
|
307
|
-
} catch {
|
|
308
|
-
return false;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
markLookupIndexReady(table, suffix) {
|
|
312
|
-
mkdirSync(getIndexMarkerDir(), { recursive: true });
|
|
313
|
-
writeFileSync(this.getLookupIndexMarkerPath(table, suffix), JSON.stringify({ updatedAt: (/* @__PURE__ */ new Date()).toISOString() }), "utf-8");
|
|
314
|
-
}
|
|
315
363
|
async ensureLookupIndex(table, suffix, columnsSql) {
|
|
316
|
-
|
|
364
|
+
const markers = await getIndexMarkerStore();
|
|
365
|
+
const markerPath = markers.buildIndexMarkerPath(this.workspaceId, this.orgId, table, suffix);
|
|
366
|
+
if (markers.hasFreshIndexMarker(markerPath))
|
|
317
367
|
return;
|
|
318
368
|
const indexName = this.buildLookupIndexName(table, suffix);
|
|
319
369
|
try {
|
|
320
370
|
await this.query(`CREATE INDEX IF NOT EXISTS "${indexName}" ON "${table}" ${columnsSql}`);
|
|
321
|
-
|
|
371
|
+
markers.writeIndexMarker(markerPath);
|
|
322
372
|
} catch (e) {
|
|
323
373
|
if (isDuplicateIndexError(e)) {
|
|
324
|
-
|
|
374
|
+
markers.writeIndexMarker(markerPath);
|
|
325
375
|
return;
|
|
326
376
|
}
|
|
327
377
|
log2(`index "${indexName}" skipped: ${e.message}`);
|
|
328
378
|
}
|
|
329
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* Ensure a vector column exists on the given table.
|
|
382
|
+
*
|
|
383
|
+
* The previous implementation always issued `ALTER TABLE ADD COLUMN IF NOT
|
|
384
|
+
* EXISTS …` on every SessionStart. On a long-running workspace that's
|
|
385
|
+
* already migrated, every call returns 500 "Column already exists" — noisy
|
|
386
|
+
* in the log and a wasted round-trip. Worse, the very first call after the
|
|
387
|
+
* column is genuinely added triggers Deeplake's post-ALTER `vector::at`
|
|
388
|
+
* window (~30s) during which subsequent INSERTs fail; minimising the
|
|
389
|
+
* number of ALTER calls minimises exposure to that window.
|
|
390
|
+
*
|
|
391
|
+
* New flow:
|
|
392
|
+
* 1. Check the local marker file (mirrors ensureLookupIndex). If fresh,
|
|
393
|
+
* return — zero network calls.
|
|
394
|
+
* 2. SELECT 1 FROM information_schema.columns WHERE table_name = T AND
|
|
395
|
+
* column_name = C. Read-only, idempotent, can't tickle the post-ALTER
|
|
396
|
+
* bug. If the column is present → mark + return.
|
|
397
|
+
* 3. Only if step 2 says the column is missing, fall back to ALTER ADD
|
|
398
|
+
* COLUMN IF NOT EXISTS. Mark on success, also mark if Deeplake reports
|
|
399
|
+
* "already exists" (race: another client added it between our SELECT
|
|
400
|
+
* and ALTER).
|
|
401
|
+
*
|
|
402
|
+
* Marker uses the same dir / TTL as ensureLookupIndex so both schema
|
|
403
|
+
* caches share an opt-out (HIVEMIND_INDEX_MARKER_DIR) and a TTL knob.
|
|
404
|
+
*/
|
|
405
|
+
async ensureEmbeddingColumn(table, column) {
|
|
406
|
+
await this.ensureColumn(table, column, "FLOAT4[]");
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Generic marker-gated column migration. Same SELECT-then-ALTER flow as
|
|
410
|
+
* ensureEmbeddingColumn, parameterized by SQL type so it can patch up any
|
|
411
|
+
* column that was added to the schema after the table was originally
|
|
412
|
+
* created. Used today for `summary_embedding`, `message_embedding`, and
|
|
413
|
+
* the `agent` column (added 2026-04-11) — the latter has no fallback if
|
|
414
|
+
* a user upgraded over a pre-2026-04-11 table, so every INSERT fails
|
|
415
|
+
* with `column "agent" does not exist`.
|
|
416
|
+
*/
|
|
417
|
+
async ensureColumn(table, column, sqlType) {
|
|
418
|
+
const markers = await getIndexMarkerStore();
|
|
419
|
+
const markerPath = markers.buildIndexMarkerPath(this.workspaceId, this.orgId, table, `col_${column}`);
|
|
420
|
+
if (markers.hasFreshIndexMarker(markerPath))
|
|
421
|
+
return;
|
|
422
|
+
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`;
|
|
423
|
+
const rows = await this.query(colCheck);
|
|
424
|
+
if (rows.length > 0) {
|
|
425
|
+
markers.writeIndexMarker(markerPath);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
await this.query(`ALTER TABLE "${table}" ADD COLUMN ${column} ${sqlType}`);
|
|
430
|
+
} catch (e) {
|
|
431
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
432
|
+
if (!/already exists/i.test(msg))
|
|
433
|
+
throw e;
|
|
434
|
+
const recheck = await this.query(colCheck);
|
|
435
|
+
if (recheck.length === 0)
|
|
436
|
+
throw e;
|
|
437
|
+
}
|
|
438
|
+
markers.writeIndexMarker(markerPath);
|
|
439
|
+
}
|
|
330
440
|
/** List all tables in the workspace (with retry). */
|
|
331
441
|
async listTables(forceRefresh = false) {
|
|
332
442
|
if (!forceRefresh && this._tablesCache)
|
|
@@ -342,7 +452,8 @@ var DeeplakeApi = class {
|
|
|
342
452
|
const resp = await fetch(`${this.apiUrl}/workspaces/${this.workspaceId}/tables`, {
|
|
343
453
|
headers: {
|
|
344
454
|
Authorization: `Bearer ${this.token}`,
|
|
345
|
-
"X-Activeloop-Org-Id": this.orgId
|
|
455
|
+
"X-Activeloop-Org-Id": this.orgId,
|
|
456
|
+
...deeplakeClientHeader()
|
|
346
457
|
}
|
|
347
458
|
});
|
|
348
459
|
if (resp.ok) {
|
|
@@ -367,29 +478,84 @@ var DeeplakeApi = class {
|
|
|
367
478
|
}
|
|
368
479
|
return { tables: [], cacheable: false };
|
|
369
480
|
}
|
|
481
|
+
/**
|
|
482
|
+
* Run a `CREATE TABLE` with an extra outer retry budget. The base
|
|
483
|
+
* `query()` already retries 3 times on fetch errors (~3.5s total), but a
|
|
484
|
+
* failed CREATE is permanent corruption — every subsequent SELECT against
|
|
485
|
+
* the missing table fails. Wrapping in an outer loop with longer backoff
|
|
486
|
+
* (2s, 5s, then 10s) gives us ~17s of reach across transient network
|
|
487
|
+
* blips before giving up. Failures still propagate; getApi() resets its
|
|
488
|
+
* cache on init failure (openclaw plugin) so the next call retries the
|
|
489
|
+
* whole init flow.
|
|
490
|
+
*/
|
|
491
|
+
async createTableWithRetry(sql, label) {
|
|
492
|
+
const OUTER_BACKOFFS_MS = [2e3, 5e3, 1e4];
|
|
493
|
+
let lastErr = null;
|
|
494
|
+
for (let attempt = 0; attempt <= OUTER_BACKOFFS_MS.length; attempt++) {
|
|
495
|
+
try {
|
|
496
|
+
await this.query(sql);
|
|
497
|
+
return;
|
|
498
|
+
} catch (err) {
|
|
499
|
+
lastErr = err;
|
|
500
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
501
|
+
log2(`CREATE TABLE "${label}" attempt ${attempt + 1}/${OUTER_BACKOFFS_MS.length + 1} failed: ${msg}`);
|
|
502
|
+
if (attempt < OUTER_BACKOFFS_MS.length) {
|
|
503
|
+
await sleep(OUTER_BACKOFFS_MS[attempt]);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
throw lastErr;
|
|
508
|
+
}
|
|
370
509
|
/** Create the memory table if it doesn't already exist. Migrate columns on existing tables. */
|
|
371
510
|
async ensureTable(name) {
|
|
372
|
-
const tbl = name ?? this.tableName;
|
|
511
|
+
const tbl = sqlIdent(name ?? this.tableName);
|
|
373
512
|
const tables = await this.listTables();
|
|
374
513
|
if (!tables.includes(tbl)) {
|
|
375
514
|
log2(`table "${tbl}" not found, creating`);
|
|
376
|
-
await this.
|
|
515
|
+
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);
|
|
377
516
|
log2(`table "${tbl}" created`);
|
|
378
517
|
if (!tables.includes(tbl))
|
|
379
518
|
this._tablesCache = [...tables, tbl];
|
|
380
519
|
}
|
|
520
|
+
await this.ensureEmbeddingColumn(tbl, SUMMARY_EMBEDDING_COL);
|
|
521
|
+
await this.ensureColumn(tbl, "agent", "TEXT NOT NULL DEFAULT ''");
|
|
381
522
|
}
|
|
382
523
|
/** Create the sessions table (uses JSONB for message since every row is a JSON event). */
|
|
383
524
|
async ensureSessionsTable(name) {
|
|
525
|
+
const safe = sqlIdent(name);
|
|
384
526
|
const tables = await this.listTables();
|
|
385
|
-
if (!tables.includes(
|
|
386
|
-
log2(`table "${
|
|
387
|
-
await this.
|
|
388
|
-
log2(`table "${
|
|
389
|
-
if (!tables.includes(
|
|
390
|
-
this._tablesCache = [...tables,
|
|
527
|
+
if (!tables.includes(safe)) {
|
|
528
|
+
log2(`table "${safe}" not found, creating`);
|
|
529
|
+
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);
|
|
530
|
+
log2(`table "${safe}" created`);
|
|
531
|
+
if (!tables.includes(safe))
|
|
532
|
+
this._tablesCache = [...tables, safe];
|
|
391
533
|
}
|
|
392
|
-
await this.
|
|
534
|
+
await this.ensureEmbeddingColumn(safe, MESSAGE_EMBEDDING_COL);
|
|
535
|
+
await this.ensureColumn(safe, "agent", "TEXT NOT NULL DEFAULT ''");
|
|
536
|
+
await this.ensureLookupIndex(safe, "path_creation_date", `("path", "creation_date")`);
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Create the skills table.
|
|
540
|
+
*
|
|
541
|
+
* One row per skill version. Workers INSERT a fresh row on every KEEP /
|
|
542
|
+
* MERGE rather than UPDATE-ing in place, so the full version history is
|
|
543
|
+
* recoverable. Uniqueness in the *current* state is by (project_key, name)
|
|
544
|
+
* — newer rows shadow older ones at read time (ORDER BY version DESC).
|
|
545
|
+
* This sidesteps the Deeplake UPDATE-coalescing quirk that bit the wiki
|
|
546
|
+
* worker.
|
|
547
|
+
*/
|
|
548
|
+
async ensureSkillsTable(name) {
|
|
549
|
+
const safe = sqlIdent(name);
|
|
550
|
+
const tables = await this.listTables();
|
|
551
|
+
if (!tables.includes(safe)) {
|
|
552
|
+
log2(`table "${safe}" not found, creating`);
|
|
553
|
+
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);
|
|
554
|
+
log2(`table "${safe}" created`);
|
|
555
|
+
if (!tables.includes(safe))
|
|
556
|
+
this._tablesCache = [...tables, safe];
|
|
557
|
+
}
|
|
558
|
+
await this.ensureLookupIndex(safe, "project_key_name", `("project_key", "name")`);
|
|
393
559
|
}
|
|
394
560
|
};
|
|
395
561
|
|
|
@@ -513,31 +679,207 @@ function bundleDirFromImportMeta(importMetaUrl) {
|
|
|
513
679
|
return dirname(fileURLToPath(importMetaUrl));
|
|
514
680
|
}
|
|
515
681
|
|
|
516
|
-
// dist/src/
|
|
517
|
-
import {
|
|
682
|
+
// dist/src/skilify/spawn-skilify-worker.js
|
|
683
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
684
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
685
|
+
import { dirname as dirname2, join as join7 } from "node:path";
|
|
686
|
+
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, appendFileSync as appendFileSync3, chmodSync } from "node:fs";
|
|
687
|
+
import { homedir as homedir5, tmpdir as tmpdir3 } from "node:os";
|
|
688
|
+
|
|
689
|
+
// dist/src/skilify/gate-runner.js
|
|
690
|
+
import { execFileSync } from "node:child_process";
|
|
691
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
518
692
|
import { homedir as homedir4 } from "node:os";
|
|
519
693
|
import { join as join6 } from "node:path";
|
|
520
|
-
|
|
521
|
-
|
|
694
|
+
function findAgentBin(agent) {
|
|
695
|
+
const which = (name) => {
|
|
696
|
+
try {
|
|
697
|
+
const out = execFileSync("which", [name], {
|
|
698
|
+
encoding: "utf-8",
|
|
699
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
700
|
+
});
|
|
701
|
+
return out.trim() || null;
|
|
702
|
+
} catch {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
switch (agent) {
|
|
707
|
+
case "claude_code":
|
|
708
|
+
return which("claude") ?? join6(homedir4(), ".claude", "local", "claude");
|
|
709
|
+
case "codex":
|
|
710
|
+
return which("codex") ?? "/usr/local/bin/codex";
|
|
711
|
+
case "cursor":
|
|
712
|
+
return which("cursor-agent") ?? "/usr/local/bin/cursor-agent";
|
|
713
|
+
case "hermes":
|
|
714
|
+
return which("hermes") ?? join6(homedir4(), ".local", "bin", "hermes");
|
|
715
|
+
case "pi":
|
|
716
|
+
return which("pi") ?? join6(homedir4(), ".local", "bin", "pi");
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// dist/src/skilify/spawn-skilify-worker.js
|
|
721
|
+
var HOME2 = homedir5();
|
|
722
|
+
var SKILIFY_LOG = join7(HOME2, ".claude", "hooks", "skilify.log");
|
|
723
|
+
function skilifyLog(msg) {
|
|
724
|
+
try {
|
|
725
|
+
mkdirSync4(dirname2(SKILIFY_LOG), { recursive: true });
|
|
726
|
+
appendFileSync3(SKILIFY_LOG, `[${utcTimestamp()}] ${msg}
|
|
727
|
+
`);
|
|
728
|
+
} catch {
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function spawnSkilifyWorker(opts) {
|
|
732
|
+
const { config, cwd, projectKey, project, bundleDir, agent, scopeConfig, currentSessionId, reason } = opts;
|
|
733
|
+
const tmpDir = join7(tmpdir3(), `deeplake-skilify-${projectKey}-${Date.now()}`);
|
|
734
|
+
mkdirSync4(tmpDir, { recursive: true, mode: 448 });
|
|
735
|
+
const gateBin = findAgentBin(agent);
|
|
736
|
+
const configFile = join7(tmpDir, "config.json");
|
|
737
|
+
writeFileSync3(configFile, JSON.stringify({
|
|
738
|
+
apiUrl: config.apiUrl,
|
|
739
|
+
token: config.token,
|
|
740
|
+
orgId: config.orgId,
|
|
741
|
+
workspaceId: config.workspaceId,
|
|
742
|
+
sessionsTable: config.sessionsTableName,
|
|
743
|
+
skillsTable: config.skillsTableName,
|
|
744
|
+
userName: config.userName,
|
|
745
|
+
cwd,
|
|
746
|
+
projectKey,
|
|
747
|
+
project,
|
|
748
|
+
agent,
|
|
749
|
+
scope: scopeConfig.scope,
|
|
750
|
+
team: scopeConfig.team,
|
|
751
|
+
install: scopeConfig.install,
|
|
752
|
+
tmpDir,
|
|
753
|
+
gateBin,
|
|
754
|
+
cursorModel: process.env.HIVEMIND_CURSOR_MODEL,
|
|
755
|
+
hermesProvider: process.env.HIVEMIND_HERMES_PROVIDER,
|
|
756
|
+
hermesModel: process.env.HIVEMIND_HERMES_MODEL,
|
|
757
|
+
piProvider: process.env.HIVEMIND_PI_PROVIDER,
|
|
758
|
+
piModel: process.env.HIVEMIND_PI_MODEL,
|
|
759
|
+
skilifyLog: SKILIFY_LOG,
|
|
760
|
+
currentSessionId
|
|
761
|
+
}), { mode: 384 });
|
|
762
|
+
try {
|
|
763
|
+
chmodSync(configFile, 384);
|
|
764
|
+
} catch {
|
|
765
|
+
}
|
|
766
|
+
skilifyLog(`${reason}: spawning skilify worker for project=${project} key=${projectKey}`);
|
|
767
|
+
const workerPath = join7(bundleDir, "skilify-worker.js");
|
|
768
|
+
spawn2("nohup", ["node", workerPath, configFile], {
|
|
769
|
+
detached: true,
|
|
770
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
771
|
+
}).unref();
|
|
772
|
+
skilifyLog(`${reason}: spawned skilify worker for ${projectKey}`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// dist/src/skilify/state.js
|
|
776
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync4, writeSync, mkdirSync as mkdirSync5, renameSync, existsSync as existsSync4, unlinkSync, openSync, closeSync } from "node:fs";
|
|
777
|
+
import { execSync as execSync2 } from "node:child_process";
|
|
778
|
+
import { homedir as homedir6 } from "node:os";
|
|
779
|
+
import { createHash } from "node:crypto";
|
|
780
|
+
import { join as join8, basename } from "node:path";
|
|
781
|
+
var dlog = (msg) => log("skilify-state", msg);
|
|
782
|
+
var STATE_DIR = join8(homedir6(), ".deeplake", "state", "skilify");
|
|
522
783
|
var YIELD_BUF = new Int32Array(new SharedArrayBuffer(4));
|
|
523
|
-
|
|
524
|
-
|
|
784
|
+
var TRIGGER_THRESHOLD = (() => {
|
|
785
|
+
const n = Number(process.env.HIVEMIND_SKILIFY_EVERY_N_TURNS ?? "");
|
|
786
|
+
return Number.isInteger(n) && n > 0 ? n : 20;
|
|
787
|
+
})();
|
|
788
|
+
function statePath(projectKey) {
|
|
789
|
+
return join8(STATE_DIR, `${projectKey}.json`);
|
|
525
790
|
}
|
|
526
|
-
function
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
791
|
+
function lockPath(projectKey) {
|
|
792
|
+
return join8(STATE_DIR, `${projectKey}.lock`);
|
|
793
|
+
}
|
|
794
|
+
function deriveProjectKey(cwd) {
|
|
795
|
+
const project = basename(cwd) || "unknown";
|
|
796
|
+
let signature = null;
|
|
797
|
+
try {
|
|
798
|
+
signature = execSync2("git config --get remote.origin.url", {
|
|
799
|
+
cwd,
|
|
800
|
+
encoding: "utf-8",
|
|
801
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
802
|
+
}).trim() || null;
|
|
803
|
+
} catch {
|
|
804
|
+
}
|
|
805
|
+
const input = signature ?? cwd;
|
|
806
|
+
const key = createHash("sha1").update(input).digest("hex").slice(0, 16);
|
|
807
|
+
return { key, project };
|
|
808
|
+
}
|
|
809
|
+
function readState(projectKey) {
|
|
810
|
+
const p = statePath(projectKey);
|
|
811
|
+
if (!existsSync4(p))
|
|
812
|
+
return null;
|
|
813
|
+
try {
|
|
814
|
+
return JSON.parse(readFileSync3(p, "utf-8"));
|
|
815
|
+
} catch {
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function writeState(projectKey, state) {
|
|
820
|
+
mkdirSync5(STATE_DIR, { recursive: true });
|
|
821
|
+
const p = statePath(projectKey);
|
|
822
|
+
const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
|
|
823
|
+
writeFileSync4(tmp, JSON.stringify(state, null, 2));
|
|
824
|
+
renameSync(tmp, p);
|
|
825
|
+
}
|
|
826
|
+
function withRmwLock(projectKey, fn) {
|
|
827
|
+
mkdirSync5(STATE_DIR, { recursive: true });
|
|
828
|
+
const rmw = lockPath(projectKey) + ".rmw";
|
|
829
|
+
const deadline = Date.now() + 2e3;
|
|
830
|
+
let fd = null;
|
|
831
|
+
while (fd === null) {
|
|
832
|
+
try {
|
|
833
|
+
fd = openSync(rmw, "wx");
|
|
834
|
+
} catch (e) {
|
|
835
|
+
if (e.code !== "EEXIST")
|
|
836
|
+
throw e;
|
|
837
|
+
if (Date.now() > deadline) {
|
|
838
|
+
dlog(`rmw lock deadline exceeded for ${projectKey}, reclaiming stale lock`);
|
|
839
|
+
try {
|
|
840
|
+
unlinkSync(rmw);
|
|
841
|
+
} catch (unlinkErr) {
|
|
842
|
+
dlog(`stale rmw lock unlink failed for ${projectKey}: ${unlinkErr.message}`);
|
|
843
|
+
}
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
Atomics.wait(YIELD_BUF, 0, 0, 10);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
try {
|
|
850
|
+
return fn();
|
|
851
|
+
} finally {
|
|
852
|
+
closeSync(fd);
|
|
853
|
+
try {
|
|
854
|
+
unlinkSync(rmw);
|
|
855
|
+
} catch (unlinkErr) {
|
|
856
|
+
dlog(`rmw lock cleanup failed for ${projectKey}: ${unlinkErr.message}`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
function resetCounter(projectKey) {
|
|
861
|
+
withRmwLock(projectKey, () => {
|
|
862
|
+
const s = readState(projectKey);
|
|
863
|
+
if (!s)
|
|
864
|
+
return;
|
|
865
|
+
writeState(projectKey, { ...s, counter: 0, updatedAt: Date.now() });
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
function tryAcquireWorkerLock(projectKey, maxAgeMs = 10 * 60 * 1e3) {
|
|
869
|
+
mkdirSync5(STATE_DIR, { recursive: true });
|
|
870
|
+
const p = lockPath(projectKey);
|
|
871
|
+
if (existsSync4(p)) {
|
|
530
872
|
try {
|
|
531
873
|
const ageMs = Date.now() - parseInt(readFileSync3(p, "utf-8"), 10);
|
|
532
874
|
if (Number.isFinite(ageMs) && ageMs < maxAgeMs)
|
|
533
875
|
return false;
|
|
534
876
|
} catch (readErr) {
|
|
535
|
-
dlog(`lock
|
|
877
|
+
dlog(`worker lock unreadable for ${projectKey}, treating as stale: ${readErr.message}`);
|
|
536
878
|
}
|
|
537
879
|
try {
|
|
538
880
|
unlinkSync(p);
|
|
539
881
|
} catch (unlinkErr) {
|
|
540
|
-
dlog(`could not unlink stale lock for ${
|
|
882
|
+
dlog(`could not unlink stale worker lock for ${projectKey}: ${unlinkErr.message}`);
|
|
541
883
|
return false;
|
|
542
884
|
}
|
|
543
885
|
}
|
|
@@ -549,6 +891,115 @@ function tryAcquireLock(sessionId, maxAgeMs = 10 * 60 * 1e3) {
|
|
|
549
891
|
closeSync(fd);
|
|
550
892
|
}
|
|
551
893
|
return true;
|
|
894
|
+
} catch {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
function releaseWorkerLock(projectKey) {
|
|
899
|
+
const p = lockPath(projectKey);
|
|
900
|
+
try {
|
|
901
|
+
unlinkSync(p);
|
|
902
|
+
} catch {
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// dist/src/skilify/scope-config.js
|
|
907
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
908
|
+
import { homedir as homedir7 } from "node:os";
|
|
909
|
+
import { join as join9 } from "node:path";
|
|
910
|
+
var STATE_DIR2 = join9(homedir7(), ".deeplake", "state", "skilify");
|
|
911
|
+
var CONFIG_PATH = join9(STATE_DIR2, "config.json");
|
|
912
|
+
var DEFAULT = { scope: "me", team: [], install: "project" };
|
|
913
|
+
function loadScopeConfig() {
|
|
914
|
+
if (!existsSync5(CONFIG_PATH))
|
|
915
|
+
return DEFAULT;
|
|
916
|
+
try {
|
|
917
|
+
const raw = JSON.parse(readFileSync4(CONFIG_PATH, "utf-8"));
|
|
918
|
+
const scope = raw.scope === "team" || raw.scope === "org" ? raw.scope : "me";
|
|
919
|
+
const team = Array.isArray(raw.team) ? raw.team.filter((s) => typeof s === "string") : [];
|
|
920
|
+
const install = raw.install === "global" ? "global" : "project";
|
|
921
|
+
return { scope, team, install };
|
|
922
|
+
} catch {
|
|
923
|
+
return DEFAULT;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// dist/src/skilify/triggers.js
|
|
928
|
+
function forceSessionEndTrigger(opts) {
|
|
929
|
+
if (process.env.HIVEMIND_SKILIFY_WORKER === "1")
|
|
930
|
+
return;
|
|
931
|
+
if (!opts.cwd)
|
|
932
|
+
return;
|
|
933
|
+
try {
|
|
934
|
+
const { key: projectKey, project } = deriveProjectKey(opts.cwd);
|
|
935
|
+
if (!tryAcquireWorkerLock(projectKey)) {
|
|
936
|
+
skilifyLog(`SessionEnd: skilify worker already running for ${projectKey}, skipping`);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (readState(projectKey)) {
|
|
940
|
+
resetCounter(projectKey);
|
|
941
|
+
}
|
|
942
|
+
skilifyLog(`SessionEnd: spawning skilify worker for project=${project} agent=${opts.agent}`);
|
|
943
|
+
try {
|
|
944
|
+
spawnSkilifyWorker({
|
|
945
|
+
config: opts.config,
|
|
946
|
+
cwd: opts.cwd,
|
|
947
|
+
projectKey,
|
|
948
|
+
project,
|
|
949
|
+
bundleDir: opts.bundleDir,
|
|
950
|
+
agent: opts.agent,
|
|
951
|
+
scopeConfig: loadScopeConfig(),
|
|
952
|
+
currentSessionId: opts.sessionId,
|
|
953
|
+
reason: "SessionEnd"
|
|
954
|
+
});
|
|
955
|
+
} catch (e) {
|
|
956
|
+
skilifyLog(`SessionEnd spawn failed: ${e?.message ?? e}`);
|
|
957
|
+
try {
|
|
958
|
+
releaseWorkerLock(projectKey);
|
|
959
|
+
} catch {
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
} catch (e) {
|
|
963
|
+
skilifyLog(`SessionEnd trigger error: ${e?.message ?? e}`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// dist/src/hooks/summary-state.js
|
|
968
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync6, writeSync as writeSync2, mkdirSync as mkdirSync7, renameSync as renameSync2, existsSync as existsSync6, unlinkSync as unlinkSync2, openSync as openSync2, closeSync as closeSync2 } from "node:fs";
|
|
969
|
+
import { homedir as homedir8 } from "node:os";
|
|
970
|
+
import { join as join10 } from "node:path";
|
|
971
|
+
var dlog2 = (msg) => log("summary-state", msg);
|
|
972
|
+
var STATE_DIR3 = join10(homedir8(), ".claude", "hooks", "summary-state");
|
|
973
|
+
var YIELD_BUF2 = new Int32Array(new SharedArrayBuffer(4));
|
|
974
|
+
function lockPath2(sessionId) {
|
|
975
|
+
return join10(STATE_DIR3, `${sessionId}.lock`);
|
|
976
|
+
}
|
|
977
|
+
function tryAcquireLock(sessionId, maxAgeMs = 10 * 60 * 1e3) {
|
|
978
|
+
mkdirSync7(STATE_DIR3, { recursive: true });
|
|
979
|
+
const p = lockPath2(sessionId);
|
|
980
|
+
if (existsSync6(p)) {
|
|
981
|
+
try {
|
|
982
|
+
const ageMs = Date.now() - parseInt(readFileSync5(p, "utf-8"), 10);
|
|
983
|
+
if (Number.isFinite(ageMs) && ageMs < maxAgeMs)
|
|
984
|
+
return false;
|
|
985
|
+
} catch (readErr) {
|
|
986
|
+
dlog2(`lock file unreadable for ${sessionId}, treating as stale: ${readErr.message}`);
|
|
987
|
+
}
|
|
988
|
+
try {
|
|
989
|
+
unlinkSync2(p);
|
|
990
|
+
} catch (unlinkErr) {
|
|
991
|
+
dlog2(`could not unlink stale lock for ${sessionId}: ${unlinkErr.message}`);
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
try {
|
|
996
|
+
const fd = openSync2(p, "wx");
|
|
997
|
+
try {
|
|
998
|
+
writeSync2(fd, String(Date.now()));
|
|
999
|
+
} finally {
|
|
1000
|
+
closeSync2(fd);
|
|
1001
|
+
}
|
|
1002
|
+
return true;
|
|
552
1003
|
} catch (e) {
|
|
553
1004
|
if (e.code === "EEXIST")
|
|
554
1005
|
return false;
|
|
@@ -557,10 +1008,10 @@ function tryAcquireLock(sessionId, maxAgeMs = 10 * 60 * 1e3) {
|
|
|
557
1008
|
}
|
|
558
1009
|
function releaseLock(sessionId) {
|
|
559
1010
|
try {
|
|
560
|
-
|
|
1011
|
+
unlinkSync2(lockPath2(sessionId));
|
|
561
1012
|
} catch (e) {
|
|
562
1013
|
if (e?.code !== "ENOENT") {
|
|
563
|
-
|
|
1014
|
+
dlog2(`releaseLock unlink failed for ${sessionId}: ${e.message}`);
|
|
564
1015
|
}
|
|
565
1016
|
}
|
|
566
1017
|
}
|
|
@@ -571,8 +1022,288 @@ function buildSessionPath(config, sessionId) {
|
|
|
571
1022
|
return `/sessions/${config.userName}/${config.userName}_${config.orgName}_${workspace}_${sessionId}.jsonl`;
|
|
572
1023
|
}
|
|
573
1024
|
|
|
1025
|
+
// dist/src/embeddings/client.js
|
|
1026
|
+
import { connect } from "node:net";
|
|
1027
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
1028
|
+
import { openSync as openSync3, closeSync as closeSync3, writeSync as writeSync3, unlinkSync as unlinkSync3, existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
|
|
1029
|
+
import { homedir as homedir9 } from "node:os";
|
|
1030
|
+
import { join as join11 } from "node:path";
|
|
1031
|
+
|
|
1032
|
+
// dist/src/embeddings/protocol.js
|
|
1033
|
+
var DEFAULT_SOCKET_DIR = "/tmp";
|
|
1034
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1035
|
+
var DEFAULT_CLIENT_TIMEOUT_MS = 2e3;
|
|
1036
|
+
function socketPathFor(uid, dir = DEFAULT_SOCKET_DIR) {
|
|
1037
|
+
return `${dir}/hivemind-embed-${uid}.sock`;
|
|
1038
|
+
}
|
|
1039
|
+
function pidPathFor(uid, dir = DEFAULT_SOCKET_DIR) {
|
|
1040
|
+
return `${dir}/hivemind-embed-${uid}.pid`;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// dist/src/embeddings/client.js
|
|
1044
|
+
var SHARED_DAEMON_PATH = join11(homedir9(), ".hivemind", "embed-deps", "embed-daemon.js");
|
|
1045
|
+
var log3 = (m) => log("embed-client", m);
|
|
1046
|
+
function getUid() {
|
|
1047
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
1048
|
+
return uid !== void 0 ? String(uid) : process.env.USER ?? "default";
|
|
1049
|
+
}
|
|
1050
|
+
var EmbedClient = class {
|
|
1051
|
+
socketPath;
|
|
1052
|
+
pidPath;
|
|
1053
|
+
timeoutMs;
|
|
1054
|
+
daemonEntry;
|
|
1055
|
+
autoSpawn;
|
|
1056
|
+
spawnWaitMs;
|
|
1057
|
+
nextId = 0;
|
|
1058
|
+
constructor(opts = {}) {
|
|
1059
|
+
const uid = getUid();
|
|
1060
|
+
const dir = opts.socketDir ?? "/tmp";
|
|
1061
|
+
this.socketPath = socketPathFor(uid, dir);
|
|
1062
|
+
this.pidPath = pidPathFor(uid, dir);
|
|
1063
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_CLIENT_TIMEOUT_MS;
|
|
1064
|
+
this.daemonEntry = opts.daemonEntry ?? process.env.HIVEMIND_EMBED_DAEMON ?? (existsSync7(SHARED_DAEMON_PATH) ? SHARED_DAEMON_PATH : void 0);
|
|
1065
|
+
this.autoSpawn = opts.autoSpawn ?? true;
|
|
1066
|
+
this.spawnWaitMs = opts.spawnWaitMs ?? 5e3;
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Returns an embedding vector, or null on timeout/failure. Hooks MUST treat
|
|
1070
|
+
* null as "skip embedding column" — never block the write path on us.
|
|
1071
|
+
*
|
|
1072
|
+
* Fire-and-forget spawn on miss: if the daemon isn't up, this call returns
|
|
1073
|
+
* null AND kicks off a background spawn. The next call finds a ready daemon.
|
|
1074
|
+
*/
|
|
1075
|
+
async embed(text, kind = "document") {
|
|
1076
|
+
let sock;
|
|
1077
|
+
try {
|
|
1078
|
+
sock = await this.connectOnce();
|
|
1079
|
+
} catch {
|
|
1080
|
+
if (this.autoSpawn)
|
|
1081
|
+
this.trySpawnDaemon();
|
|
1082
|
+
return null;
|
|
1083
|
+
}
|
|
1084
|
+
try {
|
|
1085
|
+
const id = String(++this.nextId);
|
|
1086
|
+
const req = { op: "embed", id, kind, text };
|
|
1087
|
+
const resp = await this.sendAndWait(sock, req);
|
|
1088
|
+
if (resp.error || !("embedding" in resp) || !resp.embedding) {
|
|
1089
|
+
log3(`embed err: ${resp.error ?? "no embedding"}`);
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
return resp.embedding;
|
|
1093
|
+
} catch (e) {
|
|
1094
|
+
const err = e instanceof Error ? e.message : String(e);
|
|
1095
|
+
log3(`embed failed: ${err}`);
|
|
1096
|
+
return null;
|
|
1097
|
+
} finally {
|
|
1098
|
+
try {
|
|
1099
|
+
sock.end();
|
|
1100
|
+
} catch {
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Wait up to spawnWaitMs for the daemon to accept connections, spawning if
|
|
1106
|
+
* necessary. Meant for SessionStart / long-running batches — not the hot path.
|
|
1107
|
+
*/
|
|
1108
|
+
async warmup() {
|
|
1109
|
+
try {
|
|
1110
|
+
const s = await this.connectOnce();
|
|
1111
|
+
s.end();
|
|
1112
|
+
return true;
|
|
1113
|
+
} catch {
|
|
1114
|
+
if (!this.autoSpawn)
|
|
1115
|
+
return false;
|
|
1116
|
+
this.trySpawnDaemon();
|
|
1117
|
+
try {
|
|
1118
|
+
const s = await this.waitForSocket();
|
|
1119
|
+
s.end();
|
|
1120
|
+
return true;
|
|
1121
|
+
} catch {
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
connectOnce() {
|
|
1127
|
+
return new Promise((resolve, reject) => {
|
|
1128
|
+
const sock = connect(this.socketPath);
|
|
1129
|
+
const to = setTimeout(() => {
|
|
1130
|
+
sock.destroy();
|
|
1131
|
+
reject(new Error("connect timeout"));
|
|
1132
|
+
}, this.timeoutMs);
|
|
1133
|
+
sock.once("connect", () => {
|
|
1134
|
+
clearTimeout(to);
|
|
1135
|
+
resolve(sock);
|
|
1136
|
+
});
|
|
1137
|
+
sock.once("error", (e) => {
|
|
1138
|
+
clearTimeout(to);
|
|
1139
|
+
reject(e);
|
|
1140
|
+
});
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
trySpawnDaemon() {
|
|
1144
|
+
let fd;
|
|
1145
|
+
try {
|
|
1146
|
+
fd = openSync3(this.pidPath, "wx", 384);
|
|
1147
|
+
writeSync3(fd, String(process.pid));
|
|
1148
|
+
} catch (e) {
|
|
1149
|
+
if (this.isPidFileStale()) {
|
|
1150
|
+
try {
|
|
1151
|
+
unlinkSync3(this.pidPath);
|
|
1152
|
+
} catch {
|
|
1153
|
+
}
|
|
1154
|
+
try {
|
|
1155
|
+
fd = openSync3(this.pidPath, "wx", 384);
|
|
1156
|
+
writeSync3(fd, String(process.pid));
|
|
1157
|
+
} catch {
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
} else {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
if (!this.daemonEntry || !existsSync7(this.daemonEntry)) {
|
|
1165
|
+
log3(`daemonEntry not configured or missing: ${this.daemonEntry}`);
|
|
1166
|
+
try {
|
|
1167
|
+
closeSync3(fd);
|
|
1168
|
+
unlinkSync3(this.pidPath);
|
|
1169
|
+
} catch {
|
|
1170
|
+
}
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
try {
|
|
1174
|
+
const child = spawn3(process.execPath, [this.daemonEntry], {
|
|
1175
|
+
detached: true,
|
|
1176
|
+
stdio: "ignore",
|
|
1177
|
+
env: process.env
|
|
1178
|
+
});
|
|
1179
|
+
child.unref();
|
|
1180
|
+
log3(`spawned daemon pid=${child.pid}`);
|
|
1181
|
+
} finally {
|
|
1182
|
+
closeSync3(fd);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
isPidFileStale() {
|
|
1186
|
+
try {
|
|
1187
|
+
const raw = readFileSync6(this.pidPath, "utf-8").trim();
|
|
1188
|
+
const pid = Number(raw);
|
|
1189
|
+
if (!pid || Number.isNaN(pid))
|
|
1190
|
+
return true;
|
|
1191
|
+
try {
|
|
1192
|
+
process.kill(pid, 0);
|
|
1193
|
+
return false;
|
|
1194
|
+
} catch {
|
|
1195
|
+
return true;
|
|
1196
|
+
}
|
|
1197
|
+
} catch {
|
|
1198
|
+
return true;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
async waitForSocket() {
|
|
1202
|
+
const deadline = Date.now() + this.spawnWaitMs;
|
|
1203
|
+
let delay = 30;
|
|
1204
|
+
while (Date.now() < deadline) {
|
|
1205
|
+
await sleep2(delay);
|
|
1206
|
+
delay = Math.min(delay * 1.5, 300);
|
|
1207
|
+
if (!existsSync7(this.socketPath))
|
|
1208
|
+
continue;
|
|
1209
|
+
try {
|
|
1210
|
+
return await this.connectOnce();
|
|
1211
|
+
} catch {
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
throw new Error("daemon did not become ready within spawnWaitMs");
|
|
1215
|
+
}
|
|
1216
|
+
sendAndWait(sock, req) {
|
|
1217
|
+
return new Promise((resolve, reject) => {
|
|
1218
|
+
let buf = "";
|
|
1219
|
+
const to = setTimeout(() => {
|
|
1220
|
+
sock.destroy();
|
|
1221
|
+
reject(new Error("request timeout"));
|
|
1222
|
+
}, this.timeoutMs);
|
|
1223
|
+
sock.setEncoding("utf-8");
|
|
1224
|
+
sock.on("data", (chunk) => {
|
|
1225
|
+
buf += chunk;
|
|
1226
|
+
const nl = buf.indexOf("\n");
|
|
1227
|
+
if (nl === -1)
|
|
1228
|
+
return;
|
|
1229
|
+
const line = buf.slice(0, nl);
|
|
1230
|
+
clearTimeout(to);
|
|
1231
|
+
try {
|
|
1232
|
+
resolve(JSON.parse(line));
|
|
1233
|
+
} catch (e) {
|
|
1234
|
+
reject(e);
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
sock.on("error", (e) => {
|
|
1238
|
+
clearTimeout(to);
|
|
1239
|
+
reject(e);
|
|
1240
|
+
});
|
|
1241
|
+
sock.on("end", () => {
|
|
1242
|
+
clearTimeout(to);
|
|
1243
|
+
reject(new Error("connection closed without response"));
|
|
1244
|
+
});
|
|
1245
|
+
sock.write(JSON.stringify(req) + "\n");
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
function sleep2(ms) {
|
|
1250
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// dist/src/embeddings/sql.js
|
|
1254
|
+
function embeddingSqlLiteral(vec) {
|
|
1255
|
+
if (!vec || vec.length === 0)
|
|
1256
|
+
return "NULL";
|
|
1257
|
+
const parts = [];
|
|
1258
|
+
for (const v of vec) {
|
|
1259
|
+
if (!Number.isFinite(v))
|
|
1260
|
+
return "NULL";
|
|
1261
|
+
parts.push(String(v));
|
|
1262
|
+
}
|
|
1263
|
+
return `ARRAY[${parts.join(",")}]::float4[]`;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// dist/src/embeddings/disable.js
|
|
1267
|
+
import { createRequire } from "node:module";
|
|
1268
|
+
import { homedir as homedir10 } from "node:os";
|
|
1269
|
+
import { join as join12 } from "node:path";
|
|
1270
|
+
import { pathToFileURL } from "node:url";
|
|
1271
|
+
var cachedStatus = null;
|
|
1272
|
+
function defaultResolveTransformers() {
|
|
1273
|
+
try {
|
|
1274
|
+
createRequire(import.meta.url).resolve("@huggingface/transformers");
|
|
1275
|
+
return;
|
|
1276
|
+
} catch {
|
|
1277
|
+
}
|
|
1278
|
+
const sharedDir = join12(homedir10(), ".hivemind", "embed-deps");
|
|
1279
|
+
createRequire(pathToFileURL(`${sharedDir}/`).href).resolve("@huggingface/transformers");
|
|
1280
|
+
}
|
|
1281
|
+
var _resolve = defaultResolveTransformers;
|
|
1282
|
+
function detectStatus() {
|
|
1283
|
+
if (process.env.HIVEMIND_EMBEDDINGS === "false")
|
|
1284
|
+
return "env-disabled";
|
|
1285
|
+
try {
|
|
1286
|
+
_resolve();
|
|
1287
|
+
return "enabled";
|
|
1288
|
+
} catch {
|
|
1289
|
+
return "no-transformers";
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
function embeddingsStatus() {
|
|
1293
|
+
if (cachedStatus !== null)
|
|
1294
|
+
return cachedStatus;
|
|
1295
|
+
cachedStatus = detectStatus();
|
|
1296
|
+
return cachedStatus;
|
|
1297
|
+
}
|
|
1298
|
+
function embeddingsDisabled() {
|
|
1299
|
+
return embeddingsStatus() !== "enabled";
|
|
1300
|
+
}
|
|
1301
|
+
|
|
574
1302
|
// dist/src/hooks/codex/stop.js
|
|
575
|
-
var
|
|
1303
|
+
var log4 = (msg) => log("codex-stop", msg);
|
|
1304
|
+
function resolveEmbedDaemonPath() {
|
|
1305
|
+
return join13(dirname3(fileURLToPath3(import.meta.url)), "embeddings", "embed-daemon.js");
|
|
1306
|
+
}
|
|
576
1307
|
var CAPTURE = process.env.HIVEMIND_CAPTURE !== "false";
|
|
577
1308
|
async function main() {
|
|
578
1309
|
if (process.env.HIVEMIND_WIKI_WORKER === "1")
|
|
@@ -583,7 +1314,7 @@ async function main() {
|
|
|
583
1314
|
return;
|
|
584
1315
|
const config = loadConfig();
|
|
585
1316
|
if (!config) {
|
|
586
|
-
|
|
1317
|
+
log4("no config");
|
|
587
1318
|
return;
|
|
588
1319
|
}
|
|
589
1320
|
if (CAPTURE) {
|
|
@@ -595,8 +1326,8 @@ async function main() {
|
|
|
595
1326
|
if (input.transcript_path) {
|
|
596
1327
|
try {
|
|
597
1328
|
const transcriptPath = input.transcript_path;
|
|
598
|
-
if (
|
|
599
|
-
const transcript =
|
|
1329
|
+
if (existsSync8(transcriptPath)) {
|
|
1330
|
+
const transcript = readFileSync7(transcriptPath, "utf-8");
|
|
600
1331
|
const lines = transcript.trim().split("\n").reverse();
|
|
601
1332
|
for (const line2 of lines) {
|
|
602
1333
|
try {
|
|
@@ -613,10 +1344,10 @@ async function main() {
|
|
|
613
1344
|
}
|
|
614
1345
|
}
|
|
615
1346
|
if (lastAssistantMessage)
|
|
616
|
-
|
|
1347
|
+
log4(`extracted assistant message from transcript (${lastAssistantMessage.length} chars)`);
|
|
617
1348
|
}
|
|
618
1349
|
} catch (e) {
|
|
619
|
-
|
|
1350
|
+
log4(`transcript read failed: ${e.message}`);
|
|
620
1351
|
}
|
|
621
1352
|
}
|
|
622
1353
|
const entry = {
|
|
@@ -635,11 +1366,13 @@ async function main() {
|
|
|
635
1366
|
const projectName = (input.cwd ?? "").split("/").pop() || "unknown";
|
|
636
1367
|
const filename = sessionPath.split("/").pop() ?? "";
|
|
637
1368
|
const jsonForSql = line.replace(/'/g, "''");
|
|
638
|
-
const
|
|
1369
|
+
const embedding = embeddingsDisabled() ? null : await new EmbedClient({ daemonEntry: resolveEmbedDaemonPath() }).embed(line, "document");
|
|
1370
|
+
const embeddingSql = embeddingSqlLiteral(embedding);
|
|
1371
|
+
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)}', 'Stop', 'codex', '${ts}', '${ts}')`;
|
|
639
1372
|
await api.query(insertSql);
|
|
640
|
-
|
|
1373
|
+
log4("stop event captured");
|
|
641
1374
|
} catch (e) {
|
|
642
|
-
|
|
1375
|
+
log4(`capture failed: ${e.message}`);
|
|
643
1376
|
}
|
|
644
1377
|
}
|
|
645
1378
|
if (!CAPTURE)
|
|
@@ -658,16 +1391,23 @@ async function main() {
|
|
|
658
1391
|
reason: "Stop"
|
|
659
1392
|
});
|
|
660
1393
|
} catch (e) {
|
|
661
|
-
|
|
1394
|
+
log4(`spawn failed: ${e.message}`);
|
|
662
1395
|
try {
|
|
663
1396
|
releaseLock(sessionId);
|
|
664
1397
|
} catch (releaseErr) {
|
|
665
|
-
|
|
1398
|
+
log4(`releaseLock after spawn failure also failed: ${releaseErr.message}`);
|
|
666
1399
|
}
|
|
667
1400
|
throw e;
|
|
668
1401
|
}
|
|
1402
|
+
forceSessionEndTrigger({
|
|
1403
|
+
config,
|
|
1404
|
+
cwd: input.cwd ?? "",
|
|
1405
|
+
bundleDir: bundleDirFromImportMeta(import.meta.url),
|
|
1406
|
+
agent: "codex",
|
|
1407
|
+
sessionId
|
|
1408
|
+
});
|
|
669
1409
|
}
|
|
670
1410
|
main().catch((e) => {
|
|
671
|
-
|
|
1411
|
+
log4(`fatal: ${e.message}`);
|
|
672
1412
|
process.exit(0);
|
|
673
1413
|
});
|