@deeplake/hivemind 0.7.31 → 0.7.32
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/bundle/cli.js +427 -233
- package/codex/bundle/capture.js +550 -122
- package/codex/bundle/embeddings/embed-daemon.js +55 -4
- package/codex/bundle/pre-tool-use.js +447 -90
- package/codex/bundle/shell/deeplake-shell.js +431 -74
- package/codex/bundle/stop.js +437 -80
- package/codex/bundle/wiki-worker.js +429 -72
- package/cursor/bundle/capture.js +625 -197
- package/cursor/bundle/embeddings/embed-daemon.js +55 -4
- package/cursor/bundle/pre-tool-use.js +432 -75
- package/cursor/bundle/session-start.js +8 -1
- package/cursor/bundle/shell/deeplake-shell.js +431 -74
- package/cursor/bundle/wiki-worker.js +429 -72
- package/hermes/bundle/capture.js +626 -198
- package/hermes/bundle/embeddings/embed-daemon.js +55 -4
- package/hermes/bundle/pre-tool-use.js +431 -74
- package/hermes/bundle/session-start.js +8 -1
- package/hermes/bundle/shell/deeplake-shell.js +431 -74
- package/hermes/bundle/wiki-worker.js +429 -72
- package/openclaw/dist/index.js +1 -1
- package/openclaw/openclaw.plugin.json +1 -1
- package/openclaw/package.json +1 -1
- package/package.json +1 -1
|
@@ -54,19 +54,19 @@ var init_index_marker_store = __esm({
|
|
|
54
54
|
|
|
55
55
|
// dist/src/hooks/codex/pre-tool-use.js
|
|
56
56
|
import { execFileSync } from "node:child_process";
|
|
57
|
-
import { existsSync as
|
|
58
|
-
import { join as
|
|
57
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
58
|
+
import { join as join11, dirname as dirname3 } from "node:path";
|
|
59
59
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
60
60
|
|
|
61
61
|
// dist/src/utils/stdin.js
|
|
62
62
|
function readStdin() {
|
|
63
|
-
return new Promise((
|
|
63
|
+
return new Promise((resolve3, reject) => {
|
|
64
64
|
let data = "";
|
|
65
65
|
process.stdin.setEncoding("utf-8");
|
|
66
66
|
process.stdin.on("data", (chunk) => data += chunk);
|
|
67
67
|
process.stdin.on("end", () => {
|
|
68
68
|
try {
|
|
69
|
-
|
|
69
|
+
resolve3(JSON.parse(data));
|
|
70
70
|
} catch (err) {
|
|
71
71
|
reject(new Error(`Failed to parse hook input: ${err}`));
|
|
72
72
|
}
|
|
@@ -182,7 +182,7 @@ function getQueryTimeoutMs() {
|
|
|
182
182
|
return Number(process.env.HIVEMIND_QUERY_TIMEOUT_MS ?? 1e4);
|
|
183
183
|
}
|
|
184
184
|
function sleep(ms) {
|
|
185
|
-
return new Promise((
|
|
185
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
186
186
|
}
|
|
187
187
|
function isTimeoutError(error) {
|
|
188
188
|
const name = error instanceof Error ? error.name.toLowerCase() : "";
|
|
@@ -212,7 +212,7 @@ var Semaphore = class {
|
|
|
212
212
|
this.active++;
|
|
213
213
|
return;
|
|
214
214
|
}
|
|
215
|
-
await new Promise((
|
|
215
|
+
await new Promise((resolve3) => this.waiting.push(resolve3));
|
|
216
216
|
}
|
|
217
217
|
release() {
|
|
218
218
|
this.active--;
|
|
@@ -1049,9 +1049,9 @@ function capOutputForClaude(output, options = {}) {
|
|
|
1049
1049
|
// dist/src/embeddings/client.js
|
|
1050
1050
|
import { connect } from "node:net";
|
|
1051
1051
|
import { spawn } from "node:child_process";
|
|
1052
|
-
import { openSync, closeSync, writeSync, unlinkSync, existsSync as
|
|
1053
|
-
import { homedir as
|
|
1054
|
-
import { join as
|
|
1052
|
+
import { openSync as openSync2, closeSync as closeSync2, writeSync, unlinkSync as unlinkSync2, existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
|
|
1053
|
+
import { homedir as homedir6 } from "node:os";
|
|
1054
|
+
import { join as join7 } from "node:path";
|
|
1055
1055
|
|
|
1056
1056
|
// dist/src/embeddings/protocol.js
|
|
1057
1057
|
var DEFAULT_SOCKET_DIR = "/tmp";
|
|
@@ -1064,13 +1064,234 @@ function pidPathFor(uid, dir = DEFAULT_SOCKET_DIR) {
|
|
|
1064
1064
|
return `${dir}/hivemind-embed-${uid}.pid`;
|
|
1065
1065
|
}
|
|
1066
1066
|
|
|
1067
|
+
// dist/src/notifications/queue.js
|
|
1068
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync, mkdirSync as mkdirSync2, openSync, closeSync, unlinkSync, statSync } from "node:fs";
|
|
1069
|
+
import { join as join4, resolve } from "node:path";
|
|
1070
|
+
import { homedir as homedir3 } from "node:os";
|
|
1071
|
+
import { setTimeout as sleep2 } from "node:timers/promises";
|
|
1072
|
+
var log3 = (msg) => log("notifications-queue", msg);
|
|
1073
|
+
var LOCK_RETRY_MAX = 50;
|
|
1074
|
+
var LOCK_RETRY_BASE_MS = 5;
|
|
1075
|
+
var LOCK_STALE_MS = 5e3;
|
|
1076
|
+
function queuePath() {
|
|
1077
|
+
return join4(homedir3(), ".deeplake", "notifications-queue.json");
|
|
1078
|
+
}
|
|
1079
|
+
function lockPath() {
|
|
1080
|
+
return `${queuePath()}.lock`;
|
|
1081
|
+
}
|
|
1082
|
+
function readQueue() {
|
|
1083
|
+
try {
|
|
1084
|
+
const raw = readFileSync3(queuePath(), "utf-8");
|
|
1085
|
+
const parsed = JSON.parse(raw);
|
|
1086
|
+
if (!parsed || !Array.isArray(parsed.queue)) {
|
|
1087
|
+
log3(`queue malformed \u2192 treating as empty`);
|
|
1088
|
+
return { queue: [] };
|
|
1089
|
+
}
|
|
1090
|
+
return { queue: parsed.queue };
|
|
1091
|
+
} catch {
|
|
1092
|
+
return { queue: [] };
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
function _isQueuePathInsideHome(path, home) {
|
|
1096
|
+
const r = resolve(path);
|
|
1097
|
+
const h = resolve(home);
|
|
1098
|
+
return r.startsWith(h + "/") || r === h;
|
|
1099
|
+
}
|
|
1100
|
+
function writeQueue(q) {
|
|
1101
|
+
const path = queuePath();
|
|
1102
|
+
const home = resolve(homedir3());
|
|
1103
|
+
if (!_isQueuePathInsideHome(path, home)) {
|
|
1104
|
+
throw new Error(`notifications-queue write blocked: ${path} is outside ${home}`);
|
|
1105
|
+
}
|
|
1106
|
+
mkdirSync2(join4(home, ".deeplake"), { recursive: true, mode: 448 });
|
|
1107
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
1108
|
+
writeFileSync2(tmp, JSON.stringify(q, null, 2), { mode: 384 });
|
|
1109
|
+
renameSync(tmp, path);
|
|
1110
|
+
}
|
|
1111
|
+
async function withQueueLock(fn) {
|
|
1112
|
+
const path = lockPath();
|
|
1113
|
+
mkdirSync2(join4(homedir3(), ".deeplake"), { recursive: true, mode: 448 });
|
|
1114
|
+
let fd = null;
|
|
1115
|
+
for (let attempt = 0; attempt < LOCK_RETRY_MAX; attempt++) {
|
|
1116
|
+
try {
|
|
1117
|
+
fd = openSync(path, "wx", 384);
|
|
1118
|
+
break;
|
|
1119
|
+
} catch (e) {
|
|
1120
|
+
const code = e.code;
|
|
1121
|
+
if (code !== "EEXIST")
|
|
1122
|
+
throw e;
|
|
1123
|
+
try {
|
|
1124
|
+
const age = Date.now() - statSync(path).mtimeMs;
|
|
1125
|
+
if (age > LOCK_STALE_MS) {
|
|
1126
|
+
unlinkSync(path);
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
} catch {
|
|
1130
|
+
}
|
|
1131
|
+
const delay = LOCK_RETRY_BASE_MS * (attempt + 1);
|
|
1132
|
+
await sleep2(delay);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (fd === null) {
|
|
1136
|
+
log3(`lock acquisition gave up after ${LOCK_RETRY_MAX} attempts \u2014 proceeding unlocked (last-writer-wins)`);
|
|
1137
|
+
return fn();
|
|
1138
|
+
}
|
|
1139
|
+
try {
|
|
1140
|
+
return fn();
|
|
1141
|
+
} finally {
|
|
1142
|
+
try {
|
|
1143
|
+
closeSync(fd);
|
|
1144
|
+
} catch {
|
|
1145
|
+
}
|
|
1146
|
+
try {
|
|
1147
|
+
unlinkSync(path);
|
|
1148
|
+
} catch {
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
function sameDedupKey(a, b) {
|
|
1153
|
+
if (a.id !== b.id)
|
|
1154
|
+
return false;
|
|
1155
|
+
return JSON.stringify(a.dedupKey) === JSON.stringify(b.dedupKey);
|
|
1156
|
+
}
|
|
1157
|
+
async function enqueueNotification(n) {
|
|
1158
|
+
await withQueueLock(() => {
|
|
1159
|
+
const q = readQueue();
|
|
1160
|
+
if (q.queue.some((existing) => sameDedupKey(existing, n))) {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
q.queue.push(n);
|
|
1164
|
+
writeQueue(q);
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// dist/src/embeddings/disable.js
|
|
1169
|
+
import { createRequire } from "node:module";
|
|
1170
|
+
import { homedir as homedir5 } from "node:os";
|
|
1171
|
+
import { join as join6 } from "node:path";
|
|
1172
|
+
import { pathToFileURL } from "node:url";
|
|
1173
|
+
|
|
1174
|
+
// dist/src/user-config.js
|
|
1175
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1176
|
+
import { homedir as homedir4 } from "node:os";
|
|
1177
|
+
import { dirname, join as join5 } from "node:path";
|
|
1178
|
+
var _configPath = () => process.env.HIVEMIND_CONFIG_PATH ?? join5(homedir4(), ".deeplake", "config.json");
|
|
1179
|
+
var _cache = null;
|
|
1180
|
+
var _migrated = false;
|
|
1181
|
+
function readUserConfig() {
|
|
1182
|
+
if (_cache !== null)
|
|
1183
|
+
return _cache;
|
|
1184
|
+
const path = _configPath();
|
|
1185
|
+
if (!existsSync3(path)) {
|
|
1186
|
+
_cache = {};
|
|
1187
|
+
return _cache;
|
|
1188
|
+
}
|
|
1189
|
+
try {
|
|
1190
|
+
const raw = readFileSync4(path, "utf-8");
|
|
1191
|
+
const parsed = JSON.parse(raw);
|
|
1192
|
+
_cache = isPlainObject(parsed) ? parsed : {};
|
|
1193
|
+
} catch {
|
|
1194
|
+
_cache = {};
|
|
1195
|
+
}
|
|
1196
|
+
return _cache;
|
|
1197
|
+
}
|
|
1198
|
+
function writeUserConfig(patch) {
|
|
1199
|
+
const current = readUserConfig();
|
|
1200
|
+
const merged = deepMerge(current, patch);
|
|
1201
|
+
const path = _configPath();
|
|
1202
|
+
const dir = dirname(path);
|
|
1203
|
+
if (!existsSync3(dir))
|
|
1204
|
+
mkdirSync3(dir, { recursive: true });
|
|
1205
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
1206
|
+
writeFileSync3(tmp, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
1207
|
+
renameSync2(tmp, path);
|
|
1208
|
+
_cache = merged;
|
|
1209
|
+
return merged;
|
|
1210
|
+
}
|
|
1211
|
+
function getEmbeddingsEnabled() {
|
|
1212
|
+
const cfg = readUserConfig();
|
|
1213
|
+
if (cfg.embeddings && typeof cfg.embeddings.enabled === "boolean") {
|
|
1214
|
+
return cfg.embeddings.enabled;
|
|
1215
|
+
}
|
|
1216
|
+
if (_migrated) {
|
|
1217
|
+
return migrationValueFromEnv();
|
|
1218
|
+
}
|
|
1219
|
+
_migrated = true;
|
|
1220
|
+
const enabled = migrationValueFromEnv();
|
|
1221
|
+
try {
|
|
1222
|
+
writeUserConfig({ embeddings: { enabled } });
|
|
1223
|
+
} catch {
|
|
1224
|
+
_cache = { ...cfg ?? {}, embeddings: { ...cfg?.embeddings ?? {}, enabled } };
|
|
1225
|
+
}
|
|
1226
|
+
return enabled;
|
|
1227
|
+
}
|
|
1228
|
+
function migrationValueFromEnv() {
|
|
1229
|
+
const raw = process.env.HIVEMIND_EMBEDDINGS;
|
|
1230
|
+
if (raw === void 0)
|
|
1231
|
+
return false;
|
|
1232
|
+
if (raw === "false")
|
|
1233
|
+
return false;
|
|
1234
|
+
return true;
|
|
1235
|
+
}
|
|
1236
|
+
function isPlainObject(value) {
|
|
1237
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1238
|
+
}
|
|
1239
|
+
function deepMerge(base, patch) {
|
|
1240
|
+
const out = { ...base };
|
|
1241
|
+
for (const key of Object.keys(patch)) {
|
|
1242
|
+
const patchVal = patch[key];
|
|
1243
|
+
const baseVal = base[key];
|
|
1244
|
+
if (isPlainObject(patchVal) && isPlainObject(baseVal)) {
|
|
1245
|
+
out[key] = { ...baseVal, ...patchVal };
|
|
1246
|
+
} else if (patchVal !== void 0) {
|
|
1247
|
+
out[key] = patchVal;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
return out;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// dist/src/embeddings/disable.js
|
|
1254
|
+
var cachedStatus = null;
|
|
1255
|
+
function defaultResolveTransformers() {
|
|
1256
|
+
const sharedDir = join6(homedir5(), ".hivemind", "embed-deps");
|
|
1257
|
+
try {
|
|
1258
|
+
createRequire(pathToFileURL(`${sharedDir}/`).href).resolve("@huggingface/transformers");
|
|
1259
|
+
return;
|
|
1260
|
+
} catch {
|
|
1261
|
+
}
|
|
1262
|
+
createRequire(import.meta.url).resolve("@huggingface/transformers");
|
|
1263
|
+
}
|
|
1264
|
+
var _resolve = defaultResolveTransformers;
|
|
1265
|
+
var _readEnabled = getEmbeddingsEnabled;
|
|
1266
|
+
function detectStatus() {
|
|
1267
|
+
if (!_readEnabled())
|
|
1268
|
+
return "user-disabled";
|
|
1269
|
+
try {
|
|
1270
|
+
_resolve();
|
|
1271
|
+
return "enabled";
|
|
1272
|
+
} catch {
|
|
1273
|
+
return "no-transformers";
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
function embeddingsStatus() {
|
|
1277
|
+
if (cachedStatus !== null)
|
|
1278
|
+
return cachedStatus;
|
|
1279
|
+
cachedStatus = detectStatus();
|
|
1280
|
+
return cachedStatus;
|
|
1281
|
+
}
|
|
1282
|
+
function embeddingsDisabled() {
|
|
1283
|
+
return embeddingsStatus() !== "enabled";
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1067
1286
|
// dist/src/embeddings/client.js
|
|
1068
|
-
var SHARED_DAEMON_PATH =
|
|
1069
|
-
var
|
|
1287
|
+
var SHARED_DAEMON_PATH = join7(homedir6(), ".hivemind", "embed-deps", "embed-daemon.js");
|
|
1288
|
+
var log4 = (m) => log("embed-client", m);
|
|
1070
1289
|
function getUid() {
|
|
1071
1290
|
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
1072
1291
|
return uid !== void 0 ? String(uid) : process.env.USER ?? "default";
|
|
1073
1292
|
}
|
|
1293
|
+
var _signalledMissingDeps = false;
|
|
1294
|
+
var _recycledStuckDaemon = false;
|
|
1074
1295
|
var EmbedClient = class {
|
|
1075
1296
|
socketPath;
|
|
1076
1297
|
pidPath;
|
|
@@ -1079,13 +1300,14 @@ var EmbedClient = class {
|
|
|
1079
1300
|
autoSpawn;
|
|
1080
1301
|
spawnWaitMs;
|
|
1081
1302
|
nextId = 0;
|
|
1303
|
+
helloVerified = false;
|
|
1082
1304
|
constructor(opts = {}) {
|
|
1083
1305
|
const uid = getUid();
|
|
1084
1306
|
const dir = opts.socketDir ?? "/tmp";
|
|
1085
1307
|
this.socketPath = socketPathFor(uid, dir);
|
|
1086
1308
|
this.pidPath = pidPathFor(uid, dir);
|
|
1087
1309
|
this.timeoutMs = opts.timeoutMs ?? DEFAULT_CLIENT_TIMEOUT_MS;
|
|
1088
|
-
this.daemonEntry = opts.daemonEntry ?? process.env.HIVEMIND_EMBED_DAEMON ?? (
|
|
1310
|
+
this.daemonEntry = opts.daemonEntry ?? process.env.HIVEMIND_EMBED_DAEMON ?? (existsSync4(SHARED_DAEMON_PATH) ? SHARED_DAEMON_PATH : void 0);
|
|
1089
1311
|
this.autoSpawn = opts.autoSpawn ?? true;
|
|
1090
1312
|
this.spawnWaitMs = opts.spawnWaitMs ?? 5e3;
|
|
1091
1313
|
}
|
|
@@ -1095,8 +1317,33 @@ var EmbedClient = class {
|
|
|
1095
1317
|
*
|
|
1096
1318
|
* Fire-and-forget spawn on miss: if the daemon isn't up, this call returns
|
|
1097
1319
|
* null AND kicks off a background spawn. The next call finds a ready daemon.
|
|
1320
|
+
*
|
|
1321
|
+
* Stuck-daemon recycle: if the daemon returns a transformers-missing
|
|
1322
|
+
* error (typical after a marketplace upgrade left an older daemon process
|
|
1323
|
+
* alive but with no node_modules accessible from its bundle path), we
|
|
1324
|
+
* SIGTERM it and clear its sock/pid so the very next call spawns a fresh
|
|
1325
|
+
* daemon from the current bundle. Without this, the stuck daemon would
|
|
1326
|
+
* keep poisoning every session until its 10-minute idle-out fires.
|
|
1098
1327
|
*/
|
|
1099
1328
|
async embed(text, kind = "document") {
|
|
1329
|
+
const v = await this.embedAttempt(text, kind);
|
|
1330
|
+
if (v !== "recycled")
|
|
1331
|
+
return v;
|
|
1332
|
+
if (!this.autoSpawn)
|
|
1333
|
+
return null;
|
|
1334
|
+
this.trySpawnDaemon();
|
|
1335
|
+
await this.waitForDaemonReady();
|
|
1336
|
+
const retry = await this.embedAttempt(text, kind);
|
|
1337
|
+
return retry === "recycled" ? null : retry;
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* One round-trip: connect → verify → embed. Returns:
|
|
1341
|
+
* - number[] : embedding vector (happy path)
|
|
1342
|
+
* - null : timeout / daemon error / transformers-missing
|
|
1343
|
+
* - "recycled": verifyDaemonOnce killed the daemon mid-call;
|
|
1344
|
+
* caller should respawn and retry once.
|
|
1345
|
+
*/
|
|
1346
|
+
async embedAttempt(text, kind) {
|
|
1100
1347
|
let sock;
|
|
1101
1348
|
try {
|
|
1102
1349
|
sock = await this.connectOnce();
|
|
@@ -1106,17 +1353,25 @@ var EmbedClient = class {
|
|
|
1106
1353
|
return null;
|
|
1107
1354
|
}
|
|
1108
1355
|
try {
|
|
1356
|
+
const recycled = await this.verifyDaemonOnce(sock);
|
|
1357
|
+
if (recycled) {
|
|
1358
|
+
return "recycled";
|
|
1359
|
+
}
|
|
1109
1360
|
const id = String(++this.nextId);
|
|
1110
1361
|
const req = { op: "embed", id, kind, text };
|
|
1111
1362
|
const resp = await this.sendAndWait(sock, req);
|
|
1112
1363
|
if (resp.error || !("embedding" in resp) || !resp.embedding) {
|
|
1113
|
-
|
|
1364
|
+
const err = resp.error ?? "no embedding";
|
|
1365
|
+
log4(`embed err: ${err}`);
|
|
1366
|
+
if (isTransformersMissingError(err)) {
|
|
1367
|
+
this.handleTransformersMissing(err);
|
|
1368
|
+
}
|
|
1114
1369
|
return null;
|
|
1115
1370
|
}
|
|
1116
1371
|
return resp.embedding;
|
|
1117
1372
|
} catch (e) {
|
|
1118
1373
|
const err = e instanceof Error ? e.message : String(e);
|
|
1119
|
-
|
|
1374
|
+
log4(`embed failed: ${err}`);
|
|
1120
1375
|
return null;
|
|
1121
1376
|
} finally {
|
|
1122
1377
|
try {
|
|
@@ -1125,6 +1380,139 @@ var EmbedClient = class {
|
|
|
1125
1380
|
}
|
|
1126
1381
|
}
|
|
1127
1382
|
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Poll for the sock file to come back after `trySpawnDaemon` — used by
|
|
1385
|
+
* the recycle retry path. Best-effort: caps at `spawnWaitMs` and
|
|
1386
|
+
* returns regardless so the retry attempt can run.
|
|
1387
|
+
*/
|
|
1388
|
+
async waitForDaemonReady() {
|
|
1389
|
+
const deadline = Date.now() + this.spawnWaitMs;
|
|
1390
|
+
while (Date.now() < deadline) {
|
|
1391
|
+
if (existsSync4(this.socketPath))
|
|
1392
|
+
return;
|
|
1393
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Send a `hello` on first successful connect per EmbedClient instance.
|
|
1398
|
+
* If the daemon answers with a path that doesn't match our configured
|
|
1399
|
+
* daemonEntry — typical after a marketplace upgrade replaced the bundle
|
|
1400
|
+
* — SIGTERM the daemon + clear sock/pid so the next call spawns from the
|
|
1401
|
+
* current bundle.
|
|
1402
|
+
*
|
|
1403
|
+
* `helloVerified` is set ONLY after we've seen a compatible response,
|
|
1404
|
+
* so a transient probe failure or a recycle-triggering mismatch leaves
|
|
1405
|
+
* the flag false; the next reconnect re-runs verification against
|
|
1406
|
+
* whatever daemon is then live (typically the fresh spawn).
|
|
1407
|
+
*/
|
|
1408
|
+
async verifyDaemonOnce(sock) {
|
|
1409
|
+
if (this.helloVerified)
|
|
1410
|
+
return false;
|
|
1411
|
+
if (!this.daemonEntry) {
|
|
1412
|
+
this.helloVerified = true;
|
|
1413
|
+
return false;
|
|
1414
|
+
}
|
|
1415
|
+
const id = String(++this.nextId);
|
|
1416
|
+
const req = { op: "hello", id };
|
|
1417
|
+
let resp;
|
|
1418
|
+
try {
|
|
1419
|
+
resp = await this.sendAndWait(sock, req);
|
|
1420
|
+
} catch (e) {
|
|
1421
|
+
log4(`hello probe failed (inconclusive, will retry next connect): ${e instanceof Error ? e.message : String(e)}`);
|
|
1422
|
+
return false;
|
|
1423
|
+
}
|
|
1424
|
+
const hello = resp;
|
|
1425
|
+
if (_recycledStuckDaemon) {
|
|
1426
|
+
return false;
|
|
1427
|
+
}
|
|
1428
|
+
if (!hello.daemonPath) {
|
|
1429
|
+
_recycledStuckDaemon = true;
|
|
1430
|
+
log4(`daemon does not implement hello (older protocol); recycling`);
|
|
1431
|
+
this.recycleDaemon(hello.pid);
|
|
1432
|
+
return true;
|
|
1433
|
+
}
|
|
1434
|
+
if (hello.daemonPath !== this.daemonEntry && !existsSync4(hello.daemonPath)) {
|
|
1435
|
+
_recycledStuckDaemon = true;
|
|
1436
|
+
log4(`daemon path no longer on disk \u2014 running=${hello.daemonPath} (gone) expected=${this.daemonEntry}; recycling`);
|
|
1437
|
+
this.recycleDaemon(hello.pid);
|
|
1438
|
+
return true;
|
|
1439
|
+
}
|
|
1440
|
+
this.helloVerified = true;
|
|
1441
|
+
return false;
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* On a transformers-missing error from the daemon, SIGTERM the stuck
|
|
1445
|
+
* daemon (the bundle daemon that can't find its deps) and clear
|
|
1446
|
+
* sock/pid so the next call spawns fresh. Also enqueue a one-time
|
|
1447
|
+
* notification telling the user to run `hivemind embeddings install`
|
|
1448
|
+
* — but only when the user has opted in. Suppressed when
|
|
1449
|
+
* embeddingsStatus() === "user-disabled" so we don't nag users who
|
|
1450
|
+
* explicitly chose to turn embeddings off.
|
|
1451
|
+
*/
|
|
1452
|
+
handleTransformersMissing(detail) {
|
|
1453
|
+
if (!_recycledStuckDaemon) {
|
|
1454
|
+
_recycledStuckDaemon = true;
|
|
1455
|
+
this.recycleDaemon(null);
|
|
1456
|
+
}
|
|
1457
|
+
if (_signalledMissingDeps)
|
|
1458
|
+
return;
|
|
1459
|
+
_signalledMissingDeps = true;
|
|
1460
|
+
let status;
|
|
1461
|
+
try {
|
|
1462
|
+
status = embeddingsStatus();
|
|
1463
|
+
} catch {
|
|
1464
|
+
status = "enabled";
|
|
1465
|
+
}
|
|
1466
|
+
if (status === "user-disabled")
|
|
1467
|
+
return;
|
|
1468
|
+
enqueueNotification({
|
|
1469
|
+
id: "embed-deps-missing",
|
|
1470
|
+
severity: "warn",
|
|
1471
|
+
title: "Hivemind embeddings disabled \u2014 deps missing",
|
|
1472
|
+
body: `Semantic memory search is off because @huggingface/transformers is not installed where the daemon can find it. Run \`hivemind embeddings install\` to enable.`,
|
|
1473
|
+
dedupKey: { reason: "transformers-missing", detail: detail.slice(0, 200) }
|
|
1474
|
+
}).catch((e) => {
|
|
1475
|
+
log4(`enqueue embed-deps-missing failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Best-effort SIGTERM + sock/pid cleanup. Tolerant of every missing-file
|
|
1480
|
+
* combination and dead-PID cases.
|
|
1481
|
+
*
|
|
1482
|
+
* Identity check: gate the SIGTERM on the daemon's socket file still
|
|
1483
|
+
* existing. We know the daemon was alive moments ago (we either just
|
|
1484
|
+
* got a hello response or the caller saw a transformers-missing error
|
|
1485
|
+
* the daemon emitted), but if the socket file is gone by the time we
|
|
1486
|
+
* try to kill, the daemon process is also gone and the PID we
|
|
1487
|
+
* captured may already have been recycled by the OS to an unrelated
|
|
1488
|
+
* user process. Mirrors the gate added to `killEmbedDaemon` in the
|
|
1489
|
+
* CLI — same failure mode, rarer trigger.
|
|
1490
|
+
*/
|
|
1491
|
+
recycleDaemon(reportedPid) {
|
|
1492
|
+
let pid = reportedPid;
|
|
1493
|
+
if (pid === null) {
|
|
1494
|
+
try {
|
|
1495
|
+
pid = Number.parseInt(readFileSync5(this.pidPath, "utf-8").trim(), 10);
|
|
1496
|
+
} catch {
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
if (Number.isFinite(pid) && pid !== null && pid > 0 && existsSync4(this.socketPath)) {
|
|
1500
|
+
try {
|
|
1501
|
+
process.kill(pid, "SIGTERM");
|
|
1502
|
+
} catch {
|
|
1503
|
+
}
|
|
1504
|
+
} else if (pid !== null) {
|
|
1505
|
+
log4(`recycle: socket gone, skipping SIGTERM on possibly-stale pid ${pid}`);
|
|
1506
|
+
}
|
|
1507
|
+
try {
|
|
1508
|
+
unlinkSync2(this.socketPath);
|
|
1509
|
+
} catch {
|
|
1510
|
+
}
|
|
1511
|
+
try {
|
|
1512
|
+
unlinkSync2(this.pidPath);
|
|
1513
|
+
} catch {
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1128
1516
|
/**
|
|
1129
1517
|
* Wait up to spawnWaitMs for the daemon to accept connections, spawning if
|
|
1130
1518
|
* necessary. Meant for SessionStart / long-running batches — not the hot path.
|
|
@@ -1148,7 +1536,7 @@ var EmbedClient = class {
|
|
|
1148
1536
|
}
|
|
1149
1537
|
}
|
|
1150
1538
|
connectOnce() {
|
|
1151
|
-
return new Promise((
|
|
1539
|
+
return new Promise((resolve3, reject) => {
|
|
1152
1540
|
const sock = connect(this.socketPath);
|
|
1153
1541
|
const to = setTimeout(() => {
|
|
1154
1542
|
sock.destroy();
|
|
@@ -1156,7 +1544,7 @@ var EmbedClient = class {
|
|
|
1156
1544
|
}, this.timeoutMs);
|
|
1157
1545
|
sock.once("connect", () => {
|
|
1158
1546
|
clearTimeout(to);
|
|
1159
|
-
|
|
1547
|
+
resolve3(sock);
|
|
1160
1548
|
});
|
|
1161
1549
|
sock.once("error", (e) => {
|
|
1162
1550
|
clearTimeout(to);
|
|
@@ -1167,16 +1555,16 @@ var EmbedClient = class {
|
|
|
1167
1555
|
trySpawnDaemon() {
|
|
1168
1556
|
let fd;
|
|
1169
1557
|
try {
|
|
1170
|
-
fd =
|
|
1558
|
+
fd = openSync2(this.pidPath, "wx", 384);
|
|
1171
1559
|
writeSync(fd, String(process.pid));
|
|
1172
1560
|
} catch (e) {
|
|
1173
1561
|
if (this.isPidFileStale()) {
|
|
1174
1562
|
try {
|
|
1175
|
-
|
|
1563
|
+
unlinkSync2(this.pidPath);
|
|
1176
1564
|
} catch {
|
|
1177
1565
|
}
|
|
1178
1566
|
try {
|
|
1179
|
-
fd =
|
|
1567
|
+
fd = openSync2(this.pidPath, "wx", 384);
|
|
1180
1568
|
writeSync(fd, String(process.pid));
|
|
1181
1569
|
} catch {
|
|
1182
1570
|
return;
|
|
@@ -1185,11 +1573,11 @@ var EmbedClient = class {
|
|
|
1185
1573
|
return;
|
|
1186
1574
|
}
|
|
1187
1575
|
}
|
|
1188
|
-
if (!this.daemonEntry || !
|
|
1189
|
-
|
|
1576
|
+
if (!this.daemonEntry || !existsSync4(this.daemonEntry)) {
|
|
1577
|
+
log4(`daemonEntry not configured or missing: ${this.daemonEntry}`);
|
|
1190
1578
|
try {
|
|
1191
|
-
|
|
1192
|
-
|
|
1579
|
+
closeSync2(fd);
|
|
1580
|
+
unlinkSync2(this.pidPath);
|
|
1193
1581
|
} catch {
|
|
1194
1582
|
}
|
|
1195
1583
|
return;
|
|
@@ -1201,14 +1589,14 @@ var EmbedClient = class {
|
|
|
1201
1589
|
env: process.env
|
|
1202
1590
|
});
|
|
1203
1591
|
child.unref();
|
|
1204
|
-
|
|
1592
|
+
log4(`spawned daemon pid=${child.pid}`);
|
|
1205
1593
|
} finally {
|
|
1206
|
-
|
|
1594
|
+
closeSync2(fd);
|
|
1207
1595
|
}
|
|
1208
1596
|
}
|
|
1209
1597
|
isPidFileStale() {
|
|
1210
1598
|
try {
|
|
1211
|
-
const raw =
|
|
1599
|
+
const raw = readFileSync5(this.pidPath, "utf-8").trim();
|
|
1212
1600
|
const pid = Number(raw);
|
|
1213
1601
|
if (!pid || Number.isNaN(pid))
|
|
1214
1602
|
return true;
|
|
@@ -1226,9 +1614,9 @@ var EmbedClient = class {
|
|
|
1226
1614
|
const deadline = Date.now() + this.spawnWaitMs;
|
|
1227
1615
|
let delay = 30;
|
|
1228
1616
|
while (Date.now() < deadline) {
|
|
1229
|
-
await
|
|
1617
|
+
await sleep3(delay);
|
|
1230
1618
|
delay = Math.min(delay * 1.5, 300);
|
|
1231
|
-
if (!
|
|
1619
|
+
if (!existsSync4(this.socketPath))
|
|
1232
1620
|
continue;
|
|
1233
1621
|
try {
|
|
1234
1622
|
return await this.connectOnce();
|
|
@@ -1238,7 +1626,7 @@ var EmbedClient = class {
|
|
|
1238
1626
|
throw new Error("daemon did not become ready within spawnWaitMs");
|
|
1239
1627
|
}
|
|
1240
1628
|
sendAndWait(sock, req) {
|
|
1241
|
-
return new Promise((
|
|
1629
|
+
return new Promise((resolve3, reject) => {
|
|
1242
1630
|
let buf = "";
|
|
1243
1631
|
const to = setTimeout(() => {
|
|
1244
1632
|
sock.destroy();
|
|
@@ -1253,7 +1641,7 @@ var EmbedClient = class {
|
|
|
1253
1641
|
const line = buf.slice(0, nl);
|
|
1254
1642
|
clearTimeout(to);
|
|
1255
1643
|
try {
|
|
1256
|
-
|
|
1644
|
+
resolve3(JSON.parse(line));
|
|
1257
1645
|
} catch (e) {
|
|
1258
1646
|
reject(e);
|
|
1259
1647
|
}
|
|
@@ -1270,53 +1658,22 @@ var EmbedClient = class {
|
|
|
1270
1658
|
});
|
|
1271
1659
|
}
|
|
1272
1660
|
};
|
|
1273
|
-
function
|
|
1661
|
+
function sleep3(ms) {
|
|
1274
1662
|
return new Promise((r) => setTimeout(r, ms));
|
|
1275
1663
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
import { join as join5 } from "node:path";
|
|
1281
|
-
import { pathToFileURL } from "node:url";
|
|
1282
|
-
var cachedStatus = null;
|
|
1283
|
-
function defaultResolveTransformers() {
|
|
1284
|
-
try {
|
|
1285
|
-
createRequire(import.meta.url).resolve("@huggingface/transformers");
|
|
1286
|
-
return;
|
|
1287
|
-
} catch {
|
|
1288
|
-
}
|
|
1289
|
-
const sharedDir = join5(homedir4(), ".hivemind", "embed-deps");
|
|
1290
|
-
createRequire(pathToFileURL(`${sharedDir}/`).href).resolve("@huggingface/transformers");
|
|
1291
|
-
}
|
|
1292
|
-
var _resolve = defaultResolveTransformers;
|
|
1293
|
-
function detectStatus() {
|
|
1294
|
-
if (process.env.HIVEMIND_EMBEDDINGS === "false")
|
|
1295
|
-
return "env-disabled";
|
|
1296
|
-
try {
|
|
1297
|
-
_resolve();
|
|
1298
|
-
return "enabled";
|
|
1299
|
-
} catch {
|
|
1300
|
-
return "no-transformers";
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
function embeddingsStatus() {
|
|
1304
|
-
if (cachedStatus !== null)
|
|
1305
|
-
return cachedStatus;
|
|
1306
|
-
cachedStatus = detectStatus();
|
|
1307
|
-
return cachedStatus;
|
|
1308
|
-
}
|
|
1309
|
-
function embeddingsDisabled() {
|
|
1310
|
-
return embeddingsStatus() !== "enabled";
|
|
1664
|
+
function isTransformersMissingError(err) {
|
|
1665
|
+
if (/hivemind embeddings install/i.test(err))
|
|
1666
|
+
return true;
|
|
1667
|
+
return /@huggingface\/transformers/i.test(err);
|
|
1311
1668
|
}
|
|
1312
1669
|
|
|
1313
1670
|
// dist/src/hooks/grep-direct.js
|
|
1314
1671
|
import { fileURLToPath } from "node:url";
|
|
1315
|
-
import { dirname, join as
|
|
1672
|
+
import { dirname as dirname2, join as join8 } from "node:path";
|
|
1316
1673
|
var SEMANTIC_ENABLED = process.env.HIVEMIND_SEMANTIC_SEARCH !== "false" && !embeddingsDisabled();
|
|
1317
1674
|
var SEMANTIC_TIMEOUT_MS = Number(process.env.HIVEMIND_SEMANTIC_EMBED_TIMEOUT_MS ?? "500");
|
|
1318
1675
|
function resolveDaemonPath() {
|
|
1319
|
-
return
|
|
1676
|
+
return join8(dirname2(fileURLToPath(import.meta.url)), "..", "embeddings", "embed-daemon.js");
|
|
1320
1677
|
}
|
|
1321
1678
|
var sharedEmbedClient = null;
|
|
1322
1679
|
function getEmbedClient() {
|
|
@@ -2319,20 +2676,20 @@ async function executeCompiledBashCommand(api, memoryTable, sessionsTable, cmd,
|
|
|
2319
2676
|
}
|
|
2320
2677
|
|
|
2321
2678
|
// dist/src/hooks/query-cache.js
|
|
2322
|
-
import { mkdirSync as
|
|
2323
|
-
import { join as
|
|
2324
|
-
import { homedir as
|
|
2325
|
-
var
|
|
2326
|
-
var DEFAULT_CACHE_ROOT =
|
|
2679
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2680
|
+
import { join as join9 } from "node:path";
|
|
2681
|
+
import { homedir as homedir7 } from "node:os";
|
|
2682
|
+
var log5 = (msg) => log("query-cache", msg);
|
|
2683
|
+
var DEFAULT_CACHE_ROOT = join9(homedir7(), ".deeplake", "query-cache");
|
|
2327
2684
|
var INDEX_CACHE_FILE = "index.md";
|
|
2328
2685
|
function getSessionQueryCacheDir(sessionId, deps = {}) {
|
|
2329
2686
|
const { cacheRoot = DEFAULT_CACHE_ROOT } = deps;
|
|
2330
|
-
return
|
|
2687
|
+
return join9(cacheRoot, sessionId);
|
|
2331
2688
|
}
|
|
2332
2689
|
function readCachedIndexContent(sessionId, deps = {}) {
|
|
2333
|
-
const { logFn =
|
|
2690
|
+
const { logFn = log5 } = deps;
|
|
2334
2691
|
try {
|
|
2335
|
-
return
|
|
2692
|
+
return readFileSync6(join9(getSessionQueryCacheDir(sessionId, deps), INDEX_CACHE_FILE), "utf-8");
|
|
2336
2693
|
} catch (e) {
|
|
2337
2694
|
if (e?.code === "ENOENT")
|
|
2338
2695
|
return null;
|
|
@@ -2341,34 +2698,34 @@ function readCachedIndexContent(sessionId, deps = {}) {
|
|
|
2341
2698
|
}
|
|
2342
2699
|
}
|
|
2343
2700
|
function writeCachedIndexContent(sessionId, content, deps = {}) {
|
|
2344
|
-
const { logFn =
|
|
2701
|
+
const { logFn = log5 } = deps;
|
|
2345
2702
|
try {
|
|
2346
2703
|
const dir = getSessionQueryCacheDir(sessionId, deps);
|
|
2347
|
-
|
|
2348
|
-
|
|
2704
|
+
mkdirSync4(dir, { recursive: true });
|
|
2705
|
+
writeFileSync4(join9(dir, INDEX_CACHE_FILE), content, "utf-8");
|
|
2349
2706
|
} catch (e) {
|
|
2350
2707
|
logFn(`write failed for session=${sessionId}: ${e.message}`);
|
|
2351
2708
|
}
|
|
2352
2709
|
}
|
|
2353
2710
|
|
|
2354
2711
|
// dist/src/utils/direct-run.js
|
|
2355
|
-
import { resolve } from "node:path";
|
|
2712
|
+
import { resolve as resolve2 } from "node:path";
|
|
2356
2713
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2357
2714
|
function isDirectRun(metaUrl) {
|
|
2358
2715
|
const entry = process.argv[1];
|
|
2359
2716
|
if (!entry)
|
|
2360
2717
|
return false;
|
|
2361
2718
|
try {
|
|
2362
|
-
return
|
|
2719
|
+
return resolve2(fileURLToPath2(metaUrl)) === resolve2(entry);
|
|
2363
2720
|
} catch {
|
|
2364
2721
|
return false;
|
|
2365
2722
|
}
|
|
2366
2723
|
}
|
|
2367
2724
|
|
|
2368
2725
|
// dist/src/hooks/memory-path-utils.js
|
|
2369
|
-
import { homedir as
|
|
2370
|
-
import { join as
|
|
2371
|
-
var MEMORY_PATH =
|
|
2726
|
+
import { homedir as homedir8 } from "node:os";
|
|
2727
|
+
import { join as join10 } from "node:path";
|
|
2728
|
+
var MEMORY_PATH = join10(homedir8(), ".deeplake", "memory");
|
|
2372
2729
|
var TILDE_PATH = "~/.deeplake/memory";
|
|
2373
2730
|
var HOME_VAR_PATH = "$HOME/.deeplake/memory";
|
|
2374
2731
|
var SAFE_BUILTINS = /* @__PURE__ */ new Set([
|
|
@@ -2484,13 +2841,13 @@ function rewritePaths(cmd) {
|
|
|
2484
2841
|
}
|
|
2485
2842
|
|
|
2486
2843
|
// dist/src/hooks/codex/pre-tool-use.js
|
|
2487
|
-
var
|
|
2488
|
-
var __bundleDir =
|
|
2489
|
-
var SHELL_BUNDLE =
|
|
2844
|
+
var log6 = (msg) => log("codex-pre", msg);
|
|
2845
|
+
var __bundleDir = dirname3(fileURLToPath3(import.meta.url));
|
|
2846
|
+
var SHELL_BUNDLE = existsSync5(join11(__bundleDir, "shell", "deeplake-shell.js")) ? join11(__bundleDir, "shell", "deeplake-shell.js") : join11(__bundleDir, "..", "shell", "deeplake-shell.js");
|
|
2490
2847
|
function buildUnsupportedGuidance() {
|
|
2491
2848
|
return "This command is not supported for ~/.deeplake/memory/ operations. Only bash builtins are available: cat, ls, grep, echo, jq, head, tail, sed, awk, wc, sort, find, etc. Do NOT use python, python3, node, curl, or other interpreters. Rewrite your command using only bash tools and retry.";
|
|
2492
2849
|
}
|
|
2493
|
-
function runVirtualShell(cmd, shellBundle = SHELL_BUNDLE, logFn =
|
|
2850
|
+
function runVirtualShell(cmd, shellBundle = SHELL_BUNDLE, logFn = log6) {
|
|
2494
2851
|
try {
|
|
2495
2852
|
return execFileSync("node", [shellBundle, "-c", cmd], {
|
|
2496
2853
|
encoding: "utf-8",
|
|
@@ -2515,7 +2872,7 @@ function buildIndexContent(rows) {
|
|
|
2515
2872
|
return lines.join("\n");
|
|
2516
2873
|
}
|
|
2517
2874
|
async function processCodexPreToolUse(input, deps = {}) {
|
|
2518
|
-
const { config = loadConfig(), createApi = (table, activeConfig) => new DeeplakeApi(activeConfig.token, activeConfig.apiUrl, activeConfig.orgId, activeConfig.workspaceId, table), executeCompiledBashCommandFn = executeCompiledBashCommand, readVirtualPathContentsFn = readVirtualPathContents, readVirtualPathContentFn = readVirtualPathContent, listVirtualPathRowsFn = listVirtualPathRows, findVirtualPathsFn = findVirtualPaths, handleGrepDirectFn = handleGrepDirect, readCachedIndexContentFn = readCachedIndexContent, writeCachedIndexContentFn = writeCachedIndexContent, runVirtualShellFn = runVirtualShell, shellBundle = SHELL_BUNDLE, logFn =
|
|
2875
|
+
const { config = loadConfig(), createApi = (table, activeConfig) => new DeeplakeApi(activeConfig.token, activeConfig.apiUrl, activeConfig.orgId, activeConfig.workspaceId, table), executeCompiledBashCommandFn = executeCompiledBashCommand, readVirtualPathContentsFn = readVirtualPathContents, readVirtualPathContentFn = readVirtualPathContent, listVirtualPathRowsFn = listVirtualPathRows, findVirtualPathsFn = findVirtualPaths, handleGrepDirectFn = handleGrepDirect, readCachedIndexContentFn = readCachedIndexContent, writeCachedIndexContentFn = writeCachedIndexContent, runVirtualShellFn = runVirtualShell, shellBundle = SHELL_BUNDLE, logFn = log6 } = deps;
|
|
2519
2876
|
const cmd = input.tool_input?.command ?? "";
|
|
2520
2877
|
logFn(`hook fired: cmd=${cmd}`);
|
|
2521
2878
|
if (!touchesMemory(cmd))
|
|
@@ -2725,7 +3082,7 @@ async function main() {
|
|
|
2725
3082
|
}
|
|
2726
3083
|
if (isDirectRun(import.meta.url)) {
|
|
2727
3084
|
main().catch((e) => {
|
|
2728
|
-
|
|
3085
|
+
log6(`fatal: ${e.message}`);
|
|
2729
3086
|
process.exit(0);
|
|
2730
3087
|
});
|
|
2731
3088
|
}
|