@askexenow/exe-os 0.9.64 → 0.9.66
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/deploy/stack-manifests/v0.9.json +4 -4
- package/dist/bin/backfill-conversations.js +22 -0
- package/dist/bin/backfill-responses.js +22 -0
- package/dist/bin/backfill-vectors.js +22 -0
- package/dist/bin/cleanup-stale-review-tasks.js +22 -0
- package/dist/bin/cli.js +2280 -1199
- package/dist/bin/exe-agent-config.js +4 -0
- package/dist/bin/exe-agent.js +16 -0
- package/dist/bin/exe-assign.js +22 -0
- package/dist/bin/exe-boot.js +116 -7
- package/dist/bin/exe-call.js +16 -0
- package/dist/bin/exe-cloud.js +6671 -464
- package/dist/bin/exe-dispatch.js +24 -0
- package/dist/bin/exe-doctor.js +2845 -1223
- package/dist/bin/exe-export-behaviors.js +24 -0
- package/dist/bin/exe-forget.js +22 -0
- package/dist/bin/exe-gateway.js +24 -0
- package/dist/bin/exe-heartbeat.js +23 -0
- package/dist/bin/exe-kill.js +22 -0
- package/dist/bin/exe-launch-agent.js +24 -0
- package/dist/bin/exe-link.js +310 -178
- package/dist/bin/exe-new-employee.js +127 -1
- package/dist/bin/exe-pending-messages.js +22 -0
- package/dist/bin/exe-pending-notifications.js +22 -0
- package/dist/bin/exe-pending-reviews.js +22 -0
- package/dist/bin/exe-rename.js +22 -0
- package/dist/bin/exe-review.js +22 -0
- package/dist/bin/exe-search.js +24 -0
- package/dist/bin/exe-session-cleanup.js +24 -0
- package/dist/bin/exe-settings.js +10 -0
- package/dist/bin/exe-start-codex.js +135 -1
- package/dist/bin/exe-start-opencode.js +149 -1
- package/dist/bin/exe-status.js +22 -0
- package/dist/bin/exe-team.js +22 -0
- package/dist/bin/git-sweep.js +24 -0
- package/dist/bin/graph-backfill.js +22 -0
- package/dist/bin/graph-export.js +22 -0
- package/dist/bin/install.js +115 -1
- package/dist/bin/intercom-check.js +24 -0
- package/dist/bin/scan-tasks.js +24 -0
- package/dist/bin/setup.js +412 -157
- package/dist/bin/shard-migrate.js +22 -0
- package/dist/bin/update.js +4 -0
- package/dist/gateway/index.js +24 -0
- package/dist/hooks/bug-report-worker.js +135 -42
- package/dist/hooks/codex-stop-task-finalizer.js +24 -0
- package/dist/hooks/commit-complete.js +24 -0
- package/dist/hooks/error-recall.js +24 -0
- package/dist/hooks/exe-heartbeat-hook.js +4 -0
- package/dist/hooks/ingest-worker.js +4 -0
- package/dist/hooks/ingest.js +23 -0
- package/dist/hooks/instructions-loaded.js +22 -0
- package/dist/hooks/notification.js +22 -0
- package/dist/hooks/post-compact.js +22 -0
- package/dist/hooks/post-tool-combined.js +24 -0
- package/dist/hooks/pre-compact.js +260 -109
- package/dist/hooks/pre-tool-use.js +22 -0
- package/dist/hooks/prompt-submit.js +24 -0
- package/dist/hooks/session-end.js +161 -122
- package/dist/hooks/session-start.js +142 -0
- package/dist/hooks/stop.js +23 -0
- package/dist/hooks/subagent-stop.js +22 -0
- package/dist/hooks/summary-worker.js +195 -79
- package/dist/index.js +24 -0
- package/dist/lib/agent-config.js +4 -0
- package/dist/lib/cloud-sync.js +50 -6
- package/dist/lib/config.js +12 -0
- package/dist/lib/consolidation.js +4 -0
- package/dist/lib/database.js +4 -0
- package/dist/lib/db-daemon-client.js +4 -0
- package/dist/lib/db.js +4 -0
- package/dist/lib/device-registry.js +4 -0
- package/dist/lib/embedder.js +12 -0
- package/dist/lib/employee-templates.js +16 -0
- package/dist/lib/employees.js +4 -0
- package/dist/lib/exe-daemon-client.js +4 -0
- package/dist/lib/exe-daemon.js +1144 -480
- package/dist/lib/hybrid-search.js +24 -0
- package/dist/lib/identity.js +4 -0
- package/dist/lib/license.js +4 -0
- package/dist/lib/messaging.js +4 -0
- package/dist/lib/reminders.js +4 -0
- package/dist/lib/schedules.js +22 -0
- package/dist/lib/skill-learning.js +12 -0
- package/dist/lib/status-brief.js +39 -0
- package/dist/lib/store.js +22 -0
- package/dist/lib/task-router.js +4 -0
- package/dist/lib/tasks.js +12 -0
- package/dist/lib/tmux-routing.js +12 -0
- package/dist/lib/token-spend.js +4 -0
- package/dist/mcp/server.js +1045 -427
- package/dist/mcp/tools/complete-reminder.js +4 -0
- package/dist/mcp/tools/create-reminder.js +4 -0
- package/dist/mcp/tools/create-task.js +12 -0
- package/dist/mcp/tools/deactivate-behavior.js +4 -0
- package/dist/mcp/tools/list-reminders.js +4 -0
- package/dist/mcp/tools/list-tasks.js +4 -0
- package/dist/mcp/tools/send-message.js +4 -0
- package/dist/mcp/tools/update-task.js +12 -0
- package/dist/runtime/index.js +24 -0
- package/dist/tui/App.js +24 -0
- package/package.json +3 -2
- package/src/commands/exe/cloud.md +15 -8
- package/src/commands/exe/link.md +7 -6
- package/stack.release.json +2 -2
package/dist/bin/exe-doctor.js
CHANGED
|
@@ -15,70 +15,6 @@ var __export = (target, all) => {
|
|
|
15
15
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
// src/types/memory.ts
|
|
19
|
-
var EMBEDDING_DIM;
|
|
20
|
-
var init_memory = __esm({
|
|
21
|
-
"src/types/memory.ts"() {
|
|
22
|
-
"use strict";
|
|
23
|
-
EMBEDDING_DIM = 1024;
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
// src/lib/db-retry.ts
|
|
28
|
-
function isBusyError(err) {
|
|
29
|
-
if (err instanceof Error) {
|
|
30
|
-
const msg = err.message.toLowerCase();
|
|
31
|
-
return msg.includes("sqlite_busy") || msg.includes("database is locked");
|
|
32
|
-
}
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
function delay(ms) {
|
|
36
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
37
|
-
}
|
|
38
|
-
async function retryOnBusy(fn, label) {
|
|
39
|
-
let lastError;
|
|
40
|
-
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
41
|
-
try {
|
|
42
|
-
return await fn();
|
|
43
|
-
} catch (err) {
|
|
44
|
-
lastError = err;
|
|
45
|
-
if (!isBusyError(err) || attempt === MAX_RETRIES) {
|
|
46
|
-
throw err;
|
|
47
|
-
}
|
|
48
|
-
const backoff = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
49
|
-
const jitter = Math.floor(Math.random() * MAX_JITTER_MS);
|
|
50
|
-
process.stderr.write(
|
|
51
|
-
`[exe-os] SQLITE_BUSY ${label} retry ${attempt + 1}/${MAX_RETRIES} \u2014 waiting ${backoff + jitter}ms
|
|
52
|
-
`
|
|
53
|
-
);
|
|
54
|
-
await delay(backoff + jitter);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
throw lastError;
|
|
58
|
-
}
|
|
59
|
-
function wrapWithRetry(client) {
|
|
60
|
-
return new Proxy(client, {
|
|
61
|
-
get(target, prop, receiver) {
|
|
62
|
-
if (prop === "execute") {
|
|
63
|
-
return (sql) => retryOnBusy(() => target.execute(sql), "execute");
|
|
64
|
-
}
|
|
65
|
-
if (prop === "batch") {
|
|
66
|
-
return (stmts, mode) => retryOnBusy(() => target.batch(stmts, mode), "batch");
|
|
67
|
-
}
|
|
68
|
-
return Reflect.get(target, prop, receiver);
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
var MAX_RETRIES, BASE_DELAY_MS, MAX_JITTER_MS;
|
|
73
|
-
var init_db_retry = __esm({
|
|
74
|
-
"src/lib/db-retry.ts"() {
|
|
75
|
-
"use strict";
|
|
76
|
-
MAX_RETRIES = 5;
|
|
77
|
-
BASE_DELAY_MS = 250;
|
|
78
|
-
MAX_JITTER_MS = 400;
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
18
|
// src/lib/secure-files.ts
|
|
83
19
|
import { chmodSync, existsSync, mkdirSync } from "fs";
|
|
84
20
|
import { chmod, mkdir } from "fs/promises";
|
|
@@ -188,6 +124,11 @@ function normalizeAutoUpdate(raw) {
|
|
|
188
124
|
const userAU = raw.autoUpdate ?? {};
|
|
189
125
|
raw.autoUpdate = { ...defaultAU, ...userAU };
|
|
190
126
|
}
|
|
127
|
+
function normalizeOrchestration(raw) {
|
|
128
|
+
const defaultOrg = DEFAULT_CONFIG.orchestration;
|
|
129
|
+
const userOrg = raw.orchestration ?? {};
|
|
130
|
+
raw.orchestration = { ...defaultOrg, ...userOrg };
|
|
131
|
+
}
|
|
191
132
|
async function loadConfig() {
|
|
192
133
|
const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
|
|
193
134
|
await ensurePrivateDir(dir);
|
|
@@ -212,6 +153,7 @@ async function loadConfig() {
|
|
|
212
153
|
normalizeScalingRoadmap(migratedCfg);
|
|
213
154
|
normalizeSessionLifecycle(migratedCfg);
|
|
214
155
|
normalizeAutoUpdate(migratedCfg);
|
|
156
|
+
normalizeOrchestration(migratedCfg);
|
|
215
157
|
const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
|
|
216
158
|
if (config.dbPath.startsWith("~")) {
|
|
217
159
|
config.dbPath = config.dbPath.replace(/^~/, os.homedir());
|
|
@@ -287,6 +229,10 @@ var init_config = __esm({
|
|
|
287
229
|
checkOnBoot: true,
|
|
288
230
|
autoInstall: false,
|
|
289
231
|
checkIntervalMs: 24 * 60 * 60 * 1e3
|
|
232
|
+
},
|
|
233
|
+
orchestration: {
|
|
234
|
+
phase: "phase_1_coo",
|
|
235
|
+
phaseSetBy: "default"
|
|
290
236
|
}
|
|
291
237
|
};
|
|
292
238
|
CONFIG_MIGRATIONS = [
|
|
@@ -302,162 +248,1104 @@ var init_config = __esm({
|
|
|
302
248
|
}
|
|
303
249
|
});
|
|
304
250
|
|
|
305
|
-
// src/lib/
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
251
|
+
// src/lib/shard-manager.ts
|
|
252
|
+
var shard_manager_exports = {};
|
|
253
|
+
__export(shard_manager_exports, {
|
|
254
|
+
auditShardHealth: () => auditShardHealth,
|
|
255
|
+
disposeShards: () => disposeShards,
|
|
256
|
+
ensureShardSchema: () => ensureShardSchema,
|
|
257
|
+
getOpenShardCount: () => getOpenShardCount,
|
|
258
|
+
getReadyShardClient: () => getReadyShardClient,
|
|
259
|
+
getShardClient: () => getShardClient,
|
|
260
|
+
getShardsDir: () => getShardsDir,
|
|
261
|
+
initShardManager: () => initShardManager,
|
|
262
|
+
isShardingEnabled: () => isShardingEnabled,
|
|
263
|
+
listShards: () => listShards,
|
|
264
|
+
shardExists: () => shardExists
|
|
265
|
+
});
|
|
309
266
|
import path2 from "path";
|
|
310
|
-
import
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
function getCoordinatorEmployee(employees) {
|
|
318
|
-
return employees.find((e) => isCoordinatorRole(e.role));
|
|
319
|
-
}
|
|
320
|
-
function getCoordinatorName(employees = loadEmployeesSync()) {
|
|
321
|
-
return getCoordinatorEmployee(employees)?.name ?? DEFAULT_COORDINATOR_TEMPLATE_NAME;
|
|
322
|
-
}
|
|
323
|
-
function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
|
|
324
|
-
if (!existsSync3(employeesPath)) return [];
|
|
325
|
-
try {
|
|
326
|
-
return JSON.parse(readFileSync2(employeesPath, "utf-8"));
|
|
327
|
-
} catch {
|
|
328
|
-
return [];
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, IDENTITY_DIR;
|
|
332
|
-
var init_employees = __esm({
|
|
333
|
-
"src/lib/employees.ts"() {
|
|
334
|
-
"use strict";
|
|
335
|
-
init_config();
|
|
336
|
-
EMPLOYEES_PATH = path2.join(EXE_AI_DIR, "exe-employees.json");
|
|
337
|
-
DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
|
|
338
|
-
COORDINATOR_ROLE = "COO";
|
|
339
|
-
IDENTITY_DIR = path2.join(EXE_AI_DIR, "identity");
|
|
267
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, renameSync as renameSync2, statSync } from "fs";
|
|
268
|
+
import { createClient } from "@libsql/client";
|
|
269
|
+
function initShardManager(encryptionKey) {
|
|
270
|
+
_encryptionKey = encryptionKey;
|
|
271
|
+
if (!existsSync3(SHARDS_DIR)) {
|
|
272
|
+
mkdirSync2(SHARDS_DIR, { recursive: true });
|
|
340
273
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
import path3 from "path";
|
|
346
|
-
import { createRequire } from "module";
|
|
347
|
-
import { pathToFileURL } from "url";
|
|
348
|
-
function quotedIdentifier(identifier) {
|
|
349
|
-
return `"${identifier.replace(/"/g, '""')}"`;
|
|
274
|
+
_shardingEnabled = true;
|
|
275
|
+
if (_evictionTimer) clearInterval(_evictionTimer);
|
|
276
|
+
_evictionTimer = setInterval(evictIdleShards, EVICTION_INTERVAL_MS);
|
|
277
|
+
_evictionTimer.unref();
|
|
350
278
|
}
|
|
351
|
-
function
|
|
352
|
-
|
|
353
|
-
const parts = raw.split(".");
|
|
354
|
-
return parts[parts.length - 1].replace(/^"|"$/g, "").toLowerCase();
|
|
279
|
+
function isShardingEnabled() {
|
|
280
|
+
return _shardingEnabled;
|
|
355
281
|
}
|
|
356
|
-
function
|
|
357
|
-
return
|
|
282
|
+
function getShardsDir() {
|
|
283
|
+
return SHARDS_DIR;
|
|
358
284
|
}
|
|
359
|
-
function
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (!returningMatch) {
|
|
363
|
-
return `${trimmed}${clause}`;
|
|
285
|
+
function getShardClient(projectName) {
|
|
286
|
+
if (!_encryptionKey) {
|
|
287
|
+
throw new Error("Shard manager not initialized. Call initShardManager() first.");
|
|
364
288
|
}
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
function normalizeStatement(stmt) {
|
|
369
|
-
if (typeof stmt === "string") {
|
|
370
|
-
return { kind: "positional", sql: stmt, args: [] };
|
|
289
|
+
const safeName = safeShardName(projectName);
|
|
290
|
+
if (!safeName || safeName === "unknown") {
|
|
291
|
+
throw new Error(`Invalid project name for shard: "${projectName}" (resolved to "${safeName}")`);
|
|
371
292
|
}
|
|
372
|
-
const
|
|
373
|
-
if (
|
|
374
|
-
|
|
293
|
+
const cached = _shards.get(safeName);
|
|
294
|
+
if (cached) {
|
|
295
|
+
_shardLastAccess.set(safeName, Date.now());
|
|
296
|
+
return cached;
|
|
375
297
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
function rewriteBooleanLiterals(sql) {
|
|
379
|
-
let out = sql;
|
|
380
|
-
for (const column of BOOLEAN_COLUMN_NAMES) {
|
|
381
|
-
const scoped = `((?:\\b[a-z_][a-z0-9_]*\\.)?${column})`;
|
|
382
|
-
out = out.replace(new RegExp(`${scoped}\\s*=\\s*0\\b`, "giu"), "$1 = FALSE");
|
|
383
|
-
out = out.replace(new RegExp(`${scoped}\\s*=\\s*1\\b`, "giu"), "$1 = TRUE");
|
|
384
|
-
out = out.replace(new RegExp(`${scoped}\\s*!=\\s*0\\b`, "giu"), "$1 != FALSE");
|
|
385
|
-
out = out.replace(new RegExp(`${scoped}\\s*!=\\s*1\\b`, "giu"), "$1 != TRUE");
|
|
386
|
-
out = out.replace(new RegExp(`${scoped}\\s*<>\\s*0\\b`, "giu"), "$1 <> FALSE");
|
|
387
|
-
out = out.replace(new RegExp(`${scoped}\\s*<>\\s*1\\b`, "giu"), "$1 <> TRUE");
|
|
298
|
+
while (_shards.size >= MAX_OPEN_SHARDS) {
|
|
299
|
+
evictLRU();
|
|
388
300
|
}
|
|
389
|
-
|
|
301
|
+
const dbPath = path2.join(SHARDS_DIR, `${safeName}.db`);
|
|
302
|
+
const client = createClient({
|
|
303
|
+
url: `file:${dbPath}`,
|
|
304
|
+
encryptionKey: _encryptionKey
|
|
305
|
+
});
|
|
306
|
+
_shards.set(safeName, client);
|
|
307
|
+
_shardLastAccess.set(safeName, Date.now());
|
|
308
|
+
return client;
|
|
390
309
|
}
|
|
391
|
-
function
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
const replaced = sql.replace(/^\s*INSERT\s+OR\s+IGNORE\s+INTO\b/iu, "INSERT INTO");
|
|
396
|
-
return /\bON\s+CONFLICT\b/iu.test(replaced) ? replaced : appendClause(replaced, " ON CONFLICT DO NOTHING");
|
|
310
|
+
function shardExists(projectName) {
|
|
311
|
+
const safeName = safeShardName(projectName);
|
|
312
|
+
return existsSync3(path2.join(SHARDS_DIR, `${safeName}.db`));
|
|
397
313
|
}
|
|
398
|
-
function
|
|
399
|
-
|
|
400
|
-
if (!match) {
|
|
401
|
-
return sql;
|
|
402
|
-
}
|
|
403
|
-
const rawTable = match[1];
|
|
404
|
-
const rawColumns = match[2];
|
|
405
|
-
const remainder = match[3];
|
|
406
|
-
const tableName = unqualifiedTableName(rawTable);
|
|
407
|
-
const conflictKeys = UPSERT_KEYS[tableName];
|
|
408
|
-
if (!conflictKeys?.length) {
|
|
409
|
-
return sql;
|
|
410
|
-
}
|
|
411
|
-
const columns = rawColumns.split(",").map((col) => col.trim().replace(/^"|"$/g, ""));
|
|
412
|
-
const updateColumns = columns.filter((col) => !conflictKeys.includes(col));
|
|
413
|
-
const conflictTarget = conflictKeys.map(quotedIdentifier).join(", ");
|
|
414
|
-
const updateClause = updateColumns.length === 0 ? " DO NOTHING" : ` DO UPDATE SET ${updateColumns.map((col) => `${quotedIdentifier(col)} = EXCLUDED.${quotedIdentifier(col)}`).join(", ")}`;
|
|
415
|
-
return `INSERT INTO ${rawTable} (${rawColumns})${appendClause(remainder, ` ON CONFLICT (${conflictTarget})${updateClause}`)}`;
|
|
314
|
+
function safeShardName(projectName) {
|
|
315
|
+
return projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
416
316
|
}
|
|
417
|
-
function
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
out = out.replace(/\bvector32\s*\(\s*\?\s*\)/giu, "?");
|
|
421
|
-
out = rewriteBooleanLiterals(out);
|
|
422
|
-
out = rewriteInsertOrReplace(out);
|
|
423
|
-
out = rewriteInsertOrIgnore(out);
|
|
424
|
-
return stripTrailingSemicolon(out);
|
|
317
|
+
function listShards() {
|
|
318
|
+
if (!existsSync3(SHARDS_DIR)) return [];
|
|
319
|
+
return readdirSync(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
|
|
425
320
|
}
|
|
426
|
-
function
|
|
427
|
-
if (
|
|
428
|
-
|
|
429
|
-
if (typeof value === "number") return value !== 0;
|
|
430
|
-
if (typeof value === "bigint") return value !== 0n;
|
|
431
|
-
if (typeof value === "string") {
|
|
432
|
-
const normalized = value.trim().toLowerCase();
|
|
433
|
-
if (normalized === "0" || normalized === "false") return false;
|
|
434
|
-
if (normalized === "1" || normalized === "true") return true;
|
|
321
|
+
async function auditShardHealth(options = {}) {
|
|
322
|
+
if (!_encryptionKey) {
|
|
323
|
+
throw new Error("Shard manager not initialized. Call initShardManager() first.");
|
|
435
324
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
325
|
+
const repair = options.repair === true;
|
|
326
|
+
const dryRun = options.dryRun === true;
|
|
327
|
+
const names = listShards();
|
|
328
|
+
const shards = [];
|
|
329
|
+
for (const name of names) {
|
|
330
|
+
const dbPath = path2.join(SHARDS_DIR, `${name}.db`);
|
|
331
|
+
const stat = statSync(dbPath);
|
|
332
|
+
const item = {
|
|
333
|
+
name,
|
|
334
|
+
path: dbPath,
|
|
335
|
+
ok: false,
|
|
336
|
+
unreadable: false,
|
|
337
|
+
error: null,
|
|
338
|
+
size: stat.size,
|
|
339
|
+
mtime: stat.mtime.toISOString(),
|
|
340
|
+
memoryCount: null
|
|
341
|
+
};
|
|
342
|
+
const client = createClient({
|
|
343
|
+
url: `file:${dbPath}`,
|
|
344
|
+
encryptionKey: _encryptionKey
|
|
345
|
+
});
|
|
346
|
+
try {
|
|
347
|
+
await client.execute("SELECT COUNT(*) as cnt FROM sqlite_schema");
|
|
348
|
+
const hasMemories = await client.execute(
|
|
349
|
+
"SELECT COUNT(*) as cnt FROM sqlite_schema WHERE type = 'table' AND name = 'memories'"
|
|
350
|
+
);
|
|
351
|
+
if (Number(hasMemories.rows[0]?.cnt ?? 0) > 0) {
|
|
352
|
+
const mem = await client.execute("SELECT COUNT(*) as cnt FROM memories");
|
|
353
|
+
item.memoryCount = Number(mem.rows[0]?.cnt ?? 0);
|
|
354
|
+
}
|
|
355
|
+
item.ok = true;
|
|
356
|
+
} catch (err) {
|
|
357
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
358
|
+
item.error = message;
|
|
359
|
+
item.unreadable = /SQLITE_NOTADB|file is not a database/i.test(message);
|
|
360
|
+
if (item.unreadable && repair && !dryRun) {
|
|
361
|
+
client.close();
|
|
362
|
+
_shards.delete(name);
|
|
363
|
+
_shardLastAccess.delete(name);
|
|
364
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
365
|
+
const archivedPath = path2.join(SHARDS_DIR, `${name}.db.broken-${stamp}`);
|
|
366
|
+
renameSync2(dbPath, archivedPath);
|
|
367
|
+
item.archivedPath = archivedPath;
|
|
368
|
+
}
|
|
369
|
+
} finally {
|
|
370
|
+
try {
|
|
371
|
+
client.close();
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
shards.push(item);
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
total: shards.length,
|
|
379
|
+
ok: shards.filter((s) => s.ok).length,
|
|
380
|
+
unreadable: shards.filter((s) => s.unreadable).length,
|
|
381
|
+
archived: shards.filter((s) => Boolean(s.archivedPath)).length,
|
|
382
|
+
shards
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
async function ensureShardSchema(client) {
|
|
386
|
+
await client.execute("PRAGMA journal_mode = WAL");
|
|
387
|
+
await client.execute("PRAGMA busy_timeout = 30000");
|
|
388
|
+
try {
|
|
389
|
+
await client.execute("PRAGMA libsql_vector_search_ef = 128");
|
|
390
|
+
} catch {
|
|
391
|
+
}
|
|
392
|
+
await client.executeMultiple(`
|
|
393
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
394
|
+
id TEXT PRIMARY KEY,
|
|
395
|
+
agent_id TEXT NOT NULL,
|
|
396
|
+
agent_role TEXT NOT NULL,
|
|
397
|
+
session_id TEXT NOT NULL,
|
|
398
|
+
timestamp TEXT NOT NULL,
|
|
399
|
+
tool_name TEXT NOT NULL,
|
|
400
|
+
project_name TEXT NOT NULL,
|
|
401
|
+
has_error INTEGER NOT NULL DEFAULT 0,
|
|
402
|
+
raw_text TEXT NOT NULL,
|
|
403
|
+
vector F32_BLOB(1024),
|
|
404
|
+
version INTEGER NOT NULL DEFAULT 0
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent_id);
|
|
408
|
+
CREATE INDEX IF NOT EXISTS idx_memories_timestamp ON memories(timestamp);
|
|
409
|
+
CREATE INDEX IF NOT EXISTS idx_memories_agent_project ON memories(agent_id, project_name);
|
|
410
|
+
`);
|
|
411
|
+
await client.executeMultiple(`
|
|
412
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
413
|
+
raw_text,
|
|
414
|
+
content='memories',
|
|
415
|
+
content_rowid='rowid'
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
CREATE TRIGGER IF NOT EXISTS memories_fts_ai AFTER INSERT ON memories BEGIN
|
|
419
|
+
INSERT INTO memories_fts(rowid, raw_text) VALUES (new.rowid, new.raw_text);
|
|
420
|
+
END;
|
|
421
|
+
|
|
422
|
+
CREATE TRIGGER IF NOT EXISTS memories_fts_ad AFTER DELETE ON memories BEGIN
|
|
423
|
+
INSERT INTO memories_fts(memories_fts, rowid, raw_text) VALUES('delete', old.rowid, old.raw_text);
|
|
424
|
+
END;
|
|
425
|
+
|
|
426
|
+
CREATE TRIGGER IF NOT EXISTS memories_fts_au AFTER UPDATE ON memories BEGIN
|
|
427
|
+
INSERT INTO memories_fts(memories_fts, rowid, raw_text) VALUES('delete', old.rowid, old.raw_text);
|
|
428
|
+
INSERT INTO memories_fts(rowid, raw_text) VALUES (new.rowid, new.raw_text);
|
|
429
|
+
END;
|
|
430
|
+
`);
|
|
431
|
+
for (const col of [
|
|
432
|
+
"ALTER TABLE memories ADD COLUMN task_id TEXT",
|
|
433
|
+
"ALTER TABLE memories ADD COLUMN consolidated INTEGER NOT NULL DEFAULT 0",
|
|
434
|
+
"ALTER TABLE memories ADD COLUMN author_device_id TEXT",
|
|
435
|
+
"ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'business'",
|
|
436
|
+
"ALTER TABLE memories ADD COLUMN importance INTEGER DEFAULT 5",
|
|
437
|
+
"ALTER TABLE memories ADD COLUMN status TEXT DEFAULT 'active'",
|
|
438
|
+
"ALTER TABLE memories ADD COLUMN wiki_synced INTEGER DEFAULT 0",
|
|
439
|
+
"ALTER TABLE memories ADD COLUMN graph_extracted INTEGER DEFAULT 0",
|
|
440
|
+
"ALTER TABLE memories ADD COLUMN content_hash TEXT",
|
|
441
|
+
"ALTER TABLE memories ADD COLUMN graph_extracted_hash TEXT",
|
|
442
|
+
"ALTER TABLE memories ADD COLUMN confidence REAL DEFAULT 0.7",
|
|
443
|
+
"ALTER TABLE memories ADD COLUMN last_accessed TEXT",
|
|
444
|
+
// Wiki linkage columns (must match database.ts)
|
|
445
|
+
"ALTER TABLE memories ADD COLUMN workspace_id TEXT",
|
|
446
|
+
"ALTER TABLE memories ADD COLUMN document_id TEXT",
|
|
447
|
+
"ALTER TABLE memories ADD COLUMN user_id TEXT",
|
|
448
|
+
"ALTER TABLE memories ADD COLUMN char_offset INTEGER",
|
|
449
|
+
"ALTER TABLE memories ADD COLUMN page_number INTEGER",
|
|
450
|
+
// Source provenance columns (must match database.ts)
|
|
451
|
+
"ALTER TABLE memories ADD COLUMN source_path TEXT",
|
|
452
|
+
"ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'",
|
|
453
|
+
"ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3",
|
|
454
|
+
"ALTER TABLE memories ADD COLUMN supersedes_id TEXT",
|
|
455
|
+
// MS-11: draft staging, MS-6a: memory_type, MS-7: trajectory
|
|
456
|
+
"ALTER TABLE memories ADD COLUMN draft INTEGER DEFAULT 0",
|
|
457
|
+
"ALTER TABLE memories ADD COLUMN memory_type TEXT DEFAULT 'raw'",
|
|
458
|
+
"ALTER TABLE memories ADD COLUMN trajectory TEXT",
|
|
459
|
+
// Metadata enrichment columns (must match database.ts)
|
|
460
|
+
"ALTER TABLE memories ADD COLUMN intent TEXT",
|
|
461
|
+
"ALTER TABLE memories ADD COLUMN outcome TEXT",
|
|
462
|
+
"ALTER TABLE memories ADD COLUMN domain TEXT",
|
|
463
|
+
"ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
|
|
464
|
+
"ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
|
|
465
|
+
"ALTER TABLE memories ADD COLUMN chain_position TEXT",
|
|
466
|
+
"ALTER TABLE memories ADD COLUMN review_status TEXT",
|
|
467
|
+
"ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
|
|
468
|
+
"ALTER TABLE memories ADD COLUMN file_paths TEXT",
|
|
469
|
+
"ALTER TABLE memories ADD COLUMN commit_hash TEXT",
|
|
470
|
+
"ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
|
|
471
|
+
"ALTER TABLE memories ADD COLUMN token_cost REAL",
|
|
472
|
+
"ALTER TABLE memories ADD COLUMN audience TEXT",
|
|
473
|
+
"ALTER TABLE memories ADD COLUMN language_type TEXT",
|
|
474
|
+
"ALTER TABLE memories ADD COLUMN parent_memory_id TEXT",
|
|
475
|
+
"ALTER TABLE memories ADD COLUMN deleted_at TEXT"
|
|
476
|
+
]) {
|
|
477
|
+
try {
|
|
478
|
+
await client.execute(col);
|
|
479
|
+
} catch {
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
for (const idx of [
|
|
483
|
+
"CREATE INDEX IF NOT EXISTS idx_memories_tier ON memories(tier)",
|
|
484
|
+
"CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL",
|
|
485
|
+
"CREATE INDEX IF NOT EXISTS idx_memories_scoped_content_hash ON memories(content_hash, agent_id, project_name, memory_type) WHERE content_hash IS NOT NULL"
|
|
486
|
+
]) {
|
|
487
|
+
try {
|
|
488
|
+
await client.execute(idx);
|
|
489
|
+
} catch {
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
await client.execute("CREATE INDEX IF NOT EXISTS idx_memories_status ON memories(status)");
|
|
494
|
+
} catch {
|
|
495
|
+
}
|
|
496
|
+
for (const idx of [
|
|
497
|
+
"CREATE INDEX IF NOT EXISTS idx_memories_workspace ON memories(workspace_id)",
|
|
498
|
+
"CREATE INDEX IF NOT EXISTS idx_memories_document ON memories(document_id)",
|
|
499
|
+
"CREATE INDEX IF NOT EXISTS idx_memories_user ON memories(user_id)"
|
|
500
|
+
]) {
|
|
501
|
+
try {
|
|
502
|
+
await client.execute(idx);
|
|
503
|
+
} catch {
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
await client.executeMultiple(`
|
|
507
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
508
|
+
id TEXT PRIMARY KEY,
|
|
509
|
+
name TEXT NOT NULL,
|
|
510
|
+
type TEXT NOT NULL,
|
|
511
|
+
first_seen TEXT NOT NULL,
|
|
512
|
+
last_seen TEXT NOT NULL,
|
|
513
|
+
properties TEXT DEFAULT '{}',
|
|
514
|
+
UNIQUE(name, type)
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
CREATE TABLE IF NOT EXISTS relationships (
|
|
518
|
+
id TEXT PRIMARY KEY,
|
|
519
|
+
source_entity_id TEXT NOT NULL,
|
|
520
|
+
target_entity_id TEXT NOT NULL,
|
|
521
|
+
type TEXT NOT NULL,
|
|
522
|
+
weight REAL DEFAULT 1.0,
|
|
523
|
+
timestamp TEXT NOT NULL,
|
|
524
|
+
properties TEXT DEFAULT '{}',
|
|
525
|
+
UNIQUE(source_entity_id, target_entity_id, type)
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
CREATE TABLE IF NOT EXISTS entity_memories (
|
|
529
|
+
entity_id TEXT NOT NULL,
|
|
530
|
+
memory_id TEXT NOT NULL,
|
|
531
|
+
PRIMARY KEY (entity_id, memory_id)
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
CREATE TABLE IF NOT EXISTS relationship_memories (
|
|
535
|
+
relationship_id TEXT NOT NULL,
|
|
536
|
+
memory_id TEXT NOT NULL,
|
|
537
|
+
PRIMARY KEY (relationship_id, memory_id)
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
|
|
541
|
+
CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
|
|
542
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_source ON relationships(source_entity_id);
|
|
543
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_target ON relationships(target_entity_id);
|
|
544
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_type ON relationships(type);
|
|
545
|
+
|
|
546
|
+
CREATE TABLE IF NOT EXISTS hyperedges (
|
|
547
|
+
id TEXT PRIMARY KEY,
|
|
548
|
+
label TEXT NOT NULL,
|
|
549
|
+
relation TEXT NOT NULL,
|
|
550
|
+
confidence REAL DEFAULT 1.0,
|
|
551
|
+
timestamp TEXT NOT NULL
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
CREATE TABLE IF NOT EXISTS hyperedge_nodes (
|
|
555
|
+
hyperedge_id TEXT NOT NULL,
|
|
556
|
+
entity_id TEXT NOT NULL,
|
|
557
|
+
PRIMARY KEY (hyperedge_id, entity_id)
|
|
558
|
+
);
|
|
559
|
+
`);
|
|
560
|
+
for (const col of [
|
|
561
|
+
"ALTER TABLE relationships ADD COLUMN confidence REAL DEFAULT 1.0",
|
|
562
|
+
"ALTER TABLE relationships ADD COLUMN confidence_label TEXT DEFAULT 'extracted'"
|
|
563
|
+
]) {
|
|
564
|
+
try {
|
|
565
|
+
await client.execute(col);
|
|
566
|
+
} catch {
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
async function getReadyShardClient(projectName) {
|
|
571
|
+
const safeName = safeShardName(projectName);
|
|
572
|
+
let client = getShardClient(projectName);
|
|
573
|
+
try {
|
|
574
|
+
await ensureShardSchema(client);
|
|
575
|
+
return client;
|
|
576
|
+
} catch (err) {
|
|
577
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
578
|
+
if (!/SQLITE_NOTADB|file is not a database/i.test(message)) throw err;
|
|
579
|
+
client.close();
|
|
580
|
+
_shards.delete(safeName);
|
|
581
|
+
_shardLastAccess.delete(safeName);
|
|
582
|
+
const dbPath = path2.join(SHARDS_DIR, `${safeName}.db`);
|
|
583
|
+
if (existsSync3(dbPath)) {
|
|
584
|
+
const stat = statSync(dbPath);
|
|
585
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
586
|
+
const archivedPath = path2.join(SHARDS_DIR, `${safeName}.db.broken-${stamp}`);
|
|
587
|
+
renameSync2(dbPath, archivedPath);
|
|
588
|
+
process.stderr.write(
|
|
589
|
+
`[shard-manager] Archived unreadable shard ${safeName}: ${archivedPath} (${stat.size} bytes, mtime ${stat.mtime.toISOString()})
|
|
590
|
+
`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
client = getShardClient(projectName);
|
|
594
|
+
await ensureShardSchema(client);
|
|
595
|
+
return client;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function evictLRU() {
|
|
599
|
+
let oldest = null;
|
|
600
|
+
let oldestTime = Infinity;
|
|
601
|
+
for (const [name, time] of _shardLastAccess) {
|
|
602
|
+
if (time < oldestTime) {
|
|
603
|
+
oldestTime = time;
|
|
604
|
+
oldest = name;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (oldest) {
|
|
608
|
+
const client = _shards.get(oldest);
|
|
609
|
+
if (client) {
|
|
610
|
+
client.close();
|
|
611
|
+
}
|
|
612
|
+
_shards.delete(oldest);
|
|
613
|
+
_shardLastAccess.delete(oldest);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function evictIdleShards() {
|
|
617
|
+
const now = Date.now();
|
|
618
|
+
const toEvict = [];
|
|
619
|
+
for (const [name, lastAccess] of _shardLastAccess) {
|
|
620
|
+
if (now - lastAccess > SHARD_IDLE_MS) {
|
|
621
|
+
toEvict.push(name);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
for (const name of toEvict) {
|
|
625
|
+
const client = _shards.get(name);
|
|
626
|
+
if (client) {
|
|
627
|
+
client.close();
|
|
628
|
+
}
|
|
629
|
+
_shards.delete(name);
|
|
630
|
+
_shardLastAccess.delete(name);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function getOpenShardCount() {
|
|
634
|
+
return _shards.size;
|
|
635
|
+
}
|
|
636
|
+
function disposeShards() {
|
|
637
|
+
if (_evictionTimer) {
|
|
638
|
+
clearInterval(_evictionTimer);
|
|
639
|
+
_evictionTimer = null;
|
|
640
|
+
}
|
|
641
|
+
for (const [, client] of _shards) {
|
|
642
|
+
client.close();
|
|
643
|
+
}
|
|
644
|
+
_shards.clear();
|
|
645
|
+
_shardLastAccess.clear();
|
|
646
|
+
_shardingEnabled = false;
|
|
647
|
+
_encryptionKey = null;
|
|
648
|
+
}
|
|
649
|
+
var SHARDS_DIR, SHARD_IDLE_MS, MAX_OPEN_SHARDS, EVICTION_INTERVAL_MS, _shards, _shardLastAccess, _evictionTimer, _encryptionKey, _shardingEnabled;
|
|
650
|
+
var init_shard_manager = __esm({
|
|
651
|
+
"src/lib/shard-manager.ts"() {
|
|
652
|
+
"use strict";
|
|
653
|
+
init_config();
|
|
654
|
+
SHARDS_DIR = path2.join(EXE_AI_DIR, "shards");
|
|
655
|
+
SHARD_IDLE_MS = 5 * 60 * 1e3;
|
|
656
|
+
MAX_OPEN_SHARDS = 10;
|
|
657
|
+
EVICTION_INTERVAL_MS = 60 * 1e3;
|
|
658
|
+
_shards = /* @__PURE__ */ new Map();
|
|
659
|
+
_shardLastAccess = /* @__PURE__ */ new Map();
|
|
660
|
+
_evictionTimer = null;
|
|
661
|
+
_encryptionKey = null;
|
|
662
|
+
_shardingEnabled = false;
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// src/lib/keychain.ts
|
|
667
|
+
var keychain_exports = {};
|
|
668
|
+
__export(keychain_exports, {
|
|
669
|
+
deleteMasterKey: () => deleteMasterKey,
|
|
670
|
+
exportMnemonic: () => exportMnemonic,
|
|
671
|
+
getMasterKey: () => getMasterKey,
|
|
672
|
+
importMnemonic: () => importMnemonic,
|
|
673
|
+
setMasterKey: () => setMasterKey
|
|
674
|
+
});
|
|
675
|
+
import { readFile as readFile2, writeFile as writeFile2, unlink, mkdir as mkdir2, chmod as chmod2 } from "fs/promises";
|
|
676
|
+
import { existsSync as existsSync4 } from "fs";
|
|
677
|
+
import { execSync } from "child_process";
|
|
678
|
+
import path3 from "path";
|
|
679
|
+
import os2 from "os";
|
|
680
|
+
function getKeyDir() {
|
|
681
|
+
return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path3.join(os2.homedir(), ".exe-os");
|
|
682
|
+
}
|
|
683
|
+
function getKeyPath() {
|
|
684
|
+
return path3.join(getKeyDir(), "master.key");
|
|
685
|
+
}
|
|
686
|
+
function macKeychainGet() {
|
|
687
|
+
if (process.platform !== "darwin") return null;
|
|
688
|
+
try {
|
|
689
|
+
return execSync(
|
|
690
|
+
`security find-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w 2>/dev/null`,
|
|
691
|
+
{ encoding: "utf-8", timeout: 5e3 }
|
|
692
|
+
).trim();
|
|
693
|
+
} catch {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
function macKeychainSet(value) {
|
|
698
|
+
if (process.platform !== "darwin") return false;
|
|
699
|
+
try {
|
|
700
|
+
try {
|
|
701
|
+
execSync(
|
|
702
|
+
`security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`,
|
|
703
|
+
{ timeout: 5e3 }
|
|
704
|
+
);
|
|
705
|
+
} catch {
|
|
706
|
+
}
|
|
707
|
+
execSync(
|
|
708
|
+
`security add-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w "${value}"`,
|
|
709
|
+
{ timeout: 5e3 }
|
|
710
|
+
);
|
|
711
|
+
return true;
|
|
712
|
+
} catch {
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
function macKeychainDelete() {
|
|
717
|
+
if (process.platform !== "darwin") return false;
|
|
718
|
+
try {
|
|
719
|
+
execSync(
|
|
720
|
+
`security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`,
|
|
721
|
+
{ timeout: 5e3 }
|
|
722
|
+
);
|
|
723
|
+
return true;
|
|
724
|
+
} catch {
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function linuxSecretGet() {
|
|
729
|
+
if (process.platform !== "linux") return null;
|
|
730
|
+
try {
|
|
731
|
+
return execSync(
|
|
732
|
+
`secret-tool lookup service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
|
|
733
|
+
{ encoding: "utf-8", timeout: 5e3 }
|
|
734
|
+
).trim();
|
|
735
|
+
} catch {
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function linuxSecretSet(value) {
|
|
740
|
+
if (process.platform !== "linux") return false;
|
|
741
|
+
try {
|
|
742
|
+
execSync(
|
|
743
|
+
`echo -n "${value}" | secret-tool store --label="exe-os master key" service "${SERVICE}" account "${ACCOUNT}"`,
|
|
744
|
+
{ timeout: 5e3 }
|
|
745
|
+
);
|
|
746
|
+
return true;
|
|
747
|
+
} catch {
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function linuxSecretDelete() {
|
|
752
|
+
if (process.platform !== "linux") return false;
|
|
753
|
+
try {
|
|
754
|
+
execSync(
|
|
755
|
+
`secret-tool clear service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
|
|
756
|
+
{ timeout: 5e3 }
|
|
757
|
+
);
|
|
758
|
+
return true;
|
|
759
|
+
} catch {
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async function tryKeytar() {
|
|
764
|
+
try {
|
|
765
|
+
return await import("keytar");
|
|
766
|
+
} catch {
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function deriveMachineKey() {
|
|
771
|
+
try {
|
|
772
|
+
const crypto2 = __require("crypto");
|
|
773
|
+
const material = [
|
|
774
|
+
os2.hostname(),
|
|
775
|
+
os2.userInfo().username,
|
|
776
|
+
os2.arch(),
|
|
777
|
+
os2.platform(),
|
|
778
|
+
// Machine ID on Linux (stable across reboots)
|
|
779
|
+
process.platform === "linux" ? readMachineId() : ""
|
|
780
|
+
].join("|");
|
|
781
|
+
return crypto2.createHash("sha256").update(material).digest();
|
|
782
|
+
} catch {
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function readMachineId() {
|
|
787
|
+
try {
|
|
788
|
+
const { readFileSync: readFileSync7 } = __require("fs");
|
|
789
|
+
return readFileSync7("/etc/machine-id", "utf-8").trim();
|
|
790
|
+
} catch {
|
|
791
|
+
return "";
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function encryptWithMachineKey(plaintext, machineKey) {
|
|
795
|
+
const crypto2 = __require("crypto");
|
|
796
|
+
const iv = crypto2.randomBytes(12);
|
|
797
|
+
const cipher = crypto2.createCipheriv("aes-256-gcm", machineKey, iv);
|
|
798
|
+
let encrypted = cipher.update(plaintext, "utf-8", "base64");
|
|
799
|
+
encrypted += cipher.final("base64");
|
|
800
|
+
const authTag = cipher.getAuthTag().toString("base64");
|
|
801
|
+
return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${authTag}:${encrypted}`;
|
|
802
|
+
}
|
|
803
|
+
function decryptWithMachineKey(encrypted, machineKey) {
|
|
804
|
+
if (!encrypted.startsWith(ENCRYPTED_PREFIX)) return null;
|
|
805
|
+
try {
|
|
806
|
+
const crypto2 = __require("crypto");
|
|
807
|
+
const parts = encrypted.slice(ENCRYPTED_PREFIX.length).split(":");
|
|
808
|
+
if (parts.length !== 3) return null;
|
|
809
|
+
const [ivB64, tagB64, cipherB64] = parts;
|
|
810
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
811
|
+
const authTag = Buffer.from(tagB64, "base64");
|
|
812
|
+
const decipher = crypto2.createDecipheriv("aes-256-gcm", machineKey, iv);
|
|
813
|
+
decipher.setAuthTag(authTag);
|
|
814
|
+
let decrypted = decipher.update(cipherB64, "base64", "utf-8");
|
|
815
|
+
decrypted += decipher.final("utf-8");
|
|
816
|
+
return decrypted;
|
|
817
|
+
} catch {
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
async function writeMachineBoundFileFallback(b64) {
|
|
822
|
+
const dir = getKeyDir();
|
|
823
|
+
await mkdir2(dir, { recursive: true });
|
|
824
|
+
const keyPath = getKeyPath();
|
|
825
|
+
const machineKey = deriveMachineKey();
|
|
826
|
+
if (machineKey) {
|
|
827
|
+
const encrypted = encryptWithMachineKey(b64, machineKey);
|
|
828
|
+
await writeFile2(keyPath, encrypted + "\n", "utf-8");
|
|
829
|
+
await chmod2(keyPath, 384);
|
|
830
|
+
return "encrypted";
|
|
831
|
+
}
|
|
832
|
+
await writeFile2(keyPath, b64 + "\n", "utf-8");
|
|
833
|
+
await chmod2(keyPath, 384);
|
|
834
|
+
return "plaintext";
|
|
835
|
+
}
|
|
836
|
+
async function getMasterKey() {
|
|
837
|
+
const nativeValue = macKeychainGet() ?? linuxSecretGet();
|
|
838
|
+
if (nativeValue) {
|
|
839
|
+
return Buffer.from(nativeValue, "base64");
|
|
840
|
+
}
|
|
841
|
+
const keytar = await tryKeytar();
|
|
842
|
+
if (keytar) {
|
|
843
|
+
try {
|
|
844
|
+
const keytarValue = await keytar.getPassword(SERVICE, ACCOUNT);
|
|
845
|
+
if (keytarValue) {
|
|
846
|
+
const migrated = macKeychainSet(keytarValue) || linuxSecretSet(keytarValue);
|
|
847
|
+
if (migrated) {
|
|
848
|
+
process.stderr.write("[keychain] Migrated key from keytar to native keychain.\n");
|
|
849
|
+
}
|
|
850
|
+
return Buffer.from(keytarValue, "base64");
|
|
851
|
+
}
|
|
852
|
+
} catch {
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
const keyPath = getKeyPath();
|
|
856
|
+
if (!existsSync4(keyPath)) {
|
|
857
|
+
process.stderr.write(
|
|
858
|
+
`[keychain] Key not found at ${keyPath} (HOME=${os2.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
|
|
859
|
+
`
|
|
860
|
+
);
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
try {
|
|
864
|
+
const content = (await readFile2(keyPath, "utf-8")).trim();
|
|
865
|
+
let b64Value;
|
|
866
|
+
if (content.startsWith(ENCRYPTED_PREFIX)) {
|
|
867
|
+
const machineKey = deriveMachineKey();
|
|
868
|
+
if (!machineKey) {
|
|
869
|
+
process.stderr.write("[keychain] Cannot derive machine key to decrypt stored key.\n");
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
const decrypted = decryptWithMachineKey(content, machineKey);
|
|
873
|
+
if (!decrypted) {
|
|
874
|
+
process.stderr.write(
|
|
875
|
+
"[keychain] Key decryption failed \u2014 machine may have changed.\n Use your 24-word recovery phrase: exe-os link import\n"
|
|
876
|
+
);
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
b64Value = decrypted;
|
|
880
|
+
} else {
|
|
881
|
+
b64Value = content;
|
|
882
|
+
}
|
|
883
|
+
const key = Buffer.from(b64Value, "base64");
|
|
884
|
+
const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
|
|
885
|
+
if (migrated) {
|
|
886
|
+
process.stderr.write("[keychain] Migrated key from file to native keychain.\n");
|
|
887
|
+
try {
|
|
888
|
+
await unlink(keyPath);
|
|
889
|
+
process.stderr.write("[keychain] Removed legacy master.key file after native keychain migration.\n");
|
|
890
|
+
} catch {
|
|
891
|
+
}
|
|
892
|
+
} else if (!content.startsWith(ENCRYPTED_PREFIX)) {
|
|
893
|
+
const fallback = await writeMachineBoundFileFallback(b64Value);
|
|
894
|
+
if (fallback === "encrypted") {
|
|
895
|
+
process.stderr.write("[keychain] Upgraded legacy plaintext master.key to machine-bound encrypted fallback.\n");
|
|
896
|
+
} else {
|
|
897
|
+
process.stderr.write(
|
|
898
|
+
"[keychain] WARNING: Could not encrypt legacy master.key \u2014 plaintext fallback remains.\n"
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return key;
|
|
903
|
+
} catch (err) {
|
|
904
|
+
process.stderr.write(
|
|
905
|
+
`[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
|
|
906
|
+
`
|
|
907
|
+
);
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
async function setMasterKey(key) {
|
|
912
|
+
const b64 = key.toString("base64");
|
|
913
|
+
if (macKeychainSet(b64) || linuxSecretSet(b64)) {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
const keytar = await tryKeytar();
|
|
917
|
+
if (keytar) {
|
|
918
|
+
try {
|
|
919
|
+
await keytar.setPassword(SERVICE, ACCOUNT, b64);
|
|
920
|
+
return;
|
|
921
|
+
} catch {
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
const fallback = await writeMachineBoundFileFallback(b64);
|
|
925
|
+
if (fallback === "encrypted") {
|
|
926
|
+
process.stderr.write("[keychain] Key stored encrypted (machine-bound).\n");
|
|
927
|
+
} else {
|
|
928
|
+
process.stderr.write(
|
|
929
|
+
"[keychain] WARNING: Key stored in plaintext file \u2014 no OS keychain available.\n"
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
async function deleteMasterKey() {
|
|
934
|
+
macKeychainDelete();
|
|
935
|
+
linuxSecretDelete();
|
|
936
|
+
const keytar = await tryKeytar();
|
|
937
|
+
if (keytar) {
|
|
938
|
+
try {
|
|
939
|
+
await keytar.deletePassword(SERVICE, ACCOUNT);
|
|
940
|
+
} catch {
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const keyPath = getKeyPath();
|
|
944
|
+
if (existsSync4(keyPath)) {
|
|
945
|
+
await unlink(keyPath);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
async function loadBip39() {
|
|
949
|
+
try {
|
|
950
|
+
return await import("bip39");
|
|
951
|
+
} catch {
|
|
952
|
+
throw new Error(
|
|
953
|
+
"bip39 package not found. Run: npm install -g bip39\nOr reinstall exe-os: npm install -g @askexenow/exe-os"
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
async function exportMnemonic(key) {
|
|
958
|
+
if (key.length !== 32) {
|
|
959
|
+
throw new Error(`Key must be 32 bytes, got ${key.length}`);
|
|
960
|
+
}
|
|
961
|
+
const { entropyToMnemonic } = await loadBip39();
|
|
962
|
+
return entropyToMnemonic(key.toString("hex"));
|
|
963
|
+
}
|
|
964
|
+
async function importMnemonic(mnemonic) {
|
|
965
|
+
const trimmed = mnemonic.trim();
|
|
966
|
+
const words = trimmed.split(/\s+/);
|
|
967
|
+
if (words.length !== 24) {
|
|
968
|
+
throw new Error(`Expected 24 words, got ${words.length}`);
|
|
969
|
+
}
|
|
970
|
+
const { validateMnemonic, mnemonicToEntropy } = await loadBip39();
|
|
971
|
+
if (!validateMnemonic(trimmed)) {
|
|
972
|
+
throw new Error("Invalid mnemonic \u2014 check for typos or missing words");
|
|
973
|
+
}
|
|
974
|
+
const entropy = mnemonicToEntropy(trimmed);
|
|
975
|
+
return Buffer.from(entropy, "hex");
|
|
976
|
+
}
|
|
977
|
+
var SERVICE, ACCOUNT, ENCRYPTED_PREFIX;
|
|
978
|
+
var init_keychain = __esm({
|
|
979
|
+
"src/lib/keychain.ts"() {
|
|
980
|
+
"use strict";
|
|
981
|
+
SERVICE = "exe-mem";
|
|
982
|
+
ACCOUNT = "master-key";
|
|
983
|
+
ENCRYPTED_PREFIX = "enc:";
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
// src/lib/key-backup-status.ts
|
|
988
|
+
var key_backup_status_exports = {};
|
|
989
|
+
__export(key_backup_status_exports, {
|
|
990
|
+
getKeyBackupStatus: () => getKeyBackupStatus,
|
|
991
|
+
keyBackupMarkerPath: () => keyBackupMarkerPath,
|
|
992
|
+
markKeyBackupConfirmed: () => markKeyBackupConfirmed
|
|
993
|
+
});
|
|
994
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
995
|
+
import path4 from "path";
|
|
996
|
+
function keyBackupMarkerPath() {
|
|
997
|
+
return path4.join(EXE_AI_DIR, "key-backup-confirmed.json");
|
|
998
|
+
}
|
|
999
|
+
function getKeyBackupStatus() {
|
|
1000
|
+
const marker = keyBackupMarkerPath();
|
|
1001
|
+
if (!existsSync5(marker)) return { exists: false };
|
|
1002
|
+
try {
|
|
1003
|
+
const parsed = JSON.parse(readFileSync2(marker, "utf8"));
|
|
1004
|
+
return {
|
|
1005
|
+
exists: true,
|
|
1006
|
+
confirmedAt: parsed.confirmedAt,
|
|
1007
|
+
source: parsed.source
|
|
1008
|
+
};
|
|
1009
|
+
} catch {
|
|
1010
|
+
return { exists: true };
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
function markKeyBackupConfirmed(source) {
|
|
1014
|
+
mkdirSync3(EXE_AI_DIR, { recursive: true, mode: 448 });
|
|
1015
|
+
writeFileSync(
|
|
1016
|
+
keyBackupMarkerPath(),
|
|
1017
|
+
JSON.stringify({ confirmedAt: (/* @__PURE__ */ new Date()).toISOString(), source }, null, 2) + "\n",
|
|
1018
|
+
{ mode: 384 }
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
var init_key_backup_status = __esm({
|
|
1022
|
+
"src/lib/key-backup-status.ts"() {
|
|
1023
|
+
"use strict";
|
|
1024
|
+
init_config();
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
// src/lib/worker-gate.ts
|
|
1029
|
+
var worker_gate_exports = {};
|
|
1030
|
+
__export(worker_gate_exports, {
|
|
1031
|
+
MAX_CONCURRENT_WORKERS: () => MAX_CONCURRENT_WORKERS,
|
|
1032
|
+
cleanupWorkerPid: () => cleanupWorkerPid,
|
|
1033
|
+
registerWorkerPid: () => registerWorkerPid,
|
|
1034
|
+
releaseBackfillLock: () => releaseBackfillLock,
|
|
1035
|
+
tryAcquireBackfillLock: () => tryAcquireBackfillLock,
|
|
1036
|
+
tryAcquireWorkerSlot: () => tryAcquireWorkerSlot
|
|
1037
|
+
});
|
|
1038
|
+
import { readdirSync as readdirSync2, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync4, existsSync as existsSync6 } from "fs";
|
|
1039
|
+
import path5 from "path";
|
|
1040
|
+
function tryAcquireWorkerSlot() {
|
|
1041
|
+
try {
|
|
1042
|
+
mkdirSync4(WORKER_PID_DIR, { recursive: true });
|
|
1043
|
+
const reservationId = `res-${process.pid}-${Date.now()}`;
|
|
1044
|
+
const reservationPath = path5.join(WORKER_PID_DIR, `${reservationId}.pid`);
|
|
1045
|
+
writeFileSync2(reservationPath, String(process.pid));
|
|
1046
|
+
const files = readdirSync2(WORKER_PID_DIR);
|
|
1047
|
+
let alive = 0;
|
|
1048
|
+
for (const f of files) {
|
|
1049
|
+
if (!f.endsWith(".pid")) continue;
|
|
1050
|
+
if (f.startsWith("res-")) {
|
|
1051
|
+
alive++;
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
const dashIdx = f.lastIndexOf("-");
|
|
1055
|
+
const pid = parseInt(f.slice(dashIdx + 1).replace(".pid", ""), 10);
|
|
1056
|
+
if (isNaN(pid)) continue;
|
|
1057
|
+
try {
|
|
1058
|
+
process.kill(pid, 0);
|
|
1059
|
+
alive++;
|
|
1060
|
+
} catch {
|
|
1061
|
+
try {
|
|
1062
|
+
unlinkSync(path5.join(WORKER_PID_DIR, f));
|
|
1063
|
+
} catch {
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (alive > MAX_CONCURRENT_WORKERS) {
|
|
1068
|
+
try {
|
|
1069
|
+
unlinkSync(reservationPath);
|
|
1070
|
+
} catch {
|
|
1071
|
+
}
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
try {
|
|
1075
|
+
unlinkSync(reservationPath);
|
|
1076
|
+
} catch {
|
|
1077
|
+
}
|
|
1078
|
+
return true;
|
|
1079
|
+
} catch {
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
function registerWorkerPid(pid) {
|
|
1084
|
+
try {
|
|
1085
|
+
mkdirSync4(WORKER_PID_DIR, { recursive: true });
|
|
1086
|
+
writeFileSync2(path5.join(WORKER_PID_DIR, `worker-${pid}.pid`), String(pid));
|
|
1087
|
+
} catch {
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
function cleanupWorkerPid() {
|
|
1091
|
+
try {
|
|
1092
|
+
unlinkSync(path5.join(WORKER_PID_DIR, `worker-${process.pid}.pid`));
|
|
1093
|
+
} catch {
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
function tryAcquireBackfillLock() {
|
|
1097
|
+
try {
|
|
1098
|
+
mkdirSync4(WORKER_PID_DIR, { recursive: true });
|
|
1099
|
+
if (existsSync6(BACKFILL_LOCK)) {
|
|
1100
|
+
try {
|
|
1101
|
+
const pid = parseInt(
|
|
1102
|
+
__require("fs").readFileSync(BACKFILL_LOCK, "utf8").trim(),
|
|
1103
|
+
10
|
|
1104
|
+
);
|
|
1105
|
+
if (!isNaN(pid) && pid > 0) {
|
|
1106
|
+
try {
|
|
1107
|
+
process.kill(pid, 0);
|
|
1108
|
+
return false;
|
|
1109
|
+
} catch {
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
} catch {
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
writeFileSync2(BACKFILL_LOCK, String(process.pid));
|
|
1116
|
+
return true;
|
|
1117
|
+
} catch {
|
|
1118
|
+
return true;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
function releaseBackfillLock() {
|
|
1122
|
+
try {
|
|
1123
|
+
unlinkSync(BACKFILL_LOCK);
|
|
1124
|
+
} catch {
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
var WORKER_PID_DIR, MAX_CONCURRENT_WORKERS, BACKFILL_LOCK;
|
|
1128
|
+
var init_worker_gate = __esm({
|
|
1129
|
+
"src/lib/worker-gate.ts"() {
|
|
1130
|
+
"use strict";
|
|
1131
|
+
init_config();
|
|
1132
|
+
WORKER_PID_DIR = path5.join(EXE_AI_DIR, "worker-pids");
|
|
1133
|
+
MAX_CONCURRENT_WORKERS = 3;
|
|
1134
|
+
BACKFILL_LOCK = path5.join(WORKER_PID_DIR, "backfill.lock");
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
// src/lib/db-retry.ts
|
|
1139
|
+
function isBusyError(err) {
|
|
1140
|
+
if (err instanceof Error) {
|
|
1141
|
+
const msg = err.message.toLowerCase();
|
|
1142
|
+
return msg.includes("sqlite_busy") || msg.includes("database is locked");
|
|
1143
|
+
}
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
function delay(ms) {
|
|
1147
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1148
|
+
}
|
|
1149
|
+
async function retryOnBusy(fn, label) {
|
|
1150
|
+
let lastError;
|
|
1151
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
1152
|
+
try {
|
|
1153
|
+
return await fn();
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
lastError = err;
|
|
1156
|
+
if (!isBusyError(err) || attempt === MAX_RETRIES) {
|
|
1157
|
+
throw err;
|
|
1158
|
+
}
|
|
1159
|
+
const backoff = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
1160
|
+
const jitter = Math.floor(Math.random() * MAX_JITTER_MS);
|
|
1161
|
+
process.stderr.write(
|
|
1162
|
+
`[exe-os] SQLITE_BUSY ${label} retry ${attempt + 1}/${MAX_RETRIES} \u2014 waiting ${backoff + jitter}ms
|
|
1163
|
+
`
|
|
1164
|
+
);
|
|
1165
|
+
await delay(backoff + jitter);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
throw lastError;
|
|
1169
|
+
}
|
|
1170
|
+
function wrapWithRetry(client) {
|
|
1171
|
+
return new Proxy(client, {
|
|
1172
|
+
get(target, prop, receiver) {
|
|
1173
|
+
if (prop === "execute") {
|
|
1174
|
+
return (sql) => retryOnBusy(() => target.execute(sql), "execute");
|
|
1175
|
+
}
|
|
1176
|
+
if (prop === "batch") {
|
|
1177
|
+
return (stmts, mode) => retryOnBusy(() => target.batch(stmts, mode), "batch");
|
|
1178
|
+
}
|
|
1179
|
+
return Reflect.get(target, prop, receiver);
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
var MAX_RETRIES, BASE_DELAY_MS, MAX_JITTER_MS;
|
|
1184
|
+
var init_db_retry = __esm({
|
|
1185
|
+
"src/lib/db-retry.ts"() {
|
|
1186
|
+
"use strict";
|
|
1187
|
+
MAX_RETRIES = 5;
|
|
1188
|
+
BASE_DELAY_MS = 250;
|
|
1189
|
+
MAX_JITTER_MS = 400;
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
// src/lib/employees.ts
|
|
1194
|
+
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
1195
|
+
import { existsSync as existsSync7, symlinkSync, readlinkSync, readFileSync as readFileSync3, renameSync as renameSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1196
|
+
import { execSync as execSync2 } from "child_process";
|
|
1197
|
+
import path6 from "path";
|
|
1198
|
+
import os3 from "os";
|
|
1199
|
+
function normalizeRole(role) {
|
|
1200
|
+
return (role ?? "").trim().toLowerCase();
|
|
1201
|
+
}
|
|
1202
|
+
function isCoordinatorRole(role) {
|
|
1203
|
+
return normalizeRole(role) === normalizeRole(COORDINATOR_ROLE);
|
|
1204
|
+
}
|
|
1205
|
+
function getCoordinatorEmployee(employees) {
|
|
1206
|
+
return employees.find((e) => isCoordinatorRole(e.role));
|
|
1207
|
+
}
|
|
1208
|
+
function getCoordinatorName(employees = loadEmployeesSync()) {
|
|
1209
|
+
return getCoordinatorEmployee(employees)?.name ?? DEFAULT_COORDINATOR_TEMPLATE_NAME;
|
|
1210
|
+
}
|
|
1211
|
+
function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
|
|
1212
|
+
if (!existsSync7(employeesPath)) return [];
|
|
1213
|
+
try {
|
|
1214
|
+
return JSON.parse(readFileSync3(employeesPath, "utf-8"));
|
|
1215
|
+
} catch {
|
|
1216
|
+
return [];
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, IDENTITY_DIR;
|
|
1220
|
+
var init_employees = __esm({
|
|
1221
|
+
"src/lib/employees.ts"() {
|
|
1222
|
+
"use strict";
|
|
1223
|
+
init_config();
|
|
1224
|
+
EMPLOYEES_PATH = path6.join(EXE_AI_DIR, "exe-employees.json");
|
|
1225
|
+
DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
|
|
1226
|
+
COORDINATOR_ROLE = "COO";
|
|
1227
|
+
IDENTITY_DIR = path6.join(EXE_AI_DIR, "identity");
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
// src/lib/database-adapter.ts
|
|
1232
|
+
import os4 from "os";
|
|
1233
|
+
import path7 from "path";
|
|
1234
|
+
import { createRequire } from "module";
|
|
1235
|
+
import { pathToFileURL } from "url";
|
|
1236
|
+
function quotedIdentifier(identifier) {
|
|
1237
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
1238
|
+
}
|
|
1239
|
+
function unqualifiedTableName(name) {
|
|
1240
|
+
const raw = name.trim().replace(/^"|"$/g, "");
|
|
1241
|
+
const parts = raw.split(".");
|
|
1242
|
+
return parts[parts.length - 1].replace(/^"|"$/g, "").toLowerCase();
|
|
1243
|
+
}
|
|
1244
|
+
function stripTrailingSemicolon(sql) {
|
|
1245
|
+
return sql.trim().replace(/;+\s*$/u, "");
|
|
1246
|
+
}
|
|
1247
|
+
function appendClause(sql, clause) {
|
|
1248
|
+
const trimmed = stripTrailingSemicolon(sql);
|
|
1249
|
+
const returningMatch = /\sRETURNING\b[\s\S]*$/iu.exec(trimmed);
|
|
1250
|
+
if (!returningMatch) {
|
|
1251
|
+
return `${trimmed}${clause}`;
|
|
1252
|
+
}
|
|
1253
|
+
const idx = returningMatch.index;
|
|
1254
|
+
return `${trimmed.slice(0, idx)}${clause}${trimmed.slice(idx)}`;
|
|
1255
|
+
}
|
|
1256
|
+
function normalizeStatement(stmt) {
|
|
1257
|
+
if (typeof stmt === "string") {
|
|
1258
|
+
return { kind: "positional", sql: stmt, args: [] };
|
|
1259
|
+
}
|
|
1260
|
+
const sql = stmt.sql;
|
|
1261
|
+
if (Array.isArray(stmt.args) || stmt.args === void 0) {
|
|
1262
|
+
return { kind: "positional", sql, args: stmt.args ?? [] };
|
|
1263
|
+
}
|
|
1264
|
+
return { kind: "named", sql, args: stmt.args };
|
|
1265
|
+
}
|
|
1266
|
+
function rewriteBooleanLiterals(sql) {
|
|
1267
|
+
let out = sql;
|
|
1268
|
+
for (const column of BOOLEAN_COLUMN_NAMES) {
|
|
1269
|
+
const scoped = `((?:\\b[a-z_][a-z0-9_]*\\.)?${column})`;
|
|
1270
|
+
out = out.replace(new RegExp(`${scoped}\\s*=\\s*0\\b`, "giu"), "$1 = FALSE");
|
|
1271
|
+
out = out.replace(new RegExp(`${scoped}\\s*=\\s*1\\b`, "giu"), "$1 = TRUE");
|
|
1272
|
+
out = out.replace(new RegExp(`${scoped}\\s*!=\\s*0\\b`, "giu"), "$1 != FALSE");
|
|
1273
|
+
out = out.replace(new RegExp(`${scoped}\\s*!=\\s*1\\b`, "giu"), "$1 != TRUE");
|
|
1274
|
+
out = out.replace(new RegExp(`${scoped}\\s*<>\\s*0\\b`, "giu"), "$1 <> FALSE");
|
|
1275
|
+
out = out.replace(new RegExp(`${scoped}\\s*<>\\s*1\\b`, "giu"), "$1 <> TRUE");
|
|
1276
|
+
}
|
|
1277
|
+
return out;
|
|
1278
|
+
}
|
|
1279
|
+
function rewriteInsertOrIgnore(sql) {
|
|
1280
|
+
if (!/^\s*INSERT\s+OR\s+IGNORE\s+INTO\b/iu.test(sql)) {
|
|
1281
|
+
return sql;
|
|
1282
|
+
}
|
|
1283
|
+
const replaced = sql.replace(/^\s*INSERT\s+OR\s+IGNORE\s+INTO\b/iu, "INSERT INTO");
|
|
1284
|
+
return /\bON\s+CONFLICT\b/iu.test(replaced) ? replaced : appendClause(replaced, " ON CONFLICT DO NOTHING");
|
|
1285
|
+
}
|
|
1286
|
+
function rewriteInsertOrReplace(sql) {
|
|
1287
|
+
const match = /^\s*INSERT\s+OR\s+REPLACE\s+INTO\s+([A-Za-z0-9_."]+)\s*\(([^)]+)\)([\s\S]*)$/iu.exec(sql);
|
|
1288
|
+
if (!match) {
|
|
1289
|
+
return sql;
|
|
1290
|
+
}
|
|
1291
|
+
const rawTable = match[1];
|
|
1292
|
+
const rawColumns = match[2];
|
|
1293
|
+
const remainder = match[3];
|
|
1294
|
+
const tableName = unqualifiedTableName(rawTable);
|
|
1295
|
+
const conflictKeys = UPSERT_KEYS[tableName];
|
|
1296
|
+
if (!conflictKeys?.length) {
|
|
1297
|
+
return sql;
|
|
1298
|
+
}
|
|
1299
|
+
const columns = rawColumns.split(",").map((col) => col.trim().replace(/^"|"$/g, ""));
|
|
1300
|
+
const updateColumns = columns.filter((col) => !conflictKeys.includes(col));
|
|
1301
|
+
const conflictTarget = conflictKeys.map(quotedIdentifier).join(", ");
|
|
1302
|
+
const updateClause = updateColumns.length === 0 ? " DO NOTHING" : ` DO UPDATE SET ${updateColumns.map((col) => `${quotedIdentifier(col)} = EXCLUDED.${quotedIdentifier(col)}`).join(", ")}`;
|
|
1303
|
+
return `INSERT INTO ${rawTable} (${rawColumns})${appendClause(remainder, ` ON CONFLICT (${conflictTarget})${updateClause}`)}`;
|
|
1304
|
+
}
|
|
1305
|
+
function rewriteSql(sql) {
|
|
1306
|
+
let out = sql;
|
|
1307
|
+
out = out.replace(/\bdatetime\(\s*['"]now['"]\s*\)/giu, "CURRENT_TIMESTAMP");
|
|
1308
|
+
out = out.replace(/\bvector32\s*\(\s*\?\s*\)/giu, "?");
|
|
1309
|
+
out = rewriteBooleanLiterals(out);
|
|
1310
|
+
out = rewriteInsertOrReplace(out);
|
|
1311
|
+
out = rewriteInsertOrIgnore(out);
|
|
1312
|
+
return stripTrailingSemicolon(out);
|
|
1313
|
+
}
|
|
1314
|
+
function toBoolean(value) {
|
|
1315
|
+
if (value === null || value === void 0) return value;
|
|
1316
|
+
if (typeof value === "boolean") return value;
|
|
1317
|
+
if (typeof value === "number") return value !== 0;
|
|
1318
|
+
if (typeof value === "bigint") return value !== 0n;
|
|
1319
|
+
if (typeof value === "string") {
|
|
1320
|
+
const normalized = value.trim().toLowerCase();
|
|
1321
|
+
if (normalized === "0" || normalized === "false") return false;
|
|
1322
|
+
if (normalized === "1" || normalized === "true") return true;
|
|
1323
|
+
}
|
|
1324
|
+
return Boolean(value);
|
|
1325
|
+
}
|
|
1326
|
+
function countQuestionMarks(sql, end) {
|
|
1327
|
+
let count = 0;
|
|
1328
|
+
let inSingle = false;
|
|
1329
|
+
let inDouble = false;
|
|
1330
|
+
let inLineComment = false;
|
|
1331
|
+
let inBlockComment = false;
|
|
1332
|
+
for (let i = 0; i < end; i++) {
|
|
1333
|
+
const ch = sql[i];
|
|
1334
|
+
const next = sql[i + 1];
|
|
1335
|
+
if (inLineComment) {
|
|
1336
|
+
if (ch === "\n") inLineComment = false;
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
if (inBlockComment) {
|
|
1340
|
+
if (ch === "*" && next === "/") {
|
|
1341
|
+
inBlockComment = false;
|
|
1342
|
+
i += 1;
|
|
1343
|
+
}
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
if (!inSingle && !inDouble && ch === "-" && next === "-") {
|
|
1347
|
+
inLineComment = true;
|
|
1348
|
+
i += 1;
|
|
461
1349
|
continue;
|
|
462
1350
|
}
|
|
463
1351
|
if (!inSingle && !inDouble && ch === "/" && next === "*") {
|
|
@@ -653,8 +1541,8 @@ async function loadPrismaClient() {
|
|
|
653
1541
|
}
|
|
654
1542
|
return new PrismaClient2();
|
|
655
1543
|
}
|
|
656
|
-
const exeDbRoot = process.env.EXE_DB_ROOT ??
|
|
657
|
-
const requireFromExeDb = createRequire(
|
|
1544
|
+
const exeDbRoot = process.env.EXE_DB_ROOT ?? path7.join(os4.homedir(), "exe-db");
|
|
1545
|
+
const requireFromExeDb = createRequire(path7.join(exeDbRoot, "package.json"));
|
|
658
1546
|
const prismaEntry = requireFromExeDb.resolve("@prisma/client");
|
|
659
1547
|
const module = await import(pathToFileURL(prismaEntry).href);
|
|
660
1548
|
const PrismaClient = module.PrismaClient ?? module.default?.PrismaClient;
|
|
@@ -924,10 +1812,19 @@ var init_database_adapter = __esm({
|
|
|
924
1812
|
}
|
|
925
1813
|
});
|
|
926
1814
|
|
|
1815
|
+
// src/types/memory.ts
|
|
1816
|
+
var EMBEDDING_DIM;
|
|
1817
|
+
var init_memory = __esm({
|
|
1818
|
+
"src/types/memory.ts"() {
|
|
1819
|
+
"use strict";
|
|
1820
|
+
EMBEDDING_DIM = 1024;
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
|
|
927
1824
|
// src/lib/daemon-auth.ts
|
|
928
1825
|
import crypto from "crypto";
|
|
929
|
-
import
|
|
930
|
-
import { existsSync as
|
|
1826
|
+
import path8 from "path";
|
|
1827
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
931
1828
|
function normalizeToken(token) {
|
|
932
1829
|
if (!token) return null;
|
|
933
1830
|
const trimmed = token.trim();
|
|
@@ -935,8 +1832,8 @@ function normalizeToken(token) {
|
|
|
935
1832
|
}
|
|
936
1833
|
function readDaemonToken() {
|
|
937
1834
|
try {
|
|
938
|
-
if (!
|
|
939
|
-
return normalizeToken(
|
|
1835
|
+
if (!existsSync8(DAEMON_TOKEN_PATH)) return null;
|
|
1836
|
+
return normalizeToken(readFileSync4(DAEMON_TOKEN_PATH, "utf8"));
|
|
940
1837
|
} catch {
|
|
941
1838
|
return null;
|
|
942
1839
|
}
|
|
@@ -946,7 +1843,7 @@ function ensureDaemonToken(seed) {
|
|
|
946
1843
|
if (existing) return existing;
|
|
947
1844
|
const token = normalizeToken(seed) ?? crypto.randomBytes(32).toString("hex");
|
|
948
1845
|
ensurePrivateDirSync(EXE_AI_DIR);
|
|
949
|
-
|
|
1846
|
+
writeFileSync4(DAEMON_TOKEN_PATH, `${token}
|
|
950
1847
|
`, "utf8");
|
|
951
1848
|
enforcePrivateFileSync(DAEMON_TOKEN_PATH);
|
|
952
1849
|
return token;
|
|
@@ -957,18 +1854,29 @@ var init_daemon_auth = __esm({
|
|
|
957
1854
|
"use strict";
|
|
958
1855
|
init_config();
|
|
959
1856
|
init_secure_files();
|
|
960
|
-
DAEMON_TOKEN_PATH =
|
|
1857
|
+
DAEMON_TOKEN_PATH = path8.join(EXE_AI_DIR, "exed.token");
|
|
961
1858
|
}
|
|
962
1859
|
});
|
|
963
1860
|
|
|
964
1861
|
// src/lib/exe-daemon-client.ts
|
|
1862
|
+
var exe_daemon_client_exports = {};
|
|
1863
|
+
__export(exe_daemon_client_exports, {
|
|
1864
|
+
connectEmbedDaemon: () => connectEmbedDaemon,
|
|
1865
|
+
disconnectClient: () => disconnectClient,
|
|
1866
|
+
embedBatchViaClient: () => embedBatchViaClient,
|
|
1867
|
+
embedViaClient: () => embedViaClient,
|
|
1868
|
+
isClientConnected: () => isClientConnected,
|
|
1869
|
+
pingDaemon: () => pingDaemon,
|
|
1870
|
+
sendDaemonRequest: () => sendDaemonRequest,
|
|
1871
|
+
sendIngestRequest: () => sendIngestRequest
|
|
1872
|
+
});
|
|
965
1873
|
import net from "net";
|
|
966
|
-
import
|
|
1874
|
+
import os5 from "os";
|
|
967
1875
|
import { spawn } from "child_process";
|
|
968
1876
|
import { randomUUID } from "crypto";
|
|
969
|
-
import { existsSync as
|
|
970
|
-
import
|
|
971
|
-
import { fileURLToPath } from "url";
|
|
1877
|
+
import { existsSync as existsSync9, unlinkSync as unlinkSync3, readFileSync as readFileSync5, openSync, closeSync, statSync as statSync2 } from "fs";
|
|
1878
|
+
import path9 from "path";
|
|
1879
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
972
1880
|
function handleData(chunk) {
|
|
973
1881
|
_buffer += chunk.toString();
|
|
974
1882
|
if (_buffer.length > MAX_BUFFER) {
|
|
@@ -995,9 +1903,9 @@ function handleData(chunk) {
|
|
|
995
1903
|
}
|
|
996
1904
|
}
|
|
997
1905
|
function cleanupStaleFiles() {
|
|
998
|
-
if (
|
|
1906
|
+
if (existsSync9(PID_PATH)) {
|
|
999
1907
|
try {
|
|
1000
|
-
const pid = parseInt(
|
|
1908
|
+
const pid = parseInt(readFileSync5(PID_PATH, "utf8").trim(), 10);
|
|
1001
1909
|
if (pid > 0) {
|
|
1002
1910
|
try {
|
|
1003
1911
|
process.kill(pid, 0);
|
|
@@ -1008,21 +1916,21 @@ function cleanupStaleFiles() {
|
|
|
1008
1916
|
} catch {
|
|
1009
1917
|
}
|
|
1010
1918
|
try {
|
|
1011
|
-
|
|
1919
|
+
unlinkSync3(PID_PATH);
|
|
1012
1920
|
} catch {
|
|
1013
1921
|
}
|
|
1014
1922
|
try {
|
|
1015
|
-
|
|
1923
|
+
unlinkSync3(SOCKET_PATH);
|
|
1016
1924
|
} catch {
|
|
1017
1925
|
}
|
|
1018
1926
|
}
|
|
1019
1927
|
}
|
|
1020
1928
|
function findPackageRoot() {
|
|
1021
|
-
let dir =
|
|
1022
|
-
const { root } =
|
|
1929
|
+
let dir = path9.dirname(fileURLToPath2(import.meta.url));
|
|
1930
|
+
const { root } = path9.parse(dir);
|
|
1023
1931
|
while (dir !== root) {
|
|
1024
|
-
if (
|
|
1025
|
-
dir =
|
|
1932
|
+
if (existsSync9(path9.join(dir, "package.json"))) return dir;
|
|
1933
|
+
dir = path9.dirname(dir);
|
|
1026
1934
|
}
|
|
1027
1935
|
return null;
|
|
1028
1936
|
}
|
|
@@ -1042,14 +1950,14 @@ function getAvailableMemoryGB() {
|
|
|
1042
1950
|
const speculativePages = speculative ? parseInt(speculative[1], 10) : 0;
|
|
1043
1951
|
return (freePages + inactivePages + speculativePages) * actualPageSize / (1024 * 1024 * 1024);
|
|
1044
1952
|
} catch {
|
|
1045
|
-
return
|
|
1953
|
+
return os5.freemem() / (1024 * 1024 * 1024);
|
|
1046
1954
|
}
|
|
1047
1955
|
}
|
|
1048
|
-
return
|
|
1956
|
+
return os5.freemem() / (1024 * 1024 * 1024);
|
|
1049
1957
|
}
|
|
1050
1958
|
function spawnDaemon() {
|
|
1051
1959
|
const freeGB = getAvailableMemoryGB();
|
|
1052
|
-
const totalGB =
|
|
1960
|
+
const totalGB = os5.totalmem() / (1024 * 1024 * 1024);
|
|
1053
1961
|
if (totalGB <= 8) {
|
|
1054
1962
|
process.stderr.write(
|
|
1055
1963
|
`[exed-client] SKIP: ${totalGB.toFixed(0)}GB system \u2014 embedding daemon disabled. Using keyword search only. Minimum 16GB recommended for vector search.
|
|
@@ -1069,8 +1977,8 @@ function spawnDaemon() {
|
|
|
1069
1977
|
process.stderr.write("[exed-client] WARN: cannot find package root\n");
|
|
1070
1978
|
return;
|
|
1071
1979
|
}
|
|
1072
|
-
const daemonPath =
|
|
1073
|
-
if (!
|
|
1980
|
+
const daemonPath = path9.join(pkgRoot, "dist", "lib", "exe-daemon.js");
|
|
1981
|
+
if (!existsSync9(daemonPath)) {
|
|
1074
1982
|
process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
|
|
1075
1983
|
`);
|
|
1076
1984
|
return;
|
|
@@ -1079,7 +1987,7 @@ function spawnDaemon() {
|
|
|
1079
1987
|
const daemonToken = ensureDaemonToken(process.env[DAEMON_TOKEN_ENV] ?? null);
|
|
1080
1988
|
process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
|
|
1081
1989
|
`);
|
|
1082
|
-
const logPath =
|
|
1990
|
+
const logPath = path9.join(path9.dirname(SOCKET_PATH), "exed.log");
|
|
1083
1991
|
let stderrFd = "ignore";
|
|
1084
1992
|
try {
|
|
1085
1993
|
stderrFd = openSync(logPath, "a");
|
|
@@ -1116,10 +2024,10 @@ function acquireSpawnLock() {
|
|
|
1116
2024
|
return true;
|
|
1117
2025
|
} catch {
|
|
1118
2026
|
try {
|
|
1119
|
-
const stat =
|
|
2027
|
+
const stat = statSync2(SPAWN_LOCK_PATH);
|
|
1120
2028
|
if (Date.now() - stat.mtimeMs > SPAWN_LOCK_STALE_MS) {
|
|
1121
2029
|
try {
|
|
1122
|
-
|
|
2030
|
+
unlinkSync3(SPAWN_LOCK_PATH);
|
|
1123
2031
|
} catch {
|
|
1124
2032
|
}
|
|
1125
2033
|
try {
|
|
@@ -1136,7 +2044,7 @@ function acquireSpawnLock() {
|
|
|
1136
2044
|
}
|
|
1137
2045
|
function releaseSpawnLock() {
|
|
1138
2046
|
try {
|
|
1139
|
-
|
|
2047
|
+
unlinkSync3(SPAWN_LOCK_PATH);
|
|
1140
2048
|
} catch {
|
|
1141
2049
|
}
|
|
1142
2050
|
}
|
|
@@ -1198,6 +2106,9 @@ async function connectEmbedDaemon() {
|
|
|
1198
2106
|
}
|
|
1199
2107
|
return false;
|
|
1200
2108
|
}
|
|
2109
|
+
function sendRequest(texts, priority) {
|
|
2110
|
+
return sendDaemonRequest({ texts, priority });
|
|
2111
|
+
}
|
|
1201
2112
|
function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
1202
2113
|
return new Promise((resolve) => {
|
|
1203
2114
|
if (!_socket || !_connected) {
|
|
@@ -1220,18 +2131,178 @@ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
1220
2131
|
}
|
|
1221
2132
|
});
|
|
1222
2133
|
}
|
|
2134
|
+
async function pingDaemon() {
|
|
2135
|
+
if (!_socket || !_connected) return null;
|
|
2136
|
+
const response = await sendDaemonRequest({ type: "health" }, 5e3);
|
|
2137
|
+
if (response.health) {
|
|
2138
|
+
return response.health;
|
|
2139
|
+
}
|
|
2140
|
+
return null;
|
|
2141
|
+
}
|
|
2142
|
+
function killAndRespawnDaemon() {
|
|
2143
|
+
if (!acquireSpawnLock()) {
|
|
2144
|
+
process.stderr.write("[exed-client] Another process is already restarting daemon \u2014 skipping\n");
|
|
2145
|
+
if (_socket) {
|
|
2146
|
+
_socket.destroy();
|
|
2147
|
+
_socket = null;
|
|
2148
|
+
}
|
|
2149
|
+
_connected = false;
|
|
2150
|
+
_buffer = "";
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
try {
|
|
2154
|
+
process.stderr.write("[exed-client] Killing daemon for restart...\n");
|
|
2155
|
+
if (existsSync9(PID_PATH)) {
|
|
2156
|
+
try {
|
|
2157
|
+
const pid = parseInt(readFileSync5(PID_PATH, "utf8").trim(), 10);
|
|
2158
|
+
if (pid > 0) {
|
|
2159
|
+
try {
|
|
2160
|
+
process.kill(pid, "SIGKILL");
|
|
2161
|
+
} catch {
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
} catch {
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
if (_socket) {
|
|
2168
|
+
_socket.destroy();
|
|
2169
|
+
_socket = null;
|
|
2170
|
+
}
|
|
2171
|
+
_connected = false;
|
|
2172
|
+
_buffer = "";
|
|
2173
|
+
try {
|
|
2174
|
+
unlinkSync3(PID_PATH);
|
|
2175
|
+
} catch {
|
|
2176
|
+
}
|
|
2177
|
+
try {
|
|
2178
|
+
unlinkSync3(SOCKET_PATH);
|
|
2179
|
+
} catch {
|
|
2180
|
+
}
|
|
2181
|
+
spawnDaemon();
|
|
2182
|
+
} finally {
|
|
2183
|
+
releaseSpawnLock();
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
function isDaemonTooYoung() {
|
|
2187
|
+
try {
|
|
2188
|
+
const stat = statSync2(PID_PATH);
|
|
2189
|
+
return Date.now() - stat.mtimeMs < MIN_DAEMON_AGE_MS;
|
|
2190
|
+
} catch {
|
|
2191
|
+
return false;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
async function retryThenRestart(doRequest, label) {
|
|
2195
|
+
const result = await doRequest();
|
|
2196
|
+
if (!result.error) {
|
|
2197
|
+
_consecutiveFailures = 0;
|
|
2198
|
+
return result;
|
|
2199
|
+
}
|
|
2200
|
+
_consecutiveFailures++;
|
|
2201
|
+
for (let i = 0; i < MAX_RETRIES_BEFORE_RESTART; i++) {
|
|
2202
|
+
const delayMs = RETRY_DELAYS_MS[i] ?? 5e3;
|
|
2203
|
+
process.stderr.write(`[exed-client] ${label} failed (${result.error}), retry ${i + 1}/${MAX_RETRIES_BEFORE_RESTART} in ${delayMs}ms
|
|
2204
|
+
`);
|
|
2205
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
2206
|
+
if (!_connected) {
|
|
2207
|
+
if (!await connectToSocket()) continue;
|
|
2208
|
+
}
|
|
2209
|
+
const retry = await doRequest();
|
|
2210
|
+
if (!retry.error) {
|
|
2211
|
+
_consecutiveFailures = 0;
|
|
2212
|
+
return retry;
|
|
2213
|
+
}
|
|
2214
|
+
_consecutiveFailures++;
|
|
2215
|
+
}
|
|
2216
|
+
if (isDaemonTooYoung()) {
|
|
2217
|
+
process.stderr.write(`[exed-client] ${label}: daemon too young (< ${MIN_DAEMON_AGE_MS / 1e3}s) \u2014 skipping restart
|
|
2218
|
+
`);
|
|
2219
|
+
return { error: result.error };
|
|
2220
|
+
}
|
|
2221
|
+
process.stderr.write(`[exed-client] ${label}: ${_consecutiveFailures} consecutive failures \u2014 restarting daemon
|
|
2222
|
+
`);
|
|
2223
|
+
killAndRespawnDaemon();
|
|
2224
|
+
const start = Date.now();
|
|
2225
|
+
let delay2 = 200;
|
|
2226
|
+
while (Date.now() - start < CONNECT_TIMEOUT_MS) {
|
|
2227
|
+
await new Promise((r) => setTimeout(r, delay2));
|
|
2228
|
+
if (await connectToSocket()) break;
|
|
2229
|
+
delay2 = Math.min(delay2 * 2, 3e3);
|
|
2230
|
+
}
|
|
2231
|
+
if (!_connected) return { error: "Daemon restart failed" };
|
|
2232
|
+
const final = await doRequest();
|
|
2233
|
+
if (!final.error) _consecutiveFailures = 0;
|
|
2234
|
+
return final;
|
|
2235
|
+
}
|
|
2236
|
+
async function embedViaClient(text, priority = "high") {
|
|
2237
|
+
if (!_connected && !await connectEmbedDaemon()) return null;
|
|
2238
|
+
_requestCount++;
|
|
2239
|
+
if (_requestCount % HEALTH_CHECK_INTERVAL === 0) {
|
|
2240
|
+
const health = await pingDaemon();
|
|
2241
|
+
if (!health && !isDaemonTooYoung()) {
|
|
2242
|
+
process.stderr.write(`[exed-client] Periodic health check failed at request ${_requestCount} \u2014 restarting daemon
|
|
2243
|
+
`);
|
|
2244
|
+
killAndRespawnDaemon();
|
|
2245
|
+
const start = Date.now();
|
|
2246
|
+
let d = 200;
|
|
2247
|
+
while (Date.now() - start < CONNECT_TIMEOUT_MS) {
|
|
2248
|
+
await new Promise((r) => setTimeout(r, d));
|
|
2249
|
+
if (await connectToSocket()) break;
|
|
2250
|
+
d = Math.min(d * 2, 3e3);
|
|
2251
|
+
}
|
|
2252
|
+
if (!_connected) return null;
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
const result = await retryThenRestart(
|
|
2256
|
+
() => sendRequest([text], priority),
|
|
2257
|
+
"Embed"
|
|
2258
|
+
);
|
|
2259
|
+
return !result.error && result.vectors?.[0] ? result.vectors[0] : null;
|
|
2260
|
+
}
|
|
2261
|
+
async function embedBatchViaClient(texts, priority = "high") {
|
|
2262
|
+
if (!_connected && !await connectEmbedDaemon()) return null;
|
|
2263
|
+
_requestCount++;
|
|
2264
|
+
const result = await retryThenRestart(
|
|
2265
|
+
() => sendRequest(texts, priority),
|
|
2266
|
+
"Batch embed"
|
|
2267
|
+
);
|
|
2268
|
+
return !result.error && result.vectors ? result.vectors : null;
|
|
2269
|
+
}
|
|
2270
|
+
function disconnectClient() {
|
|
2271
|
+
if (_socket) {
|
|
2272
|
+
_socket.destroy();
|
|
2273
|
+
_socket = null;
|
|
2274
|
+
}
|
|
2275
|
+
_connected = false;
|
|
2276
|
+
_buffer = "";
|
|
2277
|
+
for (const [id, entry] of _pending) {
|
|
2278
|
+
clearTimeout(entry.timer);
|
|
2279
|
+
_pending.delete(id);
|
|
2280
|
+
entry.resolve({ error: "Client disconnected" });
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
1223
2283
|
function isClientConnected() {
|
|
1224
2284
|
return _connected;
|
|
1225
2285
|
}
|
|
1226
|
-
|
|
2286
|
+
function sendIngestRequest(payload) {
|
|
2287
|
+
if (!_socket || !_connected) return false;
|
|
2288
|
+
try {
|
|
2289
|
+
const id = randomUUID();
|
|
2290
|
+
const token = process.env[DAEMON_TOKEN_ENV] ?? readDaemonToken();
|
|
2291
|
+
_socket.write(JSON.stringify({ id, token, type: "ingest", ...payload }) + "\n");
|
|
2292
|
+
return true;
|
|
2293
|
+
} catch {
|
|
2294
|
+
return false;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, DAEMON_TOKEN_ENV, _socket, _connected, _buffer, _requestCount, _consecutiveFailures, HEALTH_CHECK_INTERVAL, MAX_RETRIES_BEFORE_RESTART, RETRY_DELAYS_MS, MIN_DAEMON_AGE_MS, _pending, MAX_BUFFER;
|
|
1227
2298
|
var init_exe_daemon_client = __esm({
|
|
1228
2299
|
"src/lib/exe-daemon-client.ts"() {
|
|
1229
2300
|
"use strict";
|
|
1230
2301
|
init_config();
|
|
1231
2302
|
init_daemon_auth();
|
|
1232
|
-
SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ??
|
|
1233
|
-
PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ??
|
|
1234
|
-
SPAWN_LOCK_PATH =
|
|
2303
|
+
SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path9.join(EXE_AI_DIR, "exed.sock");
|
|
2304
|
+
PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path9.join(EXE_AI_DIR, "exed.pid");
|
|
2305
|
+
SPAWN_LOCK_PATH = path9.join(EXE_AI_DIR, "exed-spawn.lock");
|
|
1235
2306
|
SPAWN_LOCK_STALE_MS = 3e4;
|
|
1236
2307
|
CONNECT_TIMEOUT_MS = 15e3;
|
|
1237
2308
|
REQUEST_TIMEOUT_MS = 3e4;
|
|
@@ -1239,12 +2310,27 @@ var init_exe_daemon_client = __esm({
|
|
|
1239
2310
|
_socket = null;
|
|
1240
2311
|
_connected = false;
|
|
1241
2312
|
_buffer = "";
|
|
2313
|
+
_requestCount = 0;
|
|
2314
|
+
_consecutiveFailures = 0;
|
|
2315
|
+
HEALTH_CHECK_INTERVAL = 100;
|
|
2316
|
+
MAX_RETRIES_BEFORE_RESTART = 3;
|
|
2317
|
+
RETRY_DELAYS_MS = [1e3, 3e3, 5e3];
|
|
2318
|
+
MIN_DAEMON_AGE_MS = 3e4;
|
|
1242
2319
|
_pending = /* @__PURE__ */ new Map();
|
|
1243
2320
|
MAX_BUFFER = 1e7;
|
|
1244
2321
|
}
|
|
1245
2322
|
});
|
|
1246
2323
|
|
|
1247
2324
|
// src/lib/daemon-protocol.ts
|
|
2325
|
+
var daemon_protocol_exports = {};
|
|
2326
|
+
__export(daemon_protocol_exports, {
|
|
2327
|
+
deserializeArgs: () => deserializeArgs,
|
|
2328
|
+
deserializeResultSet: () => deserializeResultSet,
|
|
2329
|
+
deserializeValue: () => deserializeValue,
|
|
2330
|
+
serializeArgs: () => serializeArgs,
|
|
2331
|
+
serializeResultSet: () => serializeResultSet,
|
|
2332
|
+
serializeValue: () => serializeValue
|
|
2333
|
+
});
|
|
1248
2334
|
function serializeValue(v) {
|
|
1249
2335
|
if (v === null || v === void 0) return null;
|
|
1250
2336
|
if (typeof v === "bigint") return Number(v);
|
|
@@ -1269,6 +2355,32 @@ function deserializeValue(v) {
|
|
|
1269
2355
|
}
|
|
1270
2356
|
return v;
|
|
1271
2357
|
}
|
|
2358
|
+
function serializeArgs(args) {
|
|
2359
|
+
return args.map(serializeValue);
|
|
2360
|
+
}
|
|
2361
|
+
function deserializeArgs(args) {
|
|
2362
|
+
return args.map(deserializeValue);
|
|
2363
|
+
}
|
|
2364
|
+
function serializeResultSet(rs) {
|
|
2365
|
+
const rows = [];
|
|
2366
|
+
for (const row of rs.rows) {
|
|
2367
|
+
const obj = {};
|
|
2368
|
+
for (let i = 0; i < rs.columns.length; i++) {
|
|
2369
|
+
const col = rs.columns[i];
|
|
2370
|
+
if (col !== void 0) {
|
|
2371
|
+
obj[col] = serializeValue(row[i]);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
rows.push(obj);
|
|
2375
|
+
}
|
|
2376
|
+
return {
|
|
2377
|
+
columns: [...rs.columns],
|
|
2378
|
+
columnTypes: [...rs.columnTypes ?? []],
|
|
2379
|
+
rows,
|
|
2380
|
+
rowsAffected: typeof rs.rowsAffected === "bigint" ? Number(rs.rowsAffected) : rs.rowsAffected ?? 0,
|
|
2381
|
+
lastInsertRowid: rs.lastInsertRowid != null ? typeof rs.lastInsertRowid === "bigint" ? Number(rs.lastInsertRowid) : rs.lastInsertRowid : null
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
1272
2384
|
function deserializeResultSet(srs) {
|
|
1273
2385
|
const rows = srs.rows.map((obj) => {
|
|
1274
2386
|
const values = srs.columns.map(
|
|
@@ -1458,7 +2570,7 @@ __export(database_exports, {
|
|
|
1458
2570
|
isInitialized: () => isInitialized,
|
|
1459
2571
|
setExternalClient: () => setExternalClient
|
|
1460
2572
|
});
|
|
1461
|
-
import { createClient } from "@libsql/client";
|
|
2573
|
+
import { createClient as createClient2 } from "@libsql/client";
|
|
1462
2574
|
async function initDatabase(config) {
|
|
1463
2575
|
if (_walCheckpointTimer) {
|
|
1464
2576
|
clearInterval(_walCheckpointTimer);
|
|
@@ -1483,7 +2595,7 @@ async function initDatabase(config) {
|
|
|
1483
2595
|
if (config.encryptionKey) {
|
|
1484
2596
|
opts.encryptionKey = config.encryptionKey;
|
|
1485
2597
|
}
|
|
1486
|
-
_client =
|
|
2598
|
+
_client = createClient2(opts);
|
|
1487
2599
|
_resilientClient = wrapWithRetry(_client);
|
|
1488
2600
|
_adapterClient = _resilientClient;
|
|
1489
2601
|
_client.execute("PRAGMA busy_timeout = 30000").catch(() => {
|
|
@@ -2618,295 +3730,19 @@ async function ensureSchema() {
|
|
|
2618
3730
|
} catch {
|
|
2619
3731
|
}
|
|
2620
3732
|
try {
|
|
2621
|
-
await client.execute(
|
|
2622
|
-
`CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memory_type)`
|
|
2623
|
-
);
|
|
2624
|
-
} catch {
|
|
2625
|
-
}
|
|
2626
|
-
try {
|
|
2627
|
-
await client.execute({
|
|
2628
|
-
sql: `ALTER TABLE memories ADD COLUMN trajectory TEXT`,
|
|
2629
|
-
args: []
|
|
2630
|
-
});
|
|
2631
|
-
} catch {
|
|
2632
|
-
}
|
|
2633
|
-
for (const col of [
|
|
2634
|
-
"ALTER TABLE memories ADD COLUMN intent TEXT",
|
|
2635
|
-
"ALTER TABLE memories ADD COLUMN outcome TEXT",
|
|
2636
|
-
"ALTER TABLE memories ADD COLUMN domain TEXT",
|
|
2637
|
-
"ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
|
|
2638
|
-
"ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
|
|
2639
|
-
"ALTER TABLE memories ADD COLUMN chain_position TEXT",
|
|
2640
|
-
"ALTER TABLE memories ADD COLUMN review_status TEXT",
|
|
2641
|
-
"ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
|
|
2642
|
-
"ALTER TABLE memories ADD COLUMN file_paths TEXT",
|
|
2643
|
-
"ALTER TABLE memories ADD COLUMN commit_hash TEXT",
|
|
2644
|
-
"ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
|
|
2645
|
-
"ALTER TABLE memories ADD COLUMN token_cost REAL",
|
|
2646
|
-
"ALTER TABLE memories ADD COLUMN audience TEXT",
|
|
2647
|
-
"ALTER TABLE memories ADD COLUMN language_type TEXT",
|
|
2648
|
-
"ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
|
|
2649
|
-
]) {
|
|
2650
|
-
try {
|
|
2651
|
-
await client.execute(col);
|
|
2652
|
-
} catch {
|
|
2653
|
-
}
|
|
2654
|
-
}
|
|
2655
|
-
try {
|
|
2656
|
-
await client.execute({
|
|
2657
|
-
sql: `UPDATE tasks SET status = 'closed' WHERE status = 'done' AND result IS NOT NULL`,
|
|
2658
|
-
args: []
|
|
2659
|
-
});
|
|
2660
|
-
} catch {
|
|
2661
|
-
}
|
|
2662
|
-
}
|
|
2663
|
-
async function disposeDatabase() {
|
|
2664
|
-
if (_walCheckpointTimer) {
|
|
2665
|
-
clearInterval(_walCheckpointTimer);
|
|
2666
|
-
_walCheckpointTimer = null;
|
|
2667
|
-
}
|
|
2668
|
-
if (_daemonClient) {
|
|
2669
|
-
_daemonClient.close();
|
|
2670
|
-
_daemonClient = null;
|
|
2671
|
-
}
|
|
2672
|
-
if (_adapterClient && _adapterClient !== _resilientClient) {
|
|
2673
|
-
_adapterClient.close();
|
|
2674
|
-
}
|
|
2675
|
-
_adapterClient = null;
|
|
2676
|
-
if (_client) {
|
|
2677
|
-
_client.close();
|
|
2678
|
-
_client = null;
|
|
2679
|
-
_resilientClient = null;
|
|
2680
|
-
}
|
|
2681
|
-
}
|
|
2682
|
-
var _client, _resilientClient, _walCheckpointTimer, _daemonClient, _adapterClient, initTurso, SOFT_DELETE_RETENTION_DAYS, disposeTurso;
|
|
2683
|
-
var init_database = __esm({
|
|
2684
|
-
"src/lib/database.ts"() {
|
|
2685
|
-
"use strict";
|
|
2686
|
-
init_db_retry();
|
|
2687
|
-
init_employees();
|
|
2688
|
-
init_database_adapter();
|
|
2689
|
-
init_memory();
|
|
2690
|
-
_client = null;
|
|
2691
|
-
_resilientClient = null;
|
|
2692
|
-
_walCheckpointTimer = null;
|
|
2693
|
-
_daemonClient = null;
|
|
2694
|
-
_adapterClient = null;
|
|
2695
|
-
initTurso = initDatabase;
|
|
2696
|
-
SOFT_DELETE_RETENTION_DAYS = 7;
|
|
2697
|
-
disposeTurso = disposeDatabase;
|
|
2698
|
-
}
|
|
2699
|
-
});
|
|
2700
|
-
|
|
2701
|
-
// src/lib/shard-manager.ts
|
|
2702
|
-
var shard_manager_exports = {};
|
|
2703
|
-
__export(shard_manager_exports, {
|
|
2704
|
-
auditShardHealth: () => auditShardHealth,
|
|
2705
|
-
disposeShards: () => disposeShards,
|
|
2706
|
-
ensureShardSchema: () => ensureShardSchema,
|
|
2707
|
-
getOpenShardCount: () => getOpenShardCount,
|
|
2708
|
-
getReadyShardClient: () => getReadyShardClient,
|
|
2709
|
-
getShardClient: () => getShardClient,
|
|
2710
|
-
getShardsDir: () => getShardsDir,
|
|
2711
|
-
initShardManager: () => initShardManager,
|
|
2712
|
-
isShardingEnabled: () => isShardingEnabled,
|
|
2713
|
-
listShards: () => listShards,
|
|
2714
|
-
shardExists: () => shardExists
|
|
2715
|
-
});
|
|
2716
|
-
import path7 from "path";
|
|
2717
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readdirSync, renameSync as renameSync3, statSync as statSync2 } from "fs";
|
|
2718
|
-
import { createClient as createClient2 } from "@libsql/client";
|
|
2719
|
-
function initShardManager(encryptionKey) {
|
|
2720
|
-
_encryptionKey = encryptionKey;
|
|
2721
|
-
if (!existsSync7(SHARDS_DIR)) {
|
|
2722
|
-
mkdirSync2(SHARDS_DIR, { recursive: true });
|
|
2723
|
-
}
|
|
2724
|
-
_shardingEnabled = true;
|
|
2725
|
-
if (_evictionTimer) clearInterval(_evictionTimer);
|
|
2726
|
-
_evictionTimer = setInterval(evictIdleShards, EVICTION_INTERVAL_MS);
|
|
2727
|
-
_evictionTimer.unref();
|
|
2728
|
-
}
|
|
2729
|
-
function isShardingEnabled() {
|
|
2730
|
-
return _shardingEnabled;
|
|
2731
|
-
}
|
|
2732
|
-
function getShardsDir() {
|
|
2733
|
-
return SHARDS_DIR;
|
|
2734
|
-
}
|
|
2735
|
-
function getShardClient(projectName) {
|
|
2736
|
-
if (!_encryptionKey) {
|
|
2737
|
-
throw new Error("Shard manager not initialized. Call initShardManager() first.");
|
|
2738
|
-
}
|
|
2739
|
-
const safeName = safeShardName(projectName);
|
|
2740
|
-
if (!safeName || safeName === "unknown") {
|
|
2741
|
-
throw new Error(`Invalid project name for shard: "${projectName}" (resolved to "${safeName}")`);
|
|
2742
|
-
}
|
|
2743
|
-
const cached = _shards.get(safeName);
|
|
2744
|
-
if (cached) {
|
|
2745
|
-
_shardLastAccess.set(safeName, Date.now());
|
|
2746
|
-
return cached;
|
|
2747
|
-
}
|
|
2748
|
-
while (_shards.size >= MAX_OPEN_SHARDS) {
|
|
2749
|
-
evictLRU();
|
|
2750
|
-
}
|
|
2751
|
-
const dbPath = path7.join(SHARDS_DIR, `${safeName}.db`);
|
|
2752
|
-
const client = createClient2({
|
|
2753
|
-
url: `file:${dbPath}`,
|
|
2754
|
-
encryptionKey: _encryptionKey
|
|
2755
|
-
});
|
|
2756
|
-
_shards.set(safeName, client);
|
|
2757
|
-
_shardLastAccess.set(safeName, Date.now());
|
|
2758
|
-
return client;
|
|
2759
|
-
}
|
|
2760
|
-
function shardExists(projectName) {
|
|
2761
|
-
const safeName = safeShardName(projectName);
|
|
2762
|
-
return existsSync7(path7.join(SHARDS_DIR, `${safeName}.db`));
|
|
2763
|
-
}
|
|
2764
|
-
function safeShardName(projectName) {
|
|
2765
|
-
return projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
2766
|
-
}
|
|
2767
|
-
function listShards() {
|
|
2768
|
-
if (!existsSync7(SHARDS_DIR)) return [];
|
|
2769
|
-
return readdirSync(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
|
|
2770
|
-
}
|
|
2771
|
-
async function auditShardHealth(options = {}) {
|
|
2772
|
-
if (!_encryptionKey) {
|
|
2773
|
-
throw new Error("Shard manager not initialized. Call initShardManager() first.");
|
|
2774
|
-
}
|
|
2775
|
-
const repair = options.repair === true;
|
|
2776
|
-
const dryRun = options.dryRun === true;
|
|
2777
|
-
const names = listShards();
|
|
2778
|
-
const shards = [];
|
|
2779
|
-
for (const name of names) {
|
|
2780
|
-
const dbPath = path7.join(SHARDS_DIR, `${name}.db`);
|
|
2781
|
-
const stat = statSync2(dbPath);
|
|
2782
|
-
const item = {
|
|
2783
|
-
name,
|
|
2784
|
-
path: dbPath,
|
|
2785
|
-
ok: false,
|
|
2786
|
-
unreadable: false,
|
|
2787
|
-
error: null,
|
|
2788
|
-
size: stat.size,
|
|
2789
|
-
mtime: stat.mtime.toISOString(),
|
|
2790
|
-
memoryCount: null
|
|
2791
|
-
};
|
|
2792
|
-
const client = createClient2({
|
|
2793
|
-
url: `file:${dbPath}`,
|
|
2794
|
-
encryptionKey: _encryptionKey
|
|
2795
|
-
});
|
|
2796
|
-
try {
|
|
2797
|
-
await client.execute("SELECT COUNT(*) as cnt FROM sqlite_schema");
|
|
2798
|
-
const hasMemories = await client.execute(
|
|
2799
|
-
"SELECT COUNT(*) as cnt FROM sqlite_schema WHERE type = 'table' AND name = 'memories'"
|
|
2800
|
-
);
|
|
2801
|
-
if (Number(hasMemories.rows[0]?.cnt ?? 0) > 0) {
|
|
2802
|
-
const mem = await client.execute("SELECT COUNT(*) as cnt FROM memories");
|
|
2803
|
-
item.memoryCount = Number(mem.rows[0]?.cnt ?? 0);
|
|
2804
|
-
}
|
|
2805
|
-
item.ok = true;
|
|
2806
|
-
} catch (err) {
|
|
2807
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2808
|
-
item.error = message;
|
|
2809
|
-
item.unreadable = /SQLITE_NOTADB|file is not a database/i.test(message);
|
|
2810
|
-
if (item.unreadable && repair && !dryRun) {
|
|
2811
|
-
client.close();
|
|
2812
|
-
_shards.delete(name);
|
|
2813
|
-
_shardLastAccess.delete(name);
|
|
2814
|
-
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2815
|
-
const archivedPath = path7.join(SHARDS_DIR, `${name}.db.broken-${stamp}`);
|
|
2816
|
-
renameSync3(dbPath, archivedPath);
|
|
2817
|
-
item.archivedPath = archivedPath;
|
|
2818
|
-
}
|
|
2819
|
-
} finally {
|
|
2820
|
-
try {
|
|
2821
|
-
client.close();
|
|
2822
|
-
} catch {
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
2825
|
-
shards.push(item);
|
|
2826
|
-
}
|
|
2827
|
-
return {
|
|
2828
|
-
total: shards.length,
|
|
2829
|
-
ok: shards.filter((s) => s.ok).length,
|
|
2830
|
-
unreadable: shards.filter((s) => s.unreadable).length,
|
|
2831
|
-
archived: shards.filter((s) => Boolean(s.archivedPath)).length,
|
|
2832
|
-
shards
|
|
2833
|
-
};
|
|
2834
|
-
}
|
|
2835
|
-
async function ensureShardSchema(client) {
|
|
2836
|
-
await client.execute("PRAGMA journal_mode = WAL");
|
|
2837
|
-
await client.execute("PRAGMA busy_timeout = 30000");
|
|
2838
|
-
try {
|
|
2839
|
-
await client.execute("PRAGMA libsql_vector_search_ef = 128");
|
|
2840
|
-
} catch {
|
|
2841
|
-
}
|
|
2842
|
-
await client.executeMultiple(`
|
|
2843
|
-
CREATE TABLE IF NOT EXISTS memories (
|
|
2844
|
-
id TEXT PRIMARY KEY,
|
|
2845
|
-
agent_id TEXT NOT NULL,
|
|
2846
|
-
agent_role TEXT NOT NULL,
|
|
2847
|
-
session_id TEXT NOT NULL,
|
|
2848
|
-
timestamp TEXT NOT NULL,
|
|
2849
|
-
tool_name TEXT NOT NULL,
|
|
2850
|
-
project_name TEXT NOT NULL,
|
|
2851
|
-
has_error INTEGER NOT NULL DEFAULT 0,
|
|
2852
|
-
raw_text TEXT NOT NULL,
|
|
2853
|
-
vector F32_BLOB(1024),
|
|
2854
|
-
version INTEGER NOT NULL DEFAULT 0
|
|
2855
|
-
);
|
|
2856
|
-
|
|
2857
|
-
CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent_id);
|
|
2858
|
-
CREATE INDEX IF NOT EXISTS idx_memories_timestamp ON memories(timestamp);
|
|
2859
|
-
CREATE INDEX IF NOT EXISTS idx_memories_agent_project ON memories(agent_id, project_name);
|
|
2860
|
-
`);
|
|
2861
|
-
await client.executeMultiple(`
|
|
2862
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
2863
|
-
raw_text,
|
|
2864
|
-
content='memories',
|
|
2865
|
-
content_rowid='rowid'
|
|
3733
|
+
await client.execute(
|
|
3734
|
+
`CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memory_type)`
|
|
2866
3735
|
);
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
CREATE TRIGGER IF NOT EXISTS memories_fts_au AFTER UPDATE ON memories BEGIN
|
|
2877
|
-
INSERT INTO memories_fts(memories_fts, rowid, raw_text) VALUES('delete', old.rowid, old.raw_text);
|
|
2878
|
-
INSERT INTO memories_fts(rowid, raw_text) VALUES (new.rowid, new.raw_text);
|
|
2879
|
-
END;
|
|
2880
|
-
`);
|
|
3736
|
+
} catch {
|
|
3737
|
+
}
|
|
3738
|
+
try {
|
|
3739
|
+
await client.execute({
|
|
3740
|
+
sql: `ALTER TABLE memories ADD COLUMN trajectory TEXT`,
|
|
3741
|
+
args: []
|
|
3742
|
+
});
|
|
3743
|
+
} catch {
|
|
3744
|
+
}
|
|
2881
3745
|
for (const col of [
|
|
2882
|
-
"ALTER TABLE memories ADD COLUMN task_id TEXT",
|
|
2883
|
-
"ALTER TABLE memories ADD COLUMN consolidated INTEGER NOT NULL DEFAULT 0",
|
|
2884
|
-
"ALTER TABLE memories ADD COLUMN author_device_id TEXT",
|
|
2885
|
-
"ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'business'",
|
|
2886
|
-
"ALTER TABLE memories ADD COLUMN importance INTEGER DEFAULT 5",
|
|
2887
|
-
"ALTER TABLE memories ADD COLUMN status TEXT DEFAULT 'active'",
|
|
2888
|
-
"ALTER TABLE memories ADD COLUMN wiki_synced INTEGER DEFAULT 0",
|
|
2889
|
-
"ALTER TABLE memories ADD COLUMN graph_extracted INTEGER DEFAULT 0",
|
|
2890
|
-
"ALTER TABLE memories ADD COLUMN content_hash TEXT",
|
|
2891
|
-
"ALTER TABLE memories ADD COLUMN graph_extracted_hash TEXT",
|
|
2892
|
-
"ALTER TABLE memories ADD COLUMN confidence REAL DEFAULT 0.7",
|
|
2893
|
-
"ALTER TABLE memories ADD COLUMN last_accessed TEXT",
|
|
2894
|
-
// Wiki linkage columns (must match database.ts)
|
|
2895
|
-
"ALTER TABLE memories ADD COLUMN workspace_id TEXT",
|
|
2896
|
-
"ALTER TABLE memories ADD COLUMN document_id TEXT",
|
|
2897
|
-
"ALTER TABLE memories ADD COLUMN user_id TEXT",
|
|
2898
|
-
"ALTER TABLE memories ADD COLUMN char_offset INTEGER",
|
|
2899
|
-
"ALTER TABLE memories ADD COLUMN page_number INTEGER",
|
|
2900
|
-
// Source provenance columns (must match database.ts)
|
|
2901
|
-
"ALTER TABLE memories ADD COLUMN source_path TEXT",
|
|
2902
|
-
"ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'",
|
|
2903
|
-
"ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3",
|
|
2904
|
-
"ALTER TABLE memories ADD COLUMN supersedes_id TEXT",
|
|
2905
|
-
// MS-11: draft staging, MS-6a: memory_type, MS-7: trajectory
|
|
2906
|
-
"ALTER TABLE memories ADD COLUMN draft INTEGER DEFAULT 0",
|
|
2907
|
-
"ALTER TABLE memories ADD COLUMN memory_type TEXT DEFAULT 'raw'",
|
|
2908
|
-
"ALTER TABLE memories ADD COLUMN trajectory TEXT",
|
|
2909
|
-
// Metadata enrichment columns (must match database.ts)
|
|
2910
3746
|
"ALTER TABLE memories ADD COLUMN intent TEXT",
|
|
2911
3747
|
"ALTER TABLE memories ADD COLUMN outcome TEXT",
|
|
2912
3748
|
"ALTER TABLE memories ADD COLUMN domain TEXT",
|
|
@@ -2921,195 +3757,305 @@ async function ensureShardSchema(client) {
|
|
|
2921
3757
|
"ALTER TABLE memories ADD COLUMN token_cost REAL",
|
|
2922
3758
|
"ALTER TABLE memories ADD COLUMN audience TEXT",
|
|
2923
3759
|
"ALTER TABLE memories ADD COLUMN language_type TEXT",
|
|
2924
|
-
"ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
|
|
2925
|
-
"ALTER TABLE memories ADD COLUMN deleted_at TEXT"
|
|
3760
|
+
"ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
|
|
2926
3761
|
]) {
|
|
2927
3762
|
try {
|
|
2928
3763
|
await client.execute(col);
|
|
2929
3764
|
} catch {
|
|
2930
3765
|
}
|
|
2931
3766
|
}
|
|
2932
|
-
for (const idx of [
|
|
2933
|
-
"CREATE INDEX IF NOT EXISTS idx_memories_tier ON memories(tier)",
|
|
2934
|
-
"CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL",
|
|
2935
|
-
"CREATE INDEX IF NOT EXISTS idx_memories_scoped_content_hash ON memories(content_hash, agent_id, project_name, memory_type) WHERE content_hash IS NOT NULL"
|
|
2936
|
-
]) {
|
|
2937
|
-
try {
|
|
2938
|
-
await client.execute(idx);
|
|
2939
|
-
} catch {
|
|
2940
|
-
}
|
|
2941
|
-
}
|
|
2942
3767
|
try {
|
|
2943
|
-
await client.execute(
|
|
3768
|
+
await client.execute({
|
|
3769
|
+
sql: `UPDATE tasks SET status = 'closed' WHERE status = 'done' AND result IS NOT NULL`,
|
|
3770
|
+
args: []
|
|
3771
|
+
});
|
|
2944
3772
|
} catch {
|
|
2945
3773
|
}
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
try {
|
|
2952
|
-
await client.execute(idx);
|
|
2953
|
-
} catch {
|
|
2954
|
-
}
|
|
3774
|
+
}
|
|
3775
|
+
async function disposeDatabase() {
|
|
3776
|
+
if (_walCheckpointTimer) {
|
|
3777
|
+
clearInterval(_walCheckpointTimer);
|
|
3778
|
+
_walCheckpointTimer = null;
|
|
2955
3779
|
}
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
name TEXT NOT NULL,
|
|
2960
|
-
type TEXT NOT NULL,
|
|
2961
|
-
first_seen TEXT NOT NULL,
|
|
2962
|
-
last_seen TEXT NOT NULL,
|
|
2963
|
-
properties TEXT DEFAULT '{}',
|
|
2964
|
-
UNIQUE(name, type)
|
|
2965
|
-
);
|
|
2966
|
-
|
|
2967
|
-
CREATE TABLE IF NOT EXISTS relationships (
|
|
2968
|
-
id TEXT PRIMARY KEY,
|
|
2969
|
-
source_entity_id TEXT NOT NULL,
|
|
2970
|
-
target_entity_id TEXT NOT NULL,
|
|
2971
|
-
type TEXT NOT NULL,
|
|
2972
|
-
weight REAL DEFAULT 1.0,
|
|
2973
|
-
timestamp TEXT NOT NULL,
|
|
2974
|
-
properties TEXT DEFAULT '{}',
|
|
2975
|
-
UNIQUE(source_entity_id, target_entity_id, type)
|
|
2976
|
-
);
|
|
2977
|
-
|
|
2978
|
-
CREATE TABLE IF NOT EXISTS entity_memories (
|
|
2979
|
-
entity_id TEXT NOT NULL,
|
|
2980
|
-
memory_id TEXT NOT NULL,
|
|
2981
|
-
PRIMARY KEY (entity_id, memory_id)
|
|
2982
|
-
);
|
|
2983
|
-
|
|
2984
|
-
CREATE TABLE IF NOT EXISTS relationship_memories (
|
|
2985
|
-
relationship_id TEXT NOT NULL,
|
|
2986
|
-
memory_id TEXT NOT NULL,
|
|
2987
|
-
PRIMARY KEY (relationship_id, memory_id)
|
|
2988
|
-
);
|
|
2989
|
-
|
|
2990
|
-
CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
|
|
2991
|
-
CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
|
|
2992
|
-
CREATE INDEX IF NOT EXISTS idx_relationships_source ON relationships(source_entity_id);
|
|
2993
|
-
CREATE INDEX IF NOT EXISTS idx_relationships_target ON relationships(target_entity_id);
|
|
2994
|
-
CREATE INDEX IF NOT EXISTS idx_relationships_type ON relationships(type);
|
|
2995
|
-
|
|
2996
|
-
CREATE TABLE IF NOT EXISTS hyperedges (
|
|
2997
|
-
id TEXT PRIMARY KEY,
|
|
2998
|
-
label TEXT NOT NULL,
|
|
2999
|
-
relation TEXT NOT NULL,
|
|
3000
|
-
confidence REAL DEFAULT 1.0,
|
|
3001
|
-
timestamp TEXT NOT NULL
|
|
3002
|
-
);
|
|
3003
|
-
|
|
3004
|
-
CREATE TABLE IF NOT EXISTS hyperedge_nodes (
|
|
3005
|
-
hyperedge_id TEXT NOT NULL,
|
|
3006
|
-
entity_id TEXT NOT NULL,
|
|
3007
|
-
PRIMARY KEY (hyperedge_id, entity_id)
|
|
3008
|
-
);
|
|
3009
|
-
`);
|
|
3010
|
-
for (const col of [
|
|
3011
|
-
"ALTER TABLE relationships ADD COLUMN confidence REAL DEFAULT 1.0",
|
|
3012
|
-
"ALTER TABLE relationships ADD COLUMN confidence_label TEXT DEFAULT 'extracted'"
|
|
3013
|
-
]) {
|
|
3014
|
-
try {
|
|
3015
|
-
await client.execute(col);
|
|
3016
|
-
} catch {
|
|
3017
|
-
}
|
|
3780
|
+
if (_daemonClient) {
|
|
3781
|
+
_daemonClient.close();
|
|
3782
|
+
_daemonClient = null;
|
|
3018
3783
|
}
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
3028
|
-
if (!/SQLITE_NOTADB|file is not a database/i.test(message)) throw err;
|
|
3029
|
-
client.close();
|
|
3030
|
-
_shards.delete(safeName);
|
|
3031
|
-
_shardLastAccess.delete(safeName);
|
|
3032
|
-
const dbPath = path7.join(SHARDS_DIR, `${safeName}.db`);
|
|
3033
|
-
if (existsSync7(dbPath)) {
|
|
3034
|
-
const stat = statSync2(dbPath);
|
|
3035
|
-
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3036
|
-
const archivedPath = path7.join(SHARDS_DIR, `${safeName}.db.broken-${stamp}`);
|
|
3037
|
-
renameSync3(dbPath, archivedPath);
|
|
3038
|
-
process.stderr.write(
|
|
3039
|
-
`[shard-manager] Archived unreadable shard ${safeName}: ${archivedPath} (${stat.size} bytes, mtime ${stat.mtime.toISOString()})
|
|
3040
|
-
`
|
|
3041
|
-
);
|
|
3042
|
-
}
|
|
3043
|
-
client = getShardClient(projectName);
|
|
3044
|
-
await ensureShardSchema(client);
|
|
3045
|
-
return client;
|
|
3784
|
+
if (_adapterClient && _adapterClient !== _resilientClient) {
|
|
3785
|
+
_adapterClient.close();
|
|
3786
|
+
}
|
|
3787
|
+
_adapterClient = null;
|
|
3788
|
+
if (_client) {
|
|
3789
|
+
_client.close();
|
|
3790
|
+
_client = null;
|
|
3791
|
+
_resilientClient = null;
|
|
3046
3792
|
}
|
|
3047
3793
|
}
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3794
|
+
var _client, _resilientClient, _walCheckpointTimer, _daemonClient, _adapterClient, initTurso, SOFT_DELETE_RETENTION_DAYS, disposeTurso;
|
|
3795
|
+
var init_database = __esm({
|
|
3796
|
+
"src/lib/database.ts"() {
|
|
3797
|
+
"use strict";
|
|
3798
|
+
init_db_retry();
|
|
3799
|
+
init_employees();
|
|
3800
|
+
init_database_adapter();
|
|
3801
|
+
init_memory();
|
|
3802
|
+
_client = null;
|
|
3803
|
+
_resilientClient = null;
|
|
3804
|
+
_walCheckpointTimer = null;
|
|
3805
|
+
_daemonClient = null;
|
|
3806
|
+
_adapterClient = null;
|
|
3807
|
+
initTurso = initDatabase;
|
|
3808
|
+
SOFT_DELETE_RETENTION_DAYS = 7;
|
|
3809
|
+
disposeTurso = disposeDatabase;
|
|
3056
3810
|
}
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3811
|
+
});
|
|
3812
|
+
|
|
3813
|
+
// src/lib/state-bus.ts
|
|
3814
|
+
var StateBus, orgBus;
|
|
3815
|
+
var init_state_bus = __esm({
|
|
3816
|
+
"src/lib/state-bus.ts"() {
|
|
3817
|
+
"use strict";
|
|
3818
|
+
StateBus = class {
|
|
3819
|
+
handlers = /* @__PURE__ */ new Map();
|
|
3820
|
+
globalHandlers = /* @__PURE__ */ new Set();
|
|
3821
|
+
/** Emit an event to all subscribers */
|
|
3822
|
+
emit(event) {
|
|
3823
|
+
const typeHandlers = this.handlers.get(event.type);
|
|
3824
|
+
if (typeHandlers) {
|
|
3825
|
+
for (const handler of typeHandlers) {
|
|
3826
|
+
try {
|
|
3827
|
+
handler(event);
|
|
3828
|
+
} catch {
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
for (const handler of this.globalHandlers) {
|
|
3833
|
+
try {
|
|
3834
|
+
handler(event);
|
|
3835
|
+
} catch {
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
/** Subscribe to a specific event type */
|
|
3840
|
+
on(type, handler) {
|
|
3841
|
+
if (!this.handlers.has(type)) {
|
|
3842
|
+
this.handlers.set(type, /* @__PURE__ */ new Set());
|
|
3843
|
+
}
|
|
3844
|
+
this.handlers.get(type).add(handler);
|
|
3845
|
+
}
|
|
3846
|
+
/** Subscribe to ALL events */
|
|
3847
|
+
onAny(handler) {
|
|
3848
|
+
this.globalHandlers.add(handler);
|
|
3849
|
+
}
|
|
3850
|
+
/** Unsubscribe from a specific event type */
|
|
3851
|
+
off(type, handler) {
|
|
3852
|
+
this.handlers.get(type)?.delete(handler);
|
|
3853
|
+
}
|
|
3854
|
+
/** Unsubscribe from ALL events */
|
|
3855
|
+
offAny(handler) {
|
|
3856
|
+
this.globalHandlers.delete(handler);
|
|
3857
|
+
}
|
|
3858
|
+
/** Remove all listeners */
|
|
3859
|
+
clear() {
|
|
3860
|
+
this.handlers.clear();
|
|
3861
|
+
this.globalHandlers.clear();
|
|
3862
|
+
}
|
|
3863
|
+
};
|
|
3864
|
+
orgBus = new StateBus();
|
|
3064
3865
|
}
|
|
3866
|
+
});
|
|
3867
|
+
|
|
3868
|
+
// src/lib/memory-write-governor.ts
|
|
3869
|
+
import { createHash } from "crypto";
|
|
3870
|
+
function normalizeMemoryText(text) {
|
|
3871
|
+
return text.replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "").replace(/\n{4,}/g, "\n\n\n").trim();
|
|
3872
|
+
}
|
|
3873
|
+
function classifyMemoryType(input) {
|
|
3874
|
+
if (input.memory_type && input.memory_type.trim()) return input.memory_type.trim();
|
|
3875
|
+
const tool = input.tool_name.toLowerCase();
|
|
3876
|
+
const text = input.raw_text.toLowerCase();
|
|
3877
|
+
if (tool.includes("store_decision") || tool.includes("decision")) return "decision";
|
|
3878
|
+
if (tool.includes("commit") || text.includes("adr-") || text.includes("architectural decision")) return "adr";
|
|
3879
|
+
if (tool.includes("store_behavior") || tool.includes("behavior")) return "behavior";
|
|
3880
|
+
if (tool.includes("global_procedure") || text.includes("organization-wide procedures")) return "procedure";
|
|
3881
|
+
if (tool.includes("checkpoint") || text.startsWith("context checkpoint")) return "checkpoint";
|
|
3882
|
+
if (tool.includes("sessionsummary") || tool.includes("session-summary")) return "summary";
|
|
3883
|
+
if (tool.includes("sessionend") || text.startsWith("session ended")) return "summary";
|
|
3884
|
+
if (tool.includes("send_whatsapp") || tool.includes("conversation")) return "conversation";
|
|
3885
|
+
if (tool === "store_memory" || tool === "manual") return "observation";
|
|
3886
|
+
return "raw";
|
|
3887
|
+
}
|
|
3888
|
+
function shouldDropMemory(text) {
|
|
3889
|
+
const normalized = normalizeMemoryText(text);
|
|
3890
|
+
if (normalized.length < 10) return { drop: true, reason: "too_short" };
|
|
3891
|
+
if (NOISE_DROP_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
3892
|
+
return { drop: true, reason: "known_boilerplate_noise" };
|
|
3893
|
+
}
|
|
3894
|
+
return { drop: false };
|
|
3895
|
+
}
|
|
3896
|
+
function shouldSkipEmbedding(input) {
|
|
3897
|
+
const type = classifyMemoryType(input);
|
|
3898
|
+
if (HIGH_VALUE_SUPERSESSION_TYPES.has(type)) return false;
|
|
3899
|
+
if (type === "raw" && input.raw_text.length > 2e4) return true;
|
|
3900
|
+
if (SKIP_EMBED_PATTERNS.some((pattern) => pattern.test(input.raw_text))) return true;
|
|
3901
|
+
return false;
|
|
3065
3902
|
}
|
|
3066
|
-
function
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3903
|
+
function hashMemoryContent(text) {
|
|
3904
|
+
return createHash("sha256").update(normalizeMemoryText(text)).digest("hex");
|
|
3905
|
+
}
|
|
3906
|
+
function scopedDedupArgs(input) {
|
|
3907
|
+
return [input.contentHash, input.agentId, input.projectName, input.memoryType];
|
|
3908
|
+
}
|
|
3909
|
+
function governMemoryRecord(record) {
|
|
3910
|
+
const normalized = normalizeMemoryText(record.raw_text);
|
|
3911
|
+
const memoryType = classifyMemoryType({
|
|
3912
|
+
raw_text: normalized,
|
|
3913
|
+
agent_id: record.agent_id,
|
|
3914
|
+
project_name: record.project_name,
|
|
3915
|
+
tool_name: record.tool_name,
|
|
3916
|
+
memory_type: record.memory_type
|
|
3917
|
+
});
|
|
3918
|
+
const drop = shouldDropMemory(normalized);
|
|
3919
|
+
const skipEmbedding = shouldSkipEmbedding({
|
|
3920
|
+
raw_text: normalized,
|
|
3921
|
+
agent_id: record.agent_id,
|
|
3922
|
+
project_name: record.project_name,
|
|
3923
|
+
tool_name: record.tool_name,
|
|
3924
|
+
memory_type: memoryType
|
|
3925
|
+
});
|
|
3926
|
+
return {
|
|
3927
|
+
record: {
|
|
3928
|
+
...record,
|
|
3929
|
+
raw_text: normalized,
|
|
3930
|
+
memory_type: memoryType,
|
|
3931
|
+
vector: skipEmbedding ? null : record.vector
|
|
3932
|
+
},
|
|
3933
|
+
contentHash: hashMemoryContent(normalized),
|
|
3934
|
+
shouldDrop: drop.drop,
|
|
3935
|
+
dropReason: drop.reason,
|
|
3936
|
+
skipEmbedding,
|
|
3937
|
+
hygiene: {
|
|
3938
|
+
dedup: true,
|
|
3939
|
+
supersession: HIGH_VALUE_SUPERSESSION_TYPES.has(memoryType)
|
|
3072
3940
|
}
|
|
3073
|
-
}
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3941
|
+
};
|
|
3942
|
+
}
|
|
3943
|
+
async function findScopedDuplicate(input) {
|
|
3944
|
+
const { getClient: getClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
|
|
3945
|
+
const client = getClient2();
|
|
3946
|
+
const args = scopedDedupArgs(input);
|
|
3947
|
+
let sql = `SELECT id FROM memories
|
|
3948
|
+
WHERE content_hash = ?
|
|
3949
|
+
AND agent_id = ?
|
|
3950
|
+
AND project_name = ?
|
|
3951
|
+
AND COALESCE(memory_type, 'raw') = ?
|
|
3952
|
+
AND COALESCE(status, 'active') != 'deleted'`;
|
|
3953
|
+
if (input.excludeId) {
|
|
3954
|
+
sql += " AND id != ?";
|
|
3955
|
+
args.push(input.excludeId);
|
|
3956
|
+
}
|
|
3957
|
+
sql += " ORDER BY timestamp DESC LIMIT 1";
|
|
3958
|
+
const result = await client.execute({ sql, args });
|
|
3959
|
+
return result.rows[0]?.id ? String(result.rows[0].id) : null;
|
|
3960
|
+
}
|
|
3961
|
+
async function runPostWriteMemoryHygiene(memoryId) {
|
|
3962
|
+
try {
|
|
3963
|
+
const { getClient: getClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
|
|
3964
|
+
const client = getClient2();
|
|
3965
|
+
const current = await client.execute({
|
|
3966
|
+
sql: `SELECT id, agent_id, project_name, memory_type, content_hash, supersedes_id,
|
|
3967
|
+
importance, timestamp
|
|
3968
|
+
FROM memories
|
|
3969
|
+
WHERE id = ?
|
|
3970
|
+
LIMIT 1`,
|
|
3971
|
+
args: [memoryId]
|
|
3972
|
+
});
|
|
3973
|
+
const row = current.rows[0];
|
|
3974
|
+
if (!row) return;
|
|
3975
|
+
const memoryType = String(row.memory_type ?? "raw");
|
|
3976
|
+
const contentHash = row.content_hash ? String(row.content_hash) : null;
|
|
3977
|
+
const agentId = String(row.agent_id);
|
|
3978
|
+
const projectName = String(row.project_name);
|
|
3979
|
+
if (contentHash) {
|
|
3980
|
+
await client.execute({
|
|
3981
|
+
sql: `UPDATE memories
|
|
3982
|
+
SET status = 'deleted',
|
|
3983
|
+
outcome = COALESCE(outcome, 'superseded')
|
|
3984
|
+
WHERE id != ?
|
|
3985
|
+
AND content_hash = ?
|
|
3986
|
+
AND agent_id = ?
|
|
3987
|
+
AND project_name = ?
|
|
3988
|
+
AND COALESCE(memory_type, 'raw') = ?
|
|
3989
|
+
AND COALESCE(status, 'active') = 'active'`,
|
|
3990
|
+
args: [memoryId, contentHash, agentId, projectName, memoryType]
|
|
3991
|
+
});
|
|
3078
3992
|
}
|
|
3079
|
-
|
|
3080
|
-
|
|
3993
|
+
const supersedesId = row.supersedes_id ? String(row.supersedes_id) : null;
|
|
3994
|
+
if (supersedesId && HIGH_VALUE_SUPERSESSION_TYPES.has(memoryType)) {
|
|
3995
|
+
const old = await client.execute({
|
|
3996
|
+
sql: `SELECT importance FROM memories WHERE id = ? LIMIT 1`,
|
|
3997
|
+
args: [supersedesId]
|
|
3998
|
+
});
|
|
3999
|
+
const oldImportance = Number(old.rows[0]?.importance ?? 0);
|
|
4000
|
+
const newImportance = Number(row.importance ?? 0);
|
|
4001
|
+
await client.batch([
|
|
4002
|
+
{
|
|
4003
|
+
sql: `UPDATE memories
|
|
4004
|
+
SET status = 'archived',
|
|
4005
|
+
outcome = COALESCE(outcome, 'superseded')
|
|
4006
|
+
WHERE id = ?`,
|
|
4007
|
+
args: [supersedesId]
|
|
4008
|
+
},
|
|
4009
|
+
{
|
|
4010
|
+
sql: `UPDATE memories
|
|
4011
|
+
SET importance = MAX(COALESCE(importance, 5), ?),
|
|
4012
|
+
parent_memory_id = COALESCE(parent_memory_id, ?)
|
|
4013
|
+
WHERE id = ?`,
|
|
4014
|
+
args: [Math.max(oldImportance, newImportance), supersedesId, memoryId]
|
|
4015
|
+
}
|
|
4016
|
+
], "write");
|
|
4017
|
+
}
|
|
4018
|
+
} catch (err) {
|
|
4019
|
+
process.stderr.write(
|
|
4020
|
+
`[memory-governor] post-write hygiene failed for ${memoryId}: ${err instanceof Error ? err.message : String(err)}
|
|
4021
|
+
`
|
|
4022
|
+
);
|
|
3081
4023
|
}
|
|
3082
4024
|
}
|
|
3083
|
-
function
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
for (const [, client] of _shards) {
|
|
3092
|
-
client.close();
|
|
3093
|
-
}
|
|
3094
|
-
_shards.clear();
|
|
3095
|
-
_shardLastAccess.clear();
|
|
3096
|
-
_shardingEnabled = false;
|
|
3097
|
-
_encryptionKey = null;
|
|
4025
|
+
function schedulePostWriteMemoryHygiene(memoryIds) {
|
|
4026
|
+
if (process.env.EXE_SKIP_MEMORY_HYGIENE === "1") return;
|
|
4027
|
+
if (memoryIds.length === 0) return;
|
|
4028
|
+
const run = () => {
|
|
4029
|
+
void Promise.all(memoryIds.map((id) => runPostWriteMemoryHygiene(id)));
|
|
4030
|
+
};
|
|
4031
|
+
if (typeof setImmediate === "function") setImmediate(run);
|
|
4032
|
+
else setTimeout(run, 0);
|
|
3098
4033
|
}
|
|
3099
|
-
var
|
|
3100
|
-
var
|
|
3101
|
-
"src/lib/
|
|
4034
|
+
var HIGH_VALUE_SUPERSESSION_TYPES, NOISE_DROP_PATTERNS, SKIP_EMBED_PATTERNS;
|
|
4035
|
+
var init_memory_write_governor = __esm({
|
|
4036
|
+
"src/lib/memory-write-governor.ts"() {
|
|
3102
4037
|
"use strict";
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
4038
|
+
HIGH_VALUE_SUPERSESSION_TYPES = /* @__PURE__ */ new Set([
|
|
4039
|
+
"decision",
|
|
4040
|
+
"adr",
|
|
4041
|
+
"behavior",
|
|
4042
|
+
"procedure"
|
|
4043
|
+
]);
|
|
4044
|
+
NOISE_DROP_PATTERNS = [
|
|
4045
|
+
/^\s*\[📋\s+\d+\s+reviews?\s+pending\b/im,
|
|
4046
|
+
/^\s*<system-reminder>[\s\S]*?<\/system-reminder>\s*$/im,
|
|
4047
|
+
/^\s*The UserPromptSubmit hook checks the DB for new tasks/im,
|
|
4048
|
+
/^\s*Intercom is a speedup, not delivery/im,
|
|
4049
|
+
/^\s*Context bar reads as USAGE not remaining/im
|
|
4050
|
+
];
|
|
4051
|
+
SKIP_EMBED_PATTERNS = [
|
|
4052
|
+
/tmux capture-pane\b/i,
|
|
4053
|
+
/docker ps\b/i,
|
|
4054
|
+
/docker images\b/i,
|
|
4055
|
+
/git status\b/i,
|
|
4056
|
+
/grep .*node_modules/i,
|
|
4057
|
+
/npm (install|ci)\b[\s\S]*(added \d+ packages|audited \d+ packages)/i
|
|
4058
|
+
];
|
|
3113
4059
|
}
|
|
3114
4060
|
});
|
|
3115
4061
|
|
|
@@ -3151,6 +4097,12 @@ var init_platform_procedures = __esm({
|
|
|
3151
4097
|
priority: "p0",
|
|
3152
4098
|
content: "Founder -> coordinator (the executive agent, internally routed as 'COO') -> CTO/CMO. CTO -> engineers. CMO -> content production. Never skip levels: the coordinator does not bypass managers for specialist work. Specialists report to their manager. If you need cross-team info, use ask_team_memory \u2014 don't read other agents' task folders. Each level owns dispatch downward and review upward."
|
|
3153
4099
|
},
|
|
4100
|
+
{
|
|
4101
|
+
title: "Customer orchestration maturity \u2014 recommend, never trap",
|
|
4102
|
+
domain: "workflow",
|
|
4103
|
+
priority: "p1",
|
|
4104
|
+
content: "New customers start best in Phase 1: founder \u2194 coordinator/Chief of Staff, building company context. Suggest Phase 2 executives when domain work repeats; suggest Phase 3 parallel execution only when review/permission gates are ready. This is guidance, not a blocker: users may jump phases anytime. Never overwrite their phase, role titles, identities, or custom org design."
|
|
4105
|
+
},
|
|
3154
4106
|
{
|
|
3155
4107
|
title: "Single dispatch path \u2014 create_task only",
|
|
3156
4108
|
domain: "workflow",
|
|
@@ -3209,6 +4161,12 @@ var init_platform_procedures = __esm({
|
|
|
3209
4161
|
priority: "p0",
|
|
3210
4162
|
content: "exe-build-adv is MANDATORY for ALL work touching 3+ files. Run /exe-build-adv --auto BEFORE implementation. Pipeline: Spec \u2192 AC \u2192 Tests \u2192 Evaluate \u2192 Fix. No multi-file feature ships without pipeline artifacts. No exceptions \u2014 managers reject work without them."
|
|
3211
4163
|
},
|
|
4164
|
+
{
|
|
4165
|
+
title: "Commit discipline \u2014 never leave verified work floating",
|
|
4166
|
+
domain: "workflow",
|
|
4167
|
+
priority: "p1",
|
|
4168
|
+
content: "After any code-change batch passes typecheck/tests/build, run git status, summarize changed files, and commit with a clear message before ending the session. If work must remain uncommitted for review/dogfood, explicitly say so, list the files, and state the blocker. Never imply work is complete while verified changes are still floating locally."
|
|
4169
|
+
},
|
|
3212
4170
|
{
|
|
3213
4171
|
title: "Desktop and TUI are the same product",
|
|
3214
4172
|
domain: "architecture",
|
|
@@ -3368,595 +4326,969 @@ ${p.content}`).join("\n\n");
|
|
|
3368
4326
|
}
|
|
3369
4327
|
});
|
|
3370
4328
|
|
|
3371
|
-
// src/lib/
|
|
3372
|
-
var
|
|
3373
|
-
__export(
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
releaseBackfillLock: () => releaseBackfillLock,
|
|
3378
|
-
tryAcquireBackfillLock: () => tryAcquireBackfillLock,
|
|
3379
|
-
tryAcquireWorkerSlot: () => tryAcquireWorkerSlot
|
|
3380
|
-
});
|
|
3381
|
-
import { readdirSync as readdirSync2, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, mkdirSync as mkdirSync3, existsSync as existsSync8 } from "fs";
|
|
3382
|
-
import path8 from "path";
|
|
3383
|
-
function tryAcquireWorkerSlot() {
|
|
3384
|
-
try {
|
|
3385
|
-
mkdirSync3(WORKER_PID_DIR, { recursive: true });
|
|
3386
|
-
const reservationId = `res-${process.pid}-${Date.now()}`;
|
|
3387
|
-
const reservationPath = path8.join(WORKER_PID_DIR, `${reservationId}.pid`);
|
|
3388
|
-
writeFileSync3(reservationPath, String(process.pid));
|
|
3389
|
-
const files = readdirSync2(WORKER_PID_DIR);
|
|
3390
|
-
let alive = 0;
|
|
3391
|
-
for (const f of files) {
|
|
3392
|
-
if (!f.endsWith(".pid")) continue;
|
|
3393
|
-
if (f.startsWith("res-")) {
|
|
3394
|
-
alive++;
|
|
3395
|
-
continue;
|
|
3396
|
-
}
|
|
3397
|
-
const dashIdx = f.lastIndexOf("-");
|
|
3398
|
-
const pid = parseInt(f.slice(dashIdx + 1).replace(".pid", ""), 10);
|
|
3399
|
-
if (isNaN(pid)) continue;
|
|
3400
|
-
try {
|
|
3401
|
-
process.kill(pid, 0);
|
|
3402
|
-
alive++;
|
|
3403
|
-
} catch {
|
|
3404
|
-
try {
|
|
3405
|
-
unlinkSync3(path8.join(WORKER_PID_DIR, f));
|
|
3406
|
-
} catch {
|
|
3407
|
-
}
|
|
3408
|
-
}
|
|
3409
|
-
}
|
|
3410
|
-
if (alive > MAX_CONCURRENT_WORKERS) {
|
|
3411
|
-
try {
|
|
3412
|
-
unlinkSync3(reservationPath);
|
|
3413
|
-
} catch {
|
|
3414
|
-
}
|
|
3415
|
-
return false;
|
|
3416
|
-
}
|
|
3417
|
-
try {
|
|
3418
|
-
unlinkSync3(reservationPath);
|
|
3419
|
-
} catch {
|
|
3420
|
-
}
|
|
3421
|
-
return true;
|
|
3422
|
-
} catch {
|
|
3423
|
-
return true;
|
|
3424
|
-
}
|
|
3425
|
-
}
|
|
3426
|
-
function registerWorkerPid(pid) {
|
|
3427
|
-
try {
|
|
3428
|
-
mkdirSync3(WORKER_PID_DIR, { recursive: true });
|
|
3429
|
-
writeFileSync3(path8.join(WORKER_PID_DIR, `worker-${pid}.pid`), String(pid));
|
|
3430
|
-
} catch {
|
|
3431
|
-
}
|
|
3432
|
-
}
|
|
3433
|
-
function cleanupWorkerPid() {
|
|
3434
|
-
try {
|
|
3435
|
-
unlinkSync3(path8.join(WORKER_PID_DIR, `worker-${process.pid}.pid`));
|
|
3436
|
-
} catch {
|
|
3437
|
-
}
|
|
3438
|
-
}
|
|
3439
|
-
function tryAcquireBackfillLock() {
|
|
3440
|
-
try {
|
|
3441
|
-
mkdirSync3(WORKER_PID_DIR, { recursive: true });
|
|
3442
|
-
if (existsSync8(BACKFILL_LOCK)) {
|
|
3443
|
-
try {
|
|
3444
|
-
const pid = parseInt(
|
|
3445
|
-
__require("fs").readFileSync(BACKFILL_LOCK, "utf8").trim(),
|
|
3446
|
-
10
|
|
3447
|
-
);
|
|
3448
|
-
if (!isNaN(pid) && pid > 0) {
|
|
3449
|
-
try {
|
|
3450
|
-
process.kill(pid, 0);
|
|
3451
|
-
return false;
|
|
3452
|
-
} catch {
|
|
3453
|
-
}
|
|
3454
|
-
}
|
|
3455
|
-
} catch {
|
|
3456
|
-
}
|
|
3457
|
-
}
|
|
3458
|
-
writeFileSync3(BACKFILL_LOCK, String(process.pid));
|
|
3459
|
-
return true;
|
|
3460
|
-
} catch {
|
|
3461
|
-
return true;
|
|
3462
|
-
}
|
|
3463
|
-
}
|
|
3464
|
-
function releaseBackfillLock() {
|
|
3465
|
-
try {
|
|
3466
|
-
unlinkSync3(BACKFILL_LOCK);
|
|
3467
|
-
} catch {
|
|
3468
|
-
}
|
|
3469
|
-
}
|
|
3470
|
-
var WORKER_PID_DIR, MAX_CONCURRENT_WORKERS, BACKFILL_LOCK;
|
|
3471
|
-
var init_worker_gate = __esm({
|
|
3472
|
-
"src/lib/worker-gate.ts"() {
|
|
3473
|
-
"use strict";
|
|
3474
|
-
init_config();
|
|
3475
|
-
WORKER_PID_DIR = path8.join(EXE_AI_DIR, "worker-pids");
|
|
3476
|
-
MAX_CONCURRENT_WORKERS = 3;
|
|
3477
|
-
BACKFILL_LOCK = path8.join(WORKER_PID_DIR, "backfill.lock");
|
|
3478
|
-
}
|
|
3479
|
-
});
|
|
3480
|
-
|
|
3481
|
-
// src/lib/db-backup.ts
|
|
3482
|
-
var db_backup_exports = {};
|
|
3483
|
-
__export(db_backup_exports, {
|
|
3484
|
-
createBackup: () => createBackup,
|
|
3485
|
-
findActiveDb: () => findActiveDb,
|
|
3486
|
-
getBackupDir: () => getBackupDir,
|
|
3487
|
-
getLatestBackup: () => getLatestBackup,
|
|
3488
|
-
hasBackupToday: () => hasBackupToday,
|
|
3489
|
-
listBackups: () => listBackups,
|
|
3490
|
-
rotateBackups: () => rotateBackups
|
|
4329
|
+
// src/lib/memory-cards.ts
|
|
4330
|
+
var memory_cards_exports = {};
|
|
4331
|
+
__export(memory_cards_exports, {
|
|
4332
|
+
extractMemoryCards: () => extractMemoryCards,
|
|
4333
|
+
insertMemoryCardsForBatch: () => insertMemoryCardsForBatch,
|
|
4334
|
+
searchMemoryCards: () => searchMemoryCards
|
|
3491
4335
|
});
|
|
3492
|
-
import {
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
const stat = statSync3(p);
|
|
3555
|
-
return { path: p, name, size: stat.size, date: stat.mtime };
|
|
3556
|
-
}).sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
3557
|
-
} catch {
|
|
3558
|
-
return [];
|
|
4336
|
+
import { createHash as createHash2 } from "crypto";
|
|
4337
|
+
function stableId(memoryId, type, content) {
|
|
4338
|
+
return createHash2("sha256").update(`${memoryId}:${type}:${content}`).digest("hex").slice(0, 32);
|
|
4339
|
+
}
|
|
4340
|
+
function cleanText(text) {
|
|
4341
|
+
return text.replace(/```[\s\S]*?```/g, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
4342
|
+
}
|
|
4343
|
+
function splitSentences(text) {
|
|
4344
|
+
return cleanText(text).split(/(?<=[.!?])\s+|\n+/).map((s) => s.trim()).filter((s) => s.length >= 24 && s.length <= MAX_SENTENCE_CHARS);
|
|
4345
|
+
}
|
|
4346
|
+
function inferCardType(sentence, toolName) {
|
|
4347
|
+
const lower = sentence.toLowerCase();
|
|
4348
|
+
if (toolName === "store_decision" || /\b(decided|decision|adr|approved|rejected)\b/.test(lower)) return "decision";
|
|
4349
|
+
if (/\b(prefers|preference|likes|dislikes|wants|doesn't want|does not want)\b/.test(lower)) return "preference";
|
|
4350
|
+
if (/\b(changed|updated|replaced|now|no longer|instead|supersedes)\b/.test(lower)) return "belief_update";
|
|
4351
|
+
if (toolName && ["Read", "Write", "Edit", "Bash"].includes(toolName)) return "code";
|
|
4352
|
+
if (/\b(meeting|deadline|shipped|launched|completed|failed|blocked|assigned|created)\b/.test(lower)) return "event";
|
|
4353
|
+
return "fact";
|
|
4354
|
+
}
|
|
4355
|
+
function extractSubject(sentence, agentId) {
|
|
4356
|
+
const explicit = sentence.match(/\b([A-Z][a-zA-Z0-9_-]{2,}(?:\s+[A-Z][a-zA-Z0-9_-]{2,})?)\b/);
|
|
4357
|
+
return explicit?.[1] ?? agentId;
|
|
4358
|
+
}
|
|
4359
|
+
function predicateFor(type) {
|
|
4360
|
+
switch (type) {
|
|
4361
|
+
case "preference":
|
|
4362
|
+
return "prefers";
|
|
4363
|
+
case "belief_update":
|
|
4364
|
+
return "updated";
|
|
4365
|
+
case "decision":
|
|
4366
|
+
return "decided";
|
|
4367
|
+
case "event":
|
|
4368
|
+
return "happened";
|
|
4369
|
+
case "code":
|
|
4370
|
+
return "implemented";
|
|
4371
|
+
default:
|
|
4372
|
+
return "states";
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
4375
|
+
function extractMemoryCards(row) {
|
|
4376
|
+
const sentences = splitSentences(row.raw_text);
|
|
4377
|
+
const cards = [];
|
|
4378
|
+
for (const sentence of sentences) {
|
|
4379
|
+
const type = inferCardType(sentence, row.tool_name);
|
|
4380
|
+
const subject = extractSubject(sentence, row.agent_id);
|
|
4381
|
+
const content = sentence.length > MAX_SENTENCE_CHARS ? `${sentence.slice(0, MAX_SENTENCE_CHARS - 1)}\u2026` : sentence;
|
|
4382
|
+
cards.push({
|
|
4383
|
+
id: stableId(row.id, type, content),
|
|
4384
|
+
memory_id: row.id,
|
|
4385
|
+
agent_id: row.agent_id,
|
|
4386
|
+
session_id: row.session_id,
|
|
4387
|
+
project_name: row.project_name ?? null,
|
|
4388
|
+
timestamp: row.timestamp,
|
|
4389
|
+
card_type: type,
|
|
4390
|
+
subject,
|
|
4391
|
+
predicate: predicateFor(type),
|
|
4392
|
+
object: content,
|
|
4393
|
+
content,
|
|
4394
|
+
source_ref: row.id,
|
|
4395
|
+
confidence: type === "fact" ? 0.55 : 0.65
|
|
4396
|
+
});
|
|
4397
|
+
if (cards.length >= MAX_CARDS_PER_MEMORY) break;
|
|
3559
4398
|
}
|
|
4399
|
+
return cards;
|
|
3560
4400
|
}
|
|
3561
|
-
function
|
|
3562
|
-
const
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
4401
|
+
async function insertMemoryCardsForBatch(rows) {
|
|
4402
|
+
const cards = rows.flatMap(extractMemoryCards);
|
|
4403
|
+
if (cards.length === 0) return 0;
|
|
4404
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4405
|
+
const client = getClient();
|
|
4406
|
+
const stmts = cards.map((card) => ({
|
|
4407
|
+
sql: `INSERT OR IGNORE INTO memory_cards
|
|
4408
|
+
(id, memory_id, agent_id, session_id, project_name, timestamp, card_type,
|
|
4409
|
+
subject, predicate, object, content, source_ref, confidence, active, created_at)
|
|
4410
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`,
|
|
4411
|
+
args: [
|
|
4412
|
+
card.id,
|
|
4413
|
+
card.memory_id,
|
|
4414
|
+
card.agent_id,
|
|
4415
|
+
card.session_id,
|
|
4416
|
+
card.project_name,
|
|
4417
|
+
card.timestamp,
|
|
4418
|
+
card.card_type,
|
|
4419
|
+
card.subject,
|
|
4420
|
+
card.predicate,
|
|
4421
|
+
card.object,
|
|
4422
|
+
card.content,
|
|
4423
|
+
card.source_ref,
|
|
4424
|
+
card.confidence,
|
|
4425
|
+
now
|
|
4426
|
+
]
|
|
4427
|
+
}));
|
|
4428
|
+
await client.batch(stmts, "write");
|
|
4429
|
+
return cards.length;
|
|
4430
|
+
}
|
|
4431
|
+
function buildMatchExpr(queryText) {
|
|
4432
|
+
const terms = queryText.toLowerCase().split(/\s+/).filter((t) => t.length >= 3).map((t) => t.replace(/[^a-z0-9_]/g, "")).filter((t) => t.length >= 3).slice(0, 12);
|
|
4433
|
+
if (terms.length === 0) return null;
|
|
4434
|
+
return terms.map((t) => `${t}*`).join(terms.length >= 3 ? " AND " : " OR ");
|
|
4435
|
+
}
|
|
4436
|
+
async function searchMemoryCards(queryText, agentId, options) {
|
|
4437
|
+
const limit = options?.limit ?? 10;
|
|
4438
|
+
const matchExpr = buildMatchExpr(queryText);
|
|
4439
|
+
if (!matchExpr) return [];
|
|
4440
|
+
let sql = `SELECT c.id, c.memory_id, c.agent_id, c.session_id, c.project_name,
|
|
4441
|
+
c.timestamp, c.card_type, c.content, c.source_ref, c.confidence
|
|
4442
|
+
FROM memory_cards c
|
|
4443
|
+
JOIN memory_cards_fts fts ON c.rowid = fts.rowid
|
|
4444
|
+
WHERE memory_cards_fts MATCH ?
|
|
4445
|
+
AND c.agent_id = ?
|
|
4446
|
+
AND COALESCE(c.active, 1) = 1`;
|
|
4447
|
+
const args = [matchExpr, agentId];
|
|
4448
|
+
if (options?.projectName) {
|
|
4449
|
+
sql += ` AND c.project_name = ?`;
|
|
4450
|
+
args.push(options.projectName);
|
|
4451
|
+
}
|
|
4452
|
+
if (options?.since) {
|
|
4453
|
+
sql += ` AND c.timestamp >= ?`;
|
|
4454
|
+
args.push(options.since);
|
|
4455
|
+
}
|
|
4456
|
+
sql += ` ORDER BY rank LIMIT ?`;
|
|
4457
|
+
args.push(limit);
|
|
4458
|
+
const result = await getClient().execute({ sql, args });
|
|
4459
|
+
return result.rows.map((row) => ({
|
|
4460
|
+
id: `card:${String(row.id)}`,
|
|
4461
|
+
agent_id: String(row.agent_id),
|
|
4462
|
+
agent_role: "memory_card",
|
|
4463
|
+
session_id: String(row.session_id),
|
|
4464
|
+
timestamp: String(row.timestamp),
|
|
4465
|
+
tool_name: `memory_card:${String(row.card_type)}`,
|
|
4466
|
+
project_name: row.project_name == null ? "" : String(row.project_name),
|
|
4467
|
+
has_error: false,
|
|
4468
|
+
raw_text: `[${String(row.card_type)}] ${String(row.content)}
|
|
4469
|
+
Source memory: ${String(row.source_ref ?? row.memory_id)}`,
|
|
4470
|
+
vector: [],
|
|
4471
|
+
importance: 6,
|
|
4472
|
+
status: "active",
|
|
4473
|
+
confidence: Number(row.confidence ?? 0.6),
|
|
4474
|
+
last_accessed: String(row.timestamp)
|
|
4475
|
+
}));
|
|
3572
4476
|
}
|
|
3573
|
-
var
|
|
3574
|
-
var
|
|
3575
|
-
"src/lib/
|
|
4477
|
+
var MAX_CARDS_PER_MEMORY, MAX_SENTENCE_CHARS;
|
|
4478
|
+
var init_memory_cards = __esm({
|
|
4479
|
+
"src/lib/memory-cards.ts"() {
|
|
3576
4480
|
"use strict";
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
DB_NAMES = ["memories.db", "exe-mem.db", "exe-os.db", "exe.db"];
|
|
4481
|
+
init_database();
|
|
4482
|
+
MAX_CARDS_PER_MEMORY = 6;
|
|
4483
|
+
MAX_SENTENCE_CHARS = 360;
|
|
3581
4484
|
}
|
|
3582
4485
|
});
|
|
3583
4486
|
|
|
3584
|
-
// src/bin/exe-doctor.ts
|
|
3585
|
-
import os6 from "os";
|
|
3586
|
-
|
|
3587
4487
|
// src/lib/store.ts
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
}
|
|
3605
|
-
function
|
|
3606
|
-
if (
|
|
3607
|
-
|
|
3608
|
-
return
|
|
3609
|
-
`security find-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w 2>/dev/null`,
|
|
3610
|
-
{ encoding: "utf-8", timeout: 5e3 }
|
|
3611
|
-
).trim();
|
|
3612
|
-
} catch {
|
|
3613
|
-
return null;
|
|
4488
|
+
var store_exports = {};
|
|
4489
|
+
__export(store_exports, {
|
|
4490
|
+
attachDocumentMetadata: () => attachDocumentMetadata,
|
|
4491
|
+
buildRawVisibilityFilter: () => buildRawVisibilityFilter,
|
|
4492
|
+
buildWikiScopeFilter: () => buildWikiScopeFilter,
|
|
4493
|
+
classifyTier: () => classifyTier,
|
|
4494
|
+
disposeStore: () => disposeStore,
|
|
4495
|
+
flushBatch: () => flushBatch,
|
|
4496
|
+
flushTier3: () => flushTier3,
|
|
4497
|
+
getMemoryCardinality: () => getMemoryCardinality,
|
|
4498
|
+
initStore: () => initStore,
|
|
4499
|
+
reserveVersions: () => reserveVersions,
|
|
4500
|
+
searchMemories: () => searchMemories,
|
|
4501
|
+
updateMemoryStatus: () => updateMemoryStatus,
|
|
4502
|
+
vectorToBlob: () => vectorToBlob,
|
|
4503
|
+
writeMemory: () => writeMemory
|
|
4504
|
+
});
|
|
4505
|
+
function isBusyError2(err) {
|
|
4506
|
+
if (err instanceof Error) {
|
|
4507
|
+
const msg = err.message.toLowerCase();
|
|
4508
|
+
return msg.includes("sqlite_busy") || msg.includes("database is locked");
|
|
3614
4509
|
}
|
|
4510
|
+
return false;
|
|
3615
4511
|
}
|
|
3616
|
-
function
|
|
3617
|
-
|
|
3618
|
-
try {
|
|
4512
|
+
async function retryOnBusy2(fn, label) {
|
|
4513
|
+
for (let attempt = 0; attempt <= INIT_MAX_RETRIES; attempt++) {
|
|
3619
4514
|
try {
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
4515
|
+
return await fn();
|
|
4516
|
+
} catch (err) {
|
|
4517
|
+
if (!isBusyError2(err) || attempt === INIT_MAX_RETRIES) throw err;
|
|
4518
|
+
process.stderr.write(
|
|
4519
|
+
`[store] SQLITE_BUSY during ${label}, retry ${attempt + 1}/${INIT_MAX_RETRIES}
|
|
4520
|
+
`
|
|
3623
4521
|
);
|
|
3624
|
-
|
|
4522
|
+
await new Promise((r) => setTimeout(r, INIT_RETRY_DELAY_MS * (attempt + 1)));
|
|
3625
4523
|
}
|
|
3626
|
-
execSync2(
|
|
3627
|
-
`security add-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w "${value}"`,
|
|
3628
|
-
{ timeout: 5e3 }
|
|
3629
|
-
);
|
|
3630
|
-
return true;
|
|
3631
|
-
} catch {
|
|
3632
|
-
return false;
|
|
3633
4524
|
}
|
|
4525
|
+
throw new Error("unreachable");
|
|
3634
4526
|
}
|
|
3635
|
-
function
|
|
3636
|
-
if (
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
`secret-tool lookup service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
|
|
3640
|
-
{ encoding: "utf-8", timeout: 5e3 }
|
|
3641
|
-
).trim();
|
|
3642
|
-
} catch {
|
|
3643
|
-
return null;
|
|
4527
|
+
async function initStore(options) {
|
|
4528
|
+
if (_flushTimer !== null) {
|
|
4529
|
+
clearInterval(_flushTimer);
|
|
4530
|
+
_flushTimer = null;
|
|
3644
4531
|
}
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
return true;
|
|
3654
|
-
} catch {
|
|
3655
|
-
return false;
|
|
4532
|
+
_pendingRecords = [];
|
|
4533
|
+
_flushing = false;
|
|
4534
|
+
_batchSize = options?.batchSize ?? 20;
|
|
4535
|
+
_flushIntervalMs = options?.flushIntervalMs ?? 1e4;
|
|
4536
|
+
let dbPath = options?.dbPath;
|
|
4537
|
+
if (!dbPath) {
|
|
4538
|
+
const config = await loadConfig();
|
|
4539
|
+
dbPath = config.dbPath;
|
|
3656
4540
|
}
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
4541
|
+
let masterKey = options?.masterKey ?? null;
|
|
4542
|
+
if (!masterKey) {
|
|
4543
|
+
masterKey = await getMasterKey();
|
|
4544
|
+
if (!masterKey) {
|
|
4545
|
+
throw new Error(
|
|
4546
|
+
"No encryption key found. Run /exe-setup to generate one."
|
|
4547
|
+
);
|
|
4548
|
+
}
|
|
3663
4549
|
}
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
4550
|
+
const hexKey = masterKey.toString("hex");
|
|
4551
|
+
await initTurso({
|
|
4552
|
+
dbPath,
|
|
4553
|
+
encryptionKey: hexKey
|
|
4554
|
+
});
|
|
4555
|
+
await retryOnBusy2(() => ensureSchema(), "ensureSchema");
|
|
3667
4556
|
try {
|
|
3668
|
-
const
|
|
3669
|
-
|
|
3670
|
-
os5.hostname(),
|
|
3671
|
-
os5.userInfo().username,
|
|
3672
|
-
os5.arch(),
|
|
3673
|
-
os5.platform(),
|
|
3674
|
-
// Machine ID on Linux (stable across reboots)
|
|
3675
|
-
process.platform === "linux" ? readMachineId() : ""
|
|
3676
|
-
].join("|");
|
|
3677
|
-
return crypto2.createHash("sha256").update(material).digest();
|
|
4557
|
+
const { initDaemonClient: initDaemonClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
|
|
4558
|
+
await initDaemonClient2();
|
|
3678
4559
|
} catch {
|
|
3679
|
-
return null;
|
|
3680
4560
|
}
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
4561
|
+
if (!options?.lightweight) {
|
|
4562
|
+
try {
|
|
4563
|
+
const { initShardManager: initShardManager2 } = await Promise.resolve().then(() => (init_shard_manager(), shard_manager_exports));
|
|
4564
|
+
initShardManager2(hexKey);
|
|
4565
|
+
} catch {
|
|
4566
|
+
}
|
|
4567
|
+
const client = getClient();
|
|
4568
|
+
const vResult = await retryOnBusy2(
|
|
4569
|
+
() => client.execute("SELECT MAX(version) as max_v FROM memories"),
|
|
4570
|
+
"version-query"
|
|
4571
|
+
);
|
|
4572
|
+
_nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
|
|
4573
|
+
try {
|
|
4574
|
+
const { loadGlobalProcedures: loadGlobalProcedures2 } = await Promise.resolve().then(() => (init_global_procedures(), global_procedures_exports));
|
|
4575
|
+
await loadGlobalProcedures2();
|
|
4576
|
+
} catch {
|
|
4577
|
+
}
|
|
3688
4578
|
}
|
|
3689
4579
|
}
|
|
3690
|
-
function
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
4580
|
+
function classifyTier(record) {
|
|
4581
|
+
if (record.tool_name === "commit_to_long_term_memory" && (record.importance ?? 0) >= 8) return 1;
|
|
4582
|
+
if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
|
|
4583
|
+
return 3;
|
|
4584
|
+
}
|
|
4585
|
+
function inferFilePaths(record) {
|
|
4586
|
+
if (!["Read", "Write", "Edit"].includes(record.tool_name)) return null;
|
|
4587
|
+
const firstLine = record.raw_text.split("\n")[0] ?? "";
|
|
4588
|
+
const match = firstLine.match(/(\/[\w./-]+\.\w+)/);
|
|
4589
|
+
return match ? JSON.stringify([match[1]]) : null;
|
|
4590
|
+
}
|
|
4591
|
+
function inferCommitHash(record) {
|
|
4592
|
+
if (record.tool_name !== "Bash") return null;
|
|
4593
|
+
const match = record.raw_text.match(/\b([a-f0-9]{7,40})\b/);
|
|
4594
|
+
return match ? match[1] : null;
|
|
4595
|
+
}
|
|
4596
|
+
function inferLanguageType(record) {
|
|
4597
|
+
const text = record.raw_text;
|
|
4598
|
+
if (!text || text.length < 10) return null;
|
|
4599
|
+
const trimmed = text.trimStart();
|
|
4600
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
|
|
4601
|
+
if (/\b(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER TABLE)\b/i.test(text)) return "sql";
|
|
4602
|
+
if (/\b(function |const |import |export |class |def |async |=>)\b/.test(text)) return "code";
|
|
4603
|
+
if (trimmed.startsWith("#") || trimmed.startsWith("*")) return "prose";
|
|
4604
|
+
return "mixed";
|
|
4605
|
+
}
|
|
4606
|
+
function inferDomain(record) {
|
|
4607
|
+
const proj = (record.project_name ?? "").toLowerCase();
|
|
4608
|
+
if (proj.includes("marketing") || proj.includes("content")) return "marketing";
|
|
4609
|
+
if (proj.includes("crm") || proj.includes("customer")) return "customer";
|
|
4610
|
+
return null;
|
|
3698
4611
|
}
|
|
3699
|
-
function
|
|
3700
|
-
if (
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
if (parts.length !== 3) return null;
|
|
3705
|
-
const [ivB64, tagB64, cipherB64] = parts;
|
|
3706
|
-
const iv = Buffer.from(ivB64, "base64");
|
|
3707
|
-
const authTag = Buffer.from(tagB64, "base64");
|
|
3708
|
-
const decipher = crypto2.createDecipheriv("aes-256-gcm", machineKey, iv);
|
|
3709
|
-
decipher.setAuthTag(authTag);
|
|
3710
|
-
let decrypted = decipher.update(cipherB64, "base64", "utf-8");
|
|
3711
|
-
decrypted += decipher.final("utf-8");
|
|
3712
|
-
return decrypted;
|
|
3713
|
-
} catch {
|
|
3714
|
-
return null;
|
|
4612
|
+
async function writeMemory(record) {
|
|
4613
|
+
if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
|
|
4614
|
+
throw new Error(
|
|
4615
|
+
`Expected ${EMBEDDING_DIM}-dim vector, got ${record.vector.length}`
|
|
4616
|
+
);
|
|
3715
4617
|
}
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
const
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
await chmod2(keyPath, 384);
|
|
3726
|
-
return "encrypted";
|
|
4618
|
+
const governed = governMemoryRecord(record);
|
|
4619
|
+
if (governed.shouldDrop) return;
|
|
4620
|
+
record = governed.record;
|
|
4621
|
+
const contentHash = governed.contentHash;
|
|
4622
|
+
const memoryType = record.memory_type ?? "raw";
|
|
4623
|
+
if (_pendingRecords.some(
|
|
4624
|
+
(r) => r.content_hash === contentHash && r.agent_id === record.agent_id && r.project_name === record.project_name && (r.memory_type ?? "raw") === memoryType
|
|
4625
|
+
)) {
|
|
4626
|
+
return;
|
|
3727
4627
|
}
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
4628
|
+
try {
|
|
4629
|
+
const existing = await findScopedDuplicate({
|
|
4630
|
+
contentHash,
|
|
4631
|
+
agentId: record.agent_id,
|
|
4632
|
+
projectName: record.project_name,
|
|
4633
|
+
memoryType
|
|
4634
|
+
});
|
|
4635
|
+
if (existing) return;
|
|
4636
|
+
} catch {
|
|
3736
4637
|
}
|
|
3737
|
-
const
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
4638
|
+
const dbRow = {
|
|
4639
|
+
id: record.id,
|
|
4640
|
+
agent_id: record.agent_id,
|
|
4641
|
+
agent_role: record.agent_role,
|
|
4642
|
+
session_id: record.session_id,
|
|
4643
|
+
timestamp: record.timestamp,
|
|
4644
|
+
tool_name: record.tool_name,
|
|
4645
|
+
project_name: record.project_name,
|
|
4646
|
+
has_error: record.has_error ? 1 : 0,
|
|
4647
|
+
raw_text: record.raw_text,
|
|
4648
|
+
vector: record.vector,
|
|
4649
|
+
version: 0,
|
|
4650
|
+
// Placeholder — assigned atomically at flush time
|
|
4651
|
+
task_id: record.task_id ?? null,
|
|
4652
|
+
importance: record.importance ?? 5,
|
|
4653
|
+
status: record.status ?? "active",
|
|
4654
|
+
confidence: record.confidence ?? 0.7,
|
|
4655
|
+
last_accessed: record.last_accessed ?? record.timestamp,
|
|
4656
|
+
workspace_id: record.workspace_id ?? null,
|
|
4657
|
+
document_id: record.document_id ?? null,
|
|
4658
|
+
user_id: record.user_id ?? null,
|
|
4659
|
+
char_offset: record.char_offset ?? null,
|
|
4660
|
+
page_number: record.page_number ?? null,
|
|
4661
|
+
source_path: record.source_path ?? null,
|
|
4662
|
+
source_type: record.source_type ?? null,
|
|
4663
|
+
tier: record.tier ?? classifyTier(record),
|
|
4664
|
+
supersedes_id: record.supersedes_id ?? null,
|
|
4665
|
+
draft: record.draft ? 1 : 0,
|
|
4666
|
+
memory_type: memoryType,
|
|
4667
|
+
trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null,
|
|
4668
|
+
content_hash: contentHash,
|
|
4669
|
+
intent: record.intent ?? null,
|
|
4670
|
+
outcome: record.outcome ?? null,
|
|
4671
|
+
domain: record.domain ?? inferDomain(record),
|
|
4672
|
+
referenced_entities: record.referenced_entities ?? null,
|
|
4673
|
+
retrieval_count: record.retrieval_count ?? 0,
|
|
4674
|
+
chain_position: record.chain_position ?? null,
|
|
4675
|
+
review_status: record.review_status ?? null,
|
|
4676
|
+
context_window_pct: record.context_window_pct ?? null,
|
|
4677
|
+
file_paths: record.file_paths ?? inferFilePaths(record),
|
|
4678
|
+
commit_hash: record.commit_hash ?? inferCommitHash(record),
|
|
4679
|
+
duration_ms: record.duration_ms ?? null,
|
|
4680
|
+
token_cost: record.token_cost ?? null,
|
|
4681
|
+
audience: record.audience ?? null,
|
|
4682
|
+
language_type: record.language_type ?? inferLanguageType(record),
|
|
4683
|
+
parent_memory_id: record.parent_memory_id ?? null
|
|
4684
|
+
};
|
|
4685
|
+
_pendingRecords.push(dbRow);
|
|
4686
|
+
orgBus.emit({
|
|
4687
|
+
type: "memory_stored",
|
|
4688
|
+
agentId: record.agent_id,
|
|
4689
|
+
project: record.project_name,
|
|
4690
|
+
timestamp: record.timestamp
|
|
4691
|
+
});
|
|
4692
|
+
const MAX_PENDING = 1e3;
|
|
4693
|
+
if (_pendingRecords.length > MAX_PENDING) {
|
|
4694
|
+
const dropped = _pendingRecords.length - MAX_PENDING;
|
|
4695
|
+
_pendingRecords = _pendingRecords.slice(-MAX_PENDING);
|
|
4696
|
+
console.warn(`[store] Dropped ${dropped} oldest pending records (overflow)`);
|
|
4697
|
+
}
|
|
4698
|
+
if (_flushTimer === null) {
|
|
4699
|
+
_flushTimer = setInterval(() => {
|
|
4700
|
+
void flushBatch();
|
|
4701
|
+
}, _flushIntervalMs);
|
|
4702
|
+
if (_flushTimer && typeof _flushTimer === "object" && "unref" in _flushTimer) {
|
|
4703
|
+
_flushTimer.unref();
|
|
3749
4704
|
}
|
|
3750
4705
|
}
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
process.stderr.write(
|
|
3754
|
-
`[keychain] Key not found at ${keyPath} (HOME=${os5.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
|
|
3755
|
-
`
|
|
3756
|
-
);
|
|
3757
|
-
return null;
|
|
4706
|
+
if (_pendingRecords.length >= _batchSize) {
|
|
4707
|
+
await flushBatch();
|
|
3758
4708
|
}
|
|
4709
|
+
}
|
|
4710
|
+
async function flushBatch() {
|
|
4711
|
+
if (_flushing || _pendingRecords.length === 0) return 0;
|
|
4712
|
+
_flushing = true;
|
|
3759
4713
|
try {
|
|
3760
|
-
const
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
return null;
|
|
3767
|
-
}
|
|
3768
|
-
const decrypted = decryptWithMachineKey(content, machineKey);
|
|
3769
|
-
if (!decrypted) {
|
|
3770
|
-
process.stderr.write(
|
|
3771
|
-
"[keychain] Key decryption failed \u2014 machine may have changed.\n Use your 24-word recovery phrase: exe-os link import\n"
|
|
3772
|
-
);
|
|
3773
|
-
return null;
|
|
3774
|
-
}
|
|
3775
|
-
b64Value = decrypted;
|
|
3776
|
-
} else {
|
|
3777
|
-
b64Value = content;
|
|
4714
|
+
const batch = _pendingRecords.slice(0);
|
|
4715
|
+
const client = getClient();
|
|
4716
|
+
const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
|
|
4717
|
+
let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
|
|
4718
|
+
for (const row of batch) {
|
|
4719
|
+
row.version = baseVersion++;
|
|
3778
4720
|
}
|
|
3779
|
-
|
|
3780
|
-
const
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
const
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
4721
|
+
_nextVersion = baseVersion;
|
|
4722
|
+
const buildStmt = (row) => {
|
|
4723
|
+
const hasVector = row.vector !== null;
|
|
4724
|
+
const taskId = row.task_id ?? null;
|
|
4725
|
+
const importance = row.importance ?? 5;
|
|
4726
|
+
const status = row.status ?? "active";
|
|
4727
|
+
const confidence = row.confidence ?? 0.7;
|
|
4728
|
+
const lastAccessed = row.last_accessed ?? row.timestamp;
|
|
4729
|
+
const workspaceId = row.workspace_id ?? null;
|
|
4730
|
+
const documentId = row.document_id ?? null;
|
|
4731
|
+
const userId = row.user_id ?? null;
|
|
4732
|
+
const charOffset = row.char_offset ?? null;
|
|
4733
|
+
const pageNumber = row.page_number ?? null;
|
|
4734
|
+
const sourcePath = row.source_path ?? null;
|
|
4735
|
+
const sourceType = row.source_type ?? null;
|
|
4736
|
+
const tier = row.tier ?? 3;
|
|
4737
|
+
const supersedesId = row.supersedes_id ?? null;
|
|
4738
|
+
const draft = row.draft ? 1 : 0;
|
|
4739
|
+
const memoryType = row.memory_type ?? "raw";
|
|
4740
|
+
const trajectory = row.trajectory ?? null;
|
|
4741
|
+
const contentHash = row.content_hash ?? null;
|
|
4742
|
+
const intent = row.intent ?? null;
|
|
4743
|
+
const outcome = row.outcome ?? null;
|
|
4744
|
+
const domain = row.domain ?? null;
|
|
4745
|
+
const referencedEntities = row.referenced_entities ?? null;
|
|
4746
|
+
const retrievalCount = row.retrieval_count ?? 0;
|
|
4747
|
+
const chainPosition = row.chain_position ?? null;
|
|
4748
|
+
const reviewStatus = row.review_status ?? null;
|
|
4749
|
+
const contextWindowPct = row.context_window_pct ?? null;
|
|
4750
|
+
const filePaths = row.file_paths ?? null;
|
|
4751
|
+
const commitHash = row.commit_hash ?? null;
|
|
4752
|
+
const durationMs = row.duration_ms ?? null;
|
|
4753
|
+
const tokenCost = row.token_cost ?? null;
|
|
4754
|
+
const audience = row.audience ?? null;
|
|
4755
|
+
const languageType = row.language_type ?? null;
|
|
4756
|
+
const parentMemoryId = row.parent_memory_id ?? null;
|
|
4757
|
+
const cols = `id, agent_id, agent_role, session_id, timestamp,
|
|
4758
|
+
tool_name, project_name,
|
|
4759
|
+
has_error, raw_text, vector, version, task_id, importance, status,
|
|
4760
|
+
confidence, last_accessed,
|
|
4761
|
+
workspace_id, document_id, user_id, char_offset, page_number,
|
|
4762
|
+
source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory, content_hash,
|
|
4763
|
+
intent, outcome, domain, referenced_entities, retrieval_count,
|
|
4764
|
+
chain_position, review_status, context_window_pct, file_paths, commit_hash,
|
|
4765
|
+
duration_ms, token_cost, audience, language_type, parent_memory_id`;
|
|
4766
|
+
const metaArgs = [
|
|
4767
|
+
intent,
|
|
4768
|
+
outcome,
|
|
4769
|
+
domain,
|
|
4770
|
+
referencedEntities,
|
|
4771
|
+
retrievalCount,
|
|
4772
|
+
chainPosition,
|
|
4773
|
+
reviewStatus,
|
|
4774
|
+
contextWindowPct,
|
|
4775
|
+
filePaths,
|
|
4776
|
+
commitHash,
|
|
4777
|
+
durationMs,
|
|
4778
|
+
tokenCost,
|
|
4779
|
+
audience,
|
|
4780
|
+
languageType,
|
|
4781
|
+
parentMemoryId
|
|
4782
|
+
];
|
|
4783
|
+
const baseArgs = [
|
|
4784
|
+
row.id,
|
|
4785
|
+
row.agent_id,
|
|
4786
|
+
row.agent_role,
|
|
4787
|
+
row.session_id,
|
|
4788
|
+
row.timestamp,
|
|
4789
|
+
row.tool_name,
|
|
4790
|
+
row.project_name,
|
|
4791
|
+
row.has_error,
|
|
4792
|
+
row.raw_text
|
|
4793
|
+
];
|
|
4794
|
+
const sharedArgs = [
|
|
4795
|
+
row.version,
|
|
4796
|
+
taskId,
|
|
4797
|
+
importance,
|
|
4798
|
+
status,
|
|
4799
|
+
confidence,
|
|
4800
|
+
lastAccessed,
|
|
4801
|
+
workspaceId,
|
|
4802
|
+
documentId,
|
|
4803
|
+
userId,
|
|
4804
|
+
charOffset,
|
|
4805
|
+
pageNumber,
|
|
4806
|
+
sourcePath,
|
|
4807
|
+
sourceType,
|
|
4808
|
+
tier,
|
|
4809
|
+
supersedesId,
|
|
4810
|
+
draft,
|
|
4811
|
+
memoryType,
|
|
4812
|
+
trajectory,
|
|
4813
|
+
contentHash
|
|
4814
|
+
];
|
|
4815
|
+
return {
|
|
4816
|
+
sql: hasVector ? `INSERT OR IGNORE INTO memories (${cols})
|
|
4817
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories (${cols})
|
|
4818
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
4819
|
+
args: hasVector ? [...baseArgs, vectorToBlob(row.vector), ...sharedArgs, ...metaArgs] : [...baseArgs, ...sharedArgs, ...metaArgs]
|
|
4820
|
+
};
|
|
4821
|
+
};
|
|
4822
|
+
const globalClient = getClient();
|
|
4823
|
+
const globalStmts = batch.map(buildStmt);
|
|
4824
|
+
await globalClient.batch(globalStmts, "write");
|
|
4825
|
+
try {
|
|
4826
|
+
const { insertMemoryCardsForBatch: insertMemoryCardsForBatch2 } = await Promise.resolve().then(() => (init_memory_cards(), memory_cards_exports));
|
|
4827
|
+
await insertMemoryCardsForBatch2(batch);
|
|
4828
|
+
} catch {
|
|
3797
4829
|
}
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
4830
|
+
schedulePostWriteMemoryHygiene(batch.map((row) => row.id));
|
|
4831
|
+
_pendingRecords.splice(0, batch.length);
|
|
4832
|
+
try {
|
|
4833
|
+
const { isShardingEnabled: isShardingEnabled2, getReadyShardClient: getReadyShardClient2 } = await Promise.resolve().then(() => (init_shard_manager(), shard_manager_exports));
|
|
4834
|
+
if (isShardingEnabled2()) {
|
|
4835
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
4836
|
+
let skippedUnknown = 0;
|
|
4837
|
+
for (const row of batch) {
|
|
4838
|
+
const proj = row.project_name?.trim();
|
|
4839
|
+
if (!proj) {
|
|
4840
|
+
skippedUnknown++;
|
|
4841
|
+
continue;
|
|
4842
|
+
}
|
|
4843
|
+
if (!byProject.has(proj)) byProject.set(proj, []);
|
|
4844
|
+
byProject.get(proj).push(row);
|
|
4845
|
+
}
|
|
4846
|
+
if (skippedUnknown > 0) {
|
|
4847
|
+
process.stderr.write(
|
|
4848
|
+
`[store] Shard skip: ${skippedUnknown} record(s) with empty project_name (kept in main DB only)
|
|
3802
4849
|
`
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
if (typeHandlers) {
|
|
3819
|
-
for (const handler of typeHandlers) {
|
|
3820
|
-
try {
|
|
3821
|
-
handler(event);
|
|
3822
|
-
} catch {
|
|
4850
|
+
);
|
|
4851
|
+
}
|
|
4852
|
+
for (const [project, rows] of byProject) {
|
|
4853
|
+
try {
|
|
4854
|
+
const shardClient = await getReadyShardClient2(project);
|
|
4855
|
+
const shardStmts = rows.map(buildStmt);
|
|
4856
|
+
await shardClient.batch(shardStmts, "write");
|
|
4857
|
+
} catch (err) {
|
|
4858
|
+
const fullError = err instanceof Error ? `${err.name}: ${err.message}${err.stack ? `
|
|
4859
|
+
${err.stack.split("\n").slice(1, 3).join("\n")}` : ""}` : String(err);
|
|
4860
|
+
process.stderr.write(
|
|
4861
|
+
`[store] Shard write failed for ${project} (${rows.length} records): ${fullError}
|
|
4862
|
+
`
|
|
4863
|
+
);
|
|
4864
|
+
}
|
|
3823
4865
|
}
|
|
3824
4866
|
}
|
|
4867
|
+
} catch {
|
|
3825
4868
|
}
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
} catch {
|
|
3830
|
-
}
|
|
3831
|
-
}
|
|
3832
|
-
}
|
|
3833
|
-
/** Subscribe to a specific event type */
|
|
3834
|
-
on(type, handler) {
|
|
3835
|
-
if (!this.handlers.has(type)) {
|
|
3836
|
-
this.handlers.set(type, /* @__PURE__ */ new Set());
|
|
3837
|
-
}
|
|
3838
|
-
this.handlers.get(type).add(handler);
|
|
3839
|
-
}
|
|
3840
|
-
/** Subscribe to ALL events */
|
|
3841
|
-
onAny(handler) {
|
|
3842
|
-
this.globalHandlers.add(handler);
|
|
3843
|
-
}
|
|
3844
|
-
/** Unsubscribe from a specific event type */
|
|
3845
|
-
off(type, handler) {
|
|
3846
|
-
this.handlers.get(type)?.delete(handler);
|
|
3847
|
-
}
|
|
3848
|
-
/** Unsubscribe from ALL events */
|
|
3849
|
-
offAny(handler) {
|
|
3850
|
-
this.globalHandlers.delete(handler);
|
|
4869
|
+
return batch.length;
|
|
4870
|
+
} finally {
|
|
4871
|
+
_flushing = false;
|
|
3851
4872
|
}
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
4873
|
+
}
|
|
4874
|
+
function buildWikiScopeFilter(options, columnPrefix) {
|
|
4875
|
+
const args = [];
|
|
4876
|
+
let clause = "";
|
|
4877
|
+
if (options?.workspaceId !== void 0) {
|
|
4878
|
+
clause += ` AND ${columnPrefix}workspace_id = ?`;
|
|
4879
|
+
args.push(options.workspaceId);
|
|
4880
|
+
}
|
|
4881
|
+
if (options?.userId === void 0) {
|
|
4882
|
+
clause += ` AND ${columnPrefix}user_id IS NULL`;
|
|
4883
|
+
} else if (options.userId === null) {
|
|
4884
|
+
clause += ` AND ${columnPrefix}user_id IS NULL`;
|
|
4885
|
+
} else {
|
|
4886
|
+
clause += ` AND (${columnPrefix}user_id = ? OR ${columnPrefix}user_id IS NULL)`;
|
|
4887
|
+
args.push(options.userId);
|
|
3856
4888
|
}
|
|
3857
|
-
};
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
var INIT_RETRY_DELAY_MS = 1e3;
|
|
3866
|
-
function isBusyError2(err) {
|
|
3867
|
-
if (err instanceof Error) {
|
|
3868
|
-
const msg = err.message.toLowerCase();
|
|
3869
|
-
return msg.includes("sqlite_busy") || msg.includes("database is locked");
|
|
4889
|
+
return { clause, args };
|
|
4890
|
+
}
|
|
4891
|
+
function buildRawVisibilityFilter(options, columnPrefix) {
|
|
4892
|
+
if (options?.includeRaw === false) {
|
|
4893
|
+
return {
|
|
4894
|
+
clause: ` AND COALESCE(${columnPrefix}memory_type, 'raw') != 'raw'`,
|
|
4895
|
+
args: []
|
|
4896
|
+
};
|
|
3870
4897
|
}
|
|
3871
|
-
return
|
|
4898
|
+
return { clause: "", args: [] };
|
|
3872
4899
|
}
|
|
3873
|
-
async function
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
`
|
|
3882
|
-
);
|
|
3883
|
-
await new Promise((r) => setTimeout(r, INIT_RETRY_DELAY_MS * (attempt + 1)));
|
|
4900
|
+
async function searchMemories(queryVector, agentId, options) {
|
|
4901
|
+
let client;
|
|
4902
|
+
try {
|
|
4903
|
+
const { isShardingEnabled: isShardingEnabled2, shardExists: shardExists2, getReadyShardClient: getReadyShardClient2 } = await Promise.resolve().then(() => (init_shard_manager(), shard_manager_exports));
|
|
4904
|
+
if (isShardingEnabled2() && options?.projectName && shardExists2(options.projectName)) {
|
|
4905
|
+
client = await getReadyShardClient2(options.projectName);
|
|
4906
|
+
} else {
|
|
4907
|
+
client = getClient();
|
|
3884
4908
|
}
|
|
4909
|
+
} catch {
|
|
4910
|
+
client = getClient();
|
|
4911
|
+
}
|
|
4912
|
+
const limit = options?.limit ?? 10;
|
|
4913
|
+
const statusFilter = options?.includeArchived ? "" : `
|
|
4914
|
+
AND COALESCE(status, 'active') = 'active'`;
|
|
4915
|
+
const draftFilter = options?.includeDrafts ? "" : `
|
|
4916
|
+
AND (draft = 0 OR draft IS NULL)`;
|
|
4917
|
+
let sql = `SELECT id, agent_id, agent_role, session_id, timestamp,
|
|
4918
|
+
tool_name, project_name,
|
|
4919
|
+
has_error, raw_text, vector, importance, status,
|
|
4920
|
+
confidence, last_accessed,
|
|
4921
|
+
workspace_id, document_id, user_id,
|
|
4922
|
+
char_offset, page_number,
|
|
4923
|
+
source_path, source_type
|
|
4924
|
+
FROM memories
|
|
4925
|
+
WHERE agent_id = ?
|
|
4926
|
+
AND vector IS NOT NULL${statusFilter}${draftFilter}
|
|
4927
|
+
AND COALESCE(confidence, 0.7) >= 0.3`;
|
|
4928
|
+
const args = [agentId];
|
|
4929
|
+
const scope = buildWikiScopeFilter(options, "");
|
|
4930
|
+
sql += scope.clause;
|
|
4931
|
+
args.push(...scope.args);
|
|
4932
|
+
const rawVisibility = buildRawVisibilityFilter(options, "");
|
|
4933
|
+
sql += rawVisibility.clause;
|
|
4934
|
+
args.push(...rawVisibility.args);
|
|
4935
|
+
if (options?.projectName) {
|
|
4936
|
+
sql += ` AND project_name = ?`;
|
|
4937
|
+
args.push(options.projectName);
|
|
4938
|
+
}
|
|
4939
|
+
if (options?.toolName) {
|
|
4940
|
+
sql += ` AND tool_name = ?`;
|
|
4941
|
+
args.push(options.toolName);
|
|
4942
|
+
}
|
|
4943
|
+
if (options?.hasError !== void 0) {
|
|
4944
|
+
sql += ` AND has_error = ?`;
|
|
4945
|
+
args.push(options.hasError ? 1 : 0);
|
|
4946
|
+
}
|
|
4947
|
+
if (options?.since) {
|
|
4948
|
+
sql += ` AND timestamp >= ?`;
|
|
4949
|
+
args.push(options.since);
|
|
4950
|
+
}
|
|
4951
|
+
if (options?.memoryTypes && options.memoryTypes.length > 0) {
|
|
4952
|
+
const uniqueTypes = [...new Set(options.memoryTypes)];
|
|
4953
|
+
sql += ` AND memory_type IN (${uniqueTypes.map(() => "?").join(",")})`;
|
|
4954
|
+
args.push(...uniqueTypes);
|
|
4955
|
+
} else if (options?.memoryType) {
|
|
4956
|
+
sql += ` AND memory_type = ?`;
|
|
4957
|
+
args.push(options.memoryType);
|
|
4958
|
+
}
|
|
4959
|
+
sql += ` ORDER BY vector_distance_cos(vector, vector32(?))`;
|
|
4960
|
+
args.push(vectorToBlob(queryVector));
|
|
4961
|
+
sql += ` LIMIT ?`;
|
|
4962
|
+
args.push(limit);
|
|
4963
|
+
const result = await client.execute({ sql, args });
|
|
4964
|
+
return result.rows.map((row) => ({
|
|
4965
|
+
id: row.id,
|
|
4966
|
+
agent_id: row.agent_id,
|
|
4967
|
+
agent_role: row.agent_role,
|
|
4968
|
+
session_id: row.session_id,
|
|
4969
|
+
timestamp: row.timestamp,
|
|
4970
|
+
tool_name: row.tool_name,
|
|
4971
|
+
project_name: row.project_name,
|
|
4972
|
+
has_error: row.has_error === 1,
|
|
4973
|
+
raw_text: row.raw_text,
|
|
4974
|
+
vector: row.vector == null ? [] : Array.isArray(row.vector) ? row.vector : Array.from(row.vector),
|
|
4975
|
+
importance: row.importance ?? 5,
|
|
4976
|
+
status: row.status ?? "active",
|
|
4977
|
+
confidence: row.confidence ?? 0.7,
|
|
4978
|
+
last_accessed: row.last_accessed ?? row.timestamp,
|
|
4979
|
+
workspace_id: row.workspace_id ?? null,
|
|
4980
|
+
document_id: row.document_id ?? null,
|
|
4981
|
+
user_id: row.user_id ?? null,
|
|
4982
|
+
char_offset: row.char_offset ?? null,
|
|
4983
|
+
page_number: row.page_number ?? null,
|
|
4984
|
+
source_path: row.source_path ?? null,
|
|
4985
|
+
source_type: row.source_type ?? null
|
|
4986
|
+
}));
|
|
4987
|
+
}
|
|
4988
|
+
async function attachDocumentMetadata(records) {
|
|
4989
|
+
const docIds = [
|
|
4990
|
+
...new Set(
|
|
4991
|
+
records.map((r) => r.document_id).filter((id) => typeof id === "string" && id.length > 0)
|
|
4992
|
+
)
|
|
4993
|
+
];
|
|
4994
|
+
if (docIds.length === 0) return records;
|
|
4995
|
+
try {
|
|
4996
|
+
const client = getClient();
|
|
4997
|
+
const placeholders = docIds.map(() => "?").join(",");
|
|
4998
|
+
const result = await client.execute({
|
|
4999
|
+
sql: `SELECT id, filename, mime, source_type, uploaded_at
|
|
5000
|
+
FROM documents
|
|
5001
|
+
WHERE id IN (${placeholders})`,
|
|
5002
|
+
args: docIds
|
|
5003
|
+
});
|
|
5004
|
+
const byId = /* @__PURE__ */ new Map();
|
|
5005
|
+
for (const row of result.rows) {
|
|
5006
|
+
const id = row.id;
|
|
5007
|
+
byId.set(id, {
|
|
5008
|
+
document_id: id,
|
|
5009
|
+
filename: row.filename,
|
|
5010
|
+
mime: row.mime ?? null,
|
|
5011
|
+
source_type: row.source_type ?? null,
|
|
5012
|
+
uploaded_at: row.uploaded_at
|
|
5013
|
+
});
|
|
5014
|
+
}
|
|
5015
|
+
for (const record of records) {
|
|
5016
|
+
if (!record.document_id) continue;
|
|
5017
|
+
record.document_metadata = byId.get(record.document_id) ?? null;
|
|
5018
|
+
}
|
|
5019
|
+
} catch {
|
|
3885
5020
|
}
|
|
3886
|
-
|
|
5021
|
+
return records;
|
|
3887
5022
|
}
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
5023
|
+
async function flushTier3(agentId, options) {
|
|
5024
|
+
const client = getClient();
|
|
5025
|
+
const maxAge = options?.maxAgeHours ?? 72;
|
|
5026
|
+
const cutoff = new Date(Date.now() - maxAge * 36e5).toISOString();
|
|
5027
|
+
if (options?.dryRun) {
|
|
5028
|
+
const result2 = await client.execute({
|
|
5029
|
+
sql: `SELECT COUNT(*) as cnt FROM memories
|
|
5030
|
+
WHERE agent_id = ? AND tier = 3 AND status = 'active' AND timestamp < ?`,
|
|
5031
|
+
args: [agentId, cutoff]
|
|
5032
|
+
});
|
|
5033
|
+
return { archived: Number(result2.rows[0]?.cnt ?? 0) };
|
|
5034
|
+
}
|
|
5035
|
+
const result = await client.execute({
|
|
5036
|
+
sql: `UPDATE memories SET status = 'archived'
|
|
5037
|
+
WHERE agent_id = ? AND tier = 3 AND status = 'active' AND timestamp < ?`,
|
|
5038
|
+
args: [agentId, cutoff]
|
|
5039
|
+
});
|
|
5040
|
+
return { archived: result.rowsAffected };
|
|
5041
|
+
}
|
|
5042
|
+
async function disposeStore() {
|
|
3895
5043
|
if (_flushTimer !== null) {
|
|
3896
5044
|
clearInterval(_flushTimer);
|
|
3897
5045
|
_flushTimer = null;
|
|
3898
5046
|
}
|
|
5047
|
+
if (_pendingRecords.length > 0) {
|
|
5048
|
+
await flushBatch();
|
|
5049
|
+
}
|
|
5050
|
+
await disposeTurso();
|
|
3899
5051
|
_pendingRecords = [];
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
5052
|
+
_nextVersion = 1;
|
|
5053
|
+
}
|
|
5054
|
+
function vectorToBlob(vector) {
|
|
5055
|
+
const f32 = vector instanceof Float32Array ? vector : new Float32Array(vector);
|
|
5056
|
+
return JSON.stringify(Array.from(f32));
|
|
5057
|
+
}
|
|
5058
|
+
async function updateMemoryStatus(id, status) {
|
|
5059
|
+
const client = getClient();
|
|
5060
|
+
await client.execute({
|
|
5061
|
+
sql: `UPDATE memories SET status = ? WHERE id = ?`,
|
|
5062
|
+
args: [status, id]
|
|
5063
|
+
});
|
|
5064
|
+
}
|
|
5065
|
+
function reserveVersions(count) {
|
|
5066
|
+
const reserved = [];
|
|
5067
|
+
for (let i = 0; i < count; i++) {
|
|
5068
|
+
reserved.push(_nextVersion++);
|
|
3907
5069
|
}
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
)
|
|
3915
|
-
|
|
5070
|
+
return reserved;
|
|
5071
|
+
}
|
|
5072
|
+
async function getMemoryCardinality(agentId) {
|
|
5073
|
+
try {
|
|
5074
|
+
const client = getClient();
|
|
5075
|
+
const result = await client.execute({
|
|
5076
|
+
sql: `SELECT COUNT(*) as cnt FROM memories WHERE agent_id = ? AND COALESCE(status, 'active') = 'active'`,
|
|
5077
|
+
args: [agentId]
|
|
5078
|
+
});
|
|
5079
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
5080
|
+
} catch {
|
|
5081
|
+
return 0;
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
var INIT_MAX_RETRIES, INIT_RETRY_DELAY_MS, _pendingRecords, _batchSize, _flushIntervalMs, _flushTimer, _flushing, _nextVersion;
|
|
5085
|
+
var init_store = __esm({
|
|
5086
|
+
"src/lib/store.ts"() {
|
|
5087
|
+
"use strict";
|
|
5088
|
+
init_memory();
|
|
5089
|
+
init_database();
|
|
5090
|
+
init_keychain();
|
|
5091
|
+
init_config();
|
|
5092
|
+
init_state_bus();
|
|
5093
|
+
init_memory_write_governor();
|
|
5094
|
+
INIT_MAX_RETRIES = 3;
|
|
5095
|
+
INIT_RETRY_DELAY_MS = 1e3;
|
|
5096
|
+
_pendingRecords = [];
|
|
5097
|
+
_batchSize = 20;
|
|
5098
|
+
_flushIntervalMs = 1e4;
|
|
5099
|
+
_flushTimer = null;
|
|
5100
|
+
_flushing = false;
|
|
5101
|
+
_nextVersion = 1;
|
|
5102
|
+
}
|
|
5103
|
+
});
|
|
5104
|
+
|
|
5105
|
+
// src/bin/fast-db-init.ts
|
|
5106
|
+
var fast_db_init_exports = {};
|
|
5107
|
+
__export(fast_db_init_exports, {
|
|
5108
|
+
fastDbInit: () => fastDbInit
|
|
5109
|
+
});
|
|
5110
|
+
async function fastDbInit() {
|
|
5111
|
+
const { isInitialized: isInitialized2, getClient: getClient2, setExternalClient: setExternalClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
|
|
5112
|
+
if (isInitialized2()) {
|
|
5113
|
+
return getClient2();
|
|
3916
5114
|
}
|
|
3917
|
-
const hexKey = masterKey.toString("hex");
|
|
3918
|
-
await initTurso({
|
|
3919
|
-
dbPath,
|
|
3920
|
-
encryptionKey: hexKey
|
|
3921
|
-
});
|
|
3922
|
-
await retryOnBusy2(() => ensureSchema(), "ensureSchema");
|
|
3923
5115
|
try {
|
|
3924
|
-
const {
|
|
3925
|
-
await
|
|
5116
|
+
const { connectEmbedDaemon: connectEmbedDaemon2, sendDaemonRequest: sendDaemonRequest2, isClientConnected: isClientConnected2 } = await Promise.resolve().then(() => (init_exe_daemon_client(), exe_daemon_client_exports));
|
|
5117
|
+
const { deserializeResultSet: deserializeResultSet2 } = await Promise.resolve().then(() => (init_daemon_protocol(), daemon_protocol_exports));
|
|
5118
|
+
await connectEmbedDaemon2();
|
|
5119
|
+
if (isClientConnected2()) {
|
|
5120
|
+
const daemonClient = {
|
|
5121
|
+
async execute(stmt) {
|
|
5122
|
+
const sql = typeof stmt === "string" ? stmt : stmt.sql;
|
|
5123
|
+
const args = typeof stmt === "string" ? [] : Array.isArray(stmt.args) ? stmt.args : [];
|
|
5124
|
+
const resp = await sendDaemonRequest2({ type: "db-execute", sql, args });
|
|
5125
|
+
if (resp.error) throw new Error(String(resp.error));
|
|
5126
|
+
if (resp.db) return deserializeResultSet2(resp.db);
|
|
5127
|
+
throw new Error("Unexpected daemon response");
|
|
5128
|
+
},
|
|
5129
|
+
async batch(stmts, mode) {
|
|
5130
|
+
const statements = stmts.map((s) => {
|
|
5131
|
+
const sql = typeof s === "string" ? s : s.sql;
|
|
5132
|
+
const args = typeof s === "string" ? [] : Array.isArray(s.args) ? s.args : [];
|
|
5133
|
+
return { sql, args };
|
|
5134
|
+
});
|
|
5135
|
+
const resp = await sendDaemonRequest2({ type: "db-batch", statements, mode: mode ?? "deferred" });
|
|
5136
|
+
if (resp.error) throw new Error(String(resp.error));
|
|
5137
|
+
const batchResults = resp["db-batch"];
|
|
5138
|
+
if (batchResults) return batchResults.map(deserializeResultSet2);
|
|
5139
|
+
throw new Error("Unexpected daemon batch response");
|
|
5140
|
+
},
|
|
5141
|
+
async transaction(_mode) {
|
|
5142
|
+
throw new Error("Transactions not supported via daemon socket");
|
|
5143
|
+
},
|
|
5144
|
+
async executeMultiple(_sql) {
|
|
5145
|
+
throw new Error("executeMultiple not supported via daemon socket");
|
|
5146
|
+
},
|
|
5147
|
+
async migrate(_stmts) {
|
|
5148
|
+
throw new Error("migrate not supported via daemon socket");
|
|
5149
|
+
},
|
|
5150
|
+
sync() {
|
|
5151
|
+
return Promise.resolve(void 0);
|
|
5152
|
+
},
|
|
5153
|
+
close() {
|
|
5154
|
+
},
|
|
5155
|
+
get closed() {
|
|
5156
|
+
return false;
|
|
5157
|
+
},
|
|
5158
|
+
get protocol() {
|
|
5159
|
+
return "file";
|
|
5160
|
+
}
|
|
5161
|
+
};
|
|
5162
|
+
setExternalClient2(daemonClient);
|
|
5163
|
+
return daemonClient;
|
|
5164
|
+
}
|
|
3926
5165
|
} catch {
|
|
3927
5166
|
}
|
|
3928
|
-
|
|
5167
|
+
const { initStore: initStore2 } = await Promise.resolve().then(() => (init_store(), store_exports));
|
|
5168
|
+
await initStore2({ lightweight: true });
|
|
5169
|
+
return getClient2();
|
|
5170
|
+
}
|
|
5171
|
+
var init_fast_db_init = __esm({
|
|
5172
|
+
"src/bin/fast-db-init.ts"() {
|
|
5173
|
+
"use strict";
|
|
5174
|
+
}
|
|
5175
|
+
});
|
|
5176
|
+
|
|
5177
|
+
// src/lib/db-backup.ts
|
|
5178
|
+
var db_backup_exports = {};
|
|
5179
|
+
__export(db_backup_exports, {
|
|
5180
|
+
createBackup: () => createBackup,
|
|
5181
|
+
findActiveDb: () => findActiveDb,
|
|
5182
|
+
getBackupDir: () => getBackupDir,
|
|
5183
|
+
getLatestBackup: () => getLatestBackup,
|
|
5184
|
+
hasBackupToday: () => hasBackupToday,
|
|
5185
|
+
listBackups: () => listBackups,
|
|
5186
|
+
rotateBackups: () => rotateBackups
|
|
5187
|
+
});
|
|
5188
|
+
import { copyFileSync, existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync3, unlinkSync as unlinkSync4, statSync as statSync3 } from "fs";
|
|
5189
|
+
import path10 from "path";
|
|
5190
|
+
function findActiveDb() {
|
|
5191
|
+
for (const name of DB_NAMES) {
|
|
5192
|
+
const p = path10.join(EXE_AI_DIR, name);
|
|
5193
|
+
if (existsSync10(p)) return p;
|
|
5194
|
+
}
|
|
5195
|
+
return null;
|
|
5196
|
+
}
|
|
5197
|
+
function createBackup(reason = "manual") {
|
|
5198
|
+
const dbPath = findActiveDb();
|
|
5199
|
+
if (!dbPath) return null;
|
|
5200
|
+
mkdirSync5(BACKUP_DIR, { recursive: true });
|
|
5201
|
+
const dbName = path10.basename(dbPath, ".db");
|
|
5202
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
5203
|
+
const backupName = `${dbName}-${reason}-${timestamp}.db`;
|
|
5204
|
+
const backupPath = path10.join(BACKUP_DIR, backupName);
|
|
5205
|
+
copyFileSync(dbPath, backupPath);
|
|
5206
|
+
const walPath = dbPath + "-wal";
|
|
5207
|
+
if (existsSync10(walPath)) {
|
|
3929
5208
|
try {
|
|
3930
|
-
|
|
3931
|
-
initShardManager2(hexKey);
|
|
5209
|
+
copyFileSync(walPath, backupPath + "-wal");
|
|
3932
5210
|
} catch {
|
|
3933
5211
|
}
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
"version-query"
|
|
3938
|
-
);
|
|
3939
|
-
_nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
|
|
5212
|
+
}
|
|
5213
|
+
const shmPath = dbPath + "-shm";
|
|
5214
|
+
if (existsSync10(shmPath)) {
|
|
3940
5215
|
try {
|
|
3941
|
-
|
|
3942
|
-
await loadGlobalProcedures2();
|
|
5216
|
+
copyFileSync(shmPath, backupPath + "-shm");
|
|
3943
5217
|
} catch {
|
|
3944
5218
|
}
|
|
3945
5219
|
}
|
|
5220
|
+
return backupPath;
|
|
5221
|
+
}
|
|
5222
|
+
function rotateBackups(keepDays = DEFAULT_KEEP_DAYS) {
|
|
5223
|
+
if (!existsSync10(BACKUP_DIR)) return 0;
|
|
5224
|
+
const cutoff = Date.now() - keepDays * 24 * 60 * 60 * 1e3;
|
|
5225
|
+
let deleted = 0;
|
|
5226
|
+
try {
|
|
5227
|
+
const files = readdirSync3(BACKUP_DIR);
|
|
5228
|
+
for (const file of files) {
|
|
5229
|
+
if (!file.endsWith(".db") && !file.endsWith(".db-wal") && !file.endsWith(".db-shm")) continue;
|
|
5230
|
+
const filePath = path10.join(BACKUP_DIR, file);
|
|
5231
|
+
try {
|
|
5232
|
+
const stat = statSync3(filePath);
|
|
5233
|
+
if (stat.mtimeMs < cutoff) {
|
|
5234
|
+
unlinkSync4(filePath);
|
|
5235
|
+
deleted++;
|
|
5236
|
+
}
|
|
5237
|
+
} catch {
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
5240
|
+
} catch {
|
|
5241
|
+
}
|
|
5242
|
+
return deleted;
|
|
5243
|
+
}
|
|
5244
|
+
function listBackups() {
|
|
5245
|
+
if (!existsSync10(BACKUP_DIR)) return [];
|
|
5246
|
+
try {
|
|
5247
|
+
const files = readdirSync3(BACKUP_DIR).filter((f) => f.endsWith(".db") && !f.endsWith("-wal") && !f.endsWith("-shm"));
|
|
5248
|
+
return files.map((name) => {
|
|
5249
|
+
const p = path10.join(BACKUP_DIR, name);
|
|
5250
|
+
const stat = statSync3(p);
|
|
5251
|
+
return { path: p, name, size: stat.size, date: stat.mtime };
|
|
5252
|
+
}).sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
5253
|
+
} catch {
|
|
5254
|
+
return [];
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
function hasBackupToday(reason) {
|
|
5258
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5259
|
+
const backups = listBackups();
|
|
5260
|
+
return backups.some((b) => b.name.includes(reason) && b.name.includes(today.replace(/-/g, "-")));
|
|
3946
5261
|
}
|
|
5262
|
+
function getLatestBackup() {
|
|
5263
|
+
const backups = listBackups();
|
|
5264
|
+
return backups.length > 0 ? backups[0].path : null;
|
|
5265
|
+
}
|
|
5266
|
+
function getBackupDir() {
|
|
5267
|
+
return BACKUP_DIR;
|
|
5268
|
+
}
|
|
5269
|
+
var BACKUP_DIR, DEFAULT_KEEP_DAYS, DB_NAMES;
|
|
5270
|
+
var init_db_backup = __esm({
|
|
5271
|
+
"src/lib/db-backup.ts"() {
|
|
5272
|
+
"use strict";
|
|
5273
|
+
init_config();
|
|
5274
|
+
BACKUP_DIR = path10.join(EXE_AI_DIR, "backups");
|
|
5275
|
+
DEFAULT_KEEP_DAYS = 3;
|
|
5276
|
+
DB_NAMES = ["memories.db", "exe-mem.db", "exe-os.db", "exe.db"];
|
|
5277
|
+
}
|
|
5278
|
+
});
|
|
3947
5279
|
|
|
3948
5280
|
// src/bin/exe-doctor.ts
|
|
3949
|
-
|
|
5281
|
+
import os6 from "os";
|
|
3950
5282
|
|
|
3951
5283
|
// src/lib/is-main.ts
|
|
3952
5284
|
import { realpathSync } from "fs";
|
|
3953
|
-
import { fileURLToPath
|
|
5285
|
+
import { fileURLToPath } from "url";
|
|
3954
5286
|
function isMainModule(importMetaUrl) {
|
|
3955
5287
|
if (process.argv[1] == null) return false;
|
|
3956
5288
|
if (process.argv[1].includes("mcp/server")) return false;
|
|
3957
5289
|
try {
|
|
3958
5290
|
const scriptPath = realpathSync(process.argv[1]);
|
|
3959
|
-
const modulePath = realpathSync(
|
|
5291
|
+
const modulePath = realpathSync(fileURLToPath(importMetaUrl));
|
|
3960
5292
|
return scriptPath === modulePath;
|
|
3961
5293
|
} catch {
|
|
3962
5294
|
return importMetaUrl === `file://${process.argv[1]}` || importMetaUrl === new URL(process.argv[1], "file://").href;
|
|
@@ -3964,9 +5296,9 @@ function isMainModule(importMetaUrl) {
|
|
|
3964
5296
|
}
|
|
3965
5297
|
|
|
3966
5298
|
// src/bin/exe-doctor.ts
|
|
3967
|
-
import { existsSync as
|
|
5299
|
+
import { existsSync as existsSync11, readFileSync as readFileSync6 } from "fs";
|
|
3968
5300
|
import { spawn as spawn2 } from "child_process";
|
|
3969
|
-
import
|
|
5301
|
+
import path11 from "path";
|
|
3970
5302
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
3971
5303
|
|
|
3972
5304
|
// src/lib/conflict-detector.ts
|
|
@@ -4229,6 +5561,140 @@ async function detectConflicts(client, projectFilter, agentFilter2) {
|
|
|
4229
5561
|
};
|
|
4230
5562
|
}
|
|
4231
5563
|
|
|
5564
|
+
// src/adapters/runtime-hook-manifest.ts
|
|
5565
|
+
var EXE_HOOKS = {
|
|
5566
|
+
postToolCombined: "dist/hooks/post-tool-combined.js",
|
|
5567
|
+
sessionStart: "dist/hooks/session-start.js",
|
|
5568
|
+
promptSubmit: "dist/hooks/prompt-submit.js",
|
|
5569
|
+
heartbeat: "dist/hooks/exe-heartbeat-hook.js",
|
|
5570
|
+
stop: "dist/hooks/stop.js",
|
|
5571
|
+
preToolUse: "dist/hooks/pre-tool-use.js",
|
|
5572
|
+
subagentStop: "dist/hooks/subagent-stop.js",
|
|
5573
|
+
preCompact: "dist/hooks/pre-compact.js",
|
|
5574
|
+
postCompact: "dist/hooks/post-compact.js",
|
|
5575
|
+
sessionEnd: "dist/hooks/session-end.js",
|
|
5576
|
+
notification: "dist/hooks/notification.js",
|
|
5577
|
+
instructionsLoaded: "dist/hooks/instructions-loaded.js"
|
|
5578
|
+
};
|
|
5579
|
+
var EXE_HOOK_MANIFEST = [
|
|
5580
|
+
{
|
|
5581
|
+
key: "postToolCombined",
|
|
5582
|
+
event: "PostToolUse",
|
|
5583
|
+
commandMarker: EXE_HOOKS.postToolCombined,
|
|
5584
|
+
owner: "exe-os",
|
|
5585
|
+
purpose: "Single PostToolUse entrypoint for ingestion, error recall, summaries, and bug detection.",
|
|
5586
|
+
runtimes: ["claude", "codex", "opencode"],
|
|
5587
|
+
checkpointRole: "none"
|
|
5588
|
+
},
|
|
5589
|
+
{
|
|
5590
|
+
key: "sessionStart",
|
|
5591
|
+
event: "SessionStart",
|
|
5592
|
+
commandMarker: EXE_HOOKS.sessionStart,
|
|
5593
|
+
owner: "exe-os",
|
|
5594
|
+
purpose: "Loads agent identity, procedures, and boot context.",
|
|
5595
|
+
runtimes: ["claude", "codex", "opencode"],
|
|
5596
|
+
checkpointRole: "none"
|
|
5597
|
+
},
|
|
5598
|
+
{
|
|
5599
|
+
key: "promptSubmit",
|
|
5600
|
+
event: "UserPromptSubmit",
|
|
5601
|
+
commandMarker: EXE_HOOKS.promptSubmit,
|
|
5602
|
+
owner: "exe-os",
|
|
5603
|
+
purpose: "Injects current tasks, pending reviews, and local context before each prompt.",
|
|
5604
|
+
runtimes: ["claude", "codex", "opencode"],
|
|
5605
|
+
checkpointRole: "none"
|
|
5606
|
+
},
|
|
5607
|
+
{
|
|
5608
|
+
key: "heartbeat",
|
|
5609
|
+
event: "UserPromptSubmit",
|
|
5610
|
+
commandMarker: EXE_HOOKS.heartbeat,
|
|
5611
|
+
owner: "exe-os",
|
|
5612
|
+
purpose: "Lightweight heartbeat/status sidecar for Claude Code sessions.",
|
|
5613
|
+
runtimes: ["claude"],
|
|
5614
|
+
checkpointRole: "none"
|
|
5615
|
+
},
|
|
5616
|
+
{
|
|
5617
|
+
key: "stop",
|
|
5618
|
+
event: "Stop",
|
|
5619
|
+
commandMarker: EXE_HOOKS.stop,
|
|
5620
|
+
owner: "exe-os",
|
|
5621
|
+
purpose: "Finalizes task state, capacity signals, and emergency checkpointing.",
|
|
5622
|
+
runtimes: ["claude", "codex", "opencode"],
|
|
5623
|
+
checkpointRole: "capacity_checkpoint"
|
|
5624
|
+
},
|
|
5625
|
+
{
|
|
5626
|
+
key: "preToolUse",
|
|
5627
|
+
event: "PreToolUse",
|
|
5628
|
+
commandMarker: EXE_HOOKS.preToolUse,
|
|
5629
|
+
owner: "exe-os",
|
|
5630
|
+
purpose: "Preflight guardrails before shell/tool execution.",
|
|
5631
|
+
runtimes: ["claude", "codex", "opencode"],
|
|
5632
|
+
checkpointRole: "none"
|
|
5633
|
+
},
|
|
5634
|
+
{
|
|
5635
|
+
key: "subagentStop",
|
|
5636
|
+
event: "SubagentStop",
|
|
5637
|
+
commandMarker: EXE_HOOKS.subagentStop,
|
|
5638
|
+
owner: "exe-os",
|
|
5639
|
+
purpose: "Captures subagent completion context.",
|
|
5640
|
+
runtimes: ["claude"],
|
|
5641
|
+
checkpointRole: "none"
|
|
5642
|
+
},
|
|
5643
|
+
{
|
|
5644
|
+
key: "preCompact",
|
|
5645
|
+
event: "PreCompact",
|
|
5646
|
+
commandMarker: EXE_HOOKS.preCompact,
|
|
5647
|
+
owner: "exe-os",
|
|
5648
|
+
purpose: "Writes active-task snapshot and compaction recovery context.",
|
|
5649
|
+
runtimes: ["claude"],
|
|
5650
|
+
checkpointRole: "recovery_context"
|
|
5651
|
+
},
|
|
5652
|
+
{
|
|
5653
|
+
key: "postCompact",
|
|
5654
|
+
event: "PostCompact",
|
|
5655
|
+
commandMarker: EXE_HOOKS.postCompact,
|
|
5656
|
+
owner: "exe-os",
|
|
5657
|
+
purpose: "Rehydrates recovery context after compaction.",
|
|
5658
|
+
runtimes: ["claude"],
|
|
5659
|
+
checkpointRole: "recovery_context"
|
|
5660
|
+
},
|
|
5661
|
+
{
|
|
5662
|
+
key: "sessionEnd",
|
|
5663
|
+
event: "SessionEnd",
|
|
5664
|
+
commandMarker: EXE_HOOKS.sessionEnd,
|
|
5665
|
+
owner: "exe-os",
|
|
5666
|
+
purpose: "Stores session-end checkpoint and triages orphaned in-progress tasks.",
|
|
5667
|
+
runtimes: ["claude"],
|
|
5668
|
+
checkpointRole: "session_summary"
|
|
5669
|
+
},
|
|
5670
|
+
{
|
|
5671
|
+
key: "notification",
|
|
5672
|
+
event: "Notification",
|
|
5673
|
+
commandMarker: EXE_HOOKS.notification,
|
|
5674
|
+
owner: "exe-os",
|
|
5675
|
+
purpose: "Captures runtime notifications and nudges.",
|
|
5676
|
+
runtimes: ["claude"],
|
|
5677
|
+
checkpointRole: "none"
|
|
5678
|
+
},
|
|
5679
|
+
{
|
|
5680
|
+
key: "instructionsLoaded",
|
|
5681
|
+
event: "InstructionsLoaded",
|
|
5682
|
+
commandMarker: EXE_HOOKS.instructionsLoaded,
|
|
5683
|
+
owner: "exe-os",
|
|
5684
|
+
purpose: "Applies runtime instruction post-processing.",
|
|
5685
|
+
runtimes: ["claude"],
|
|
5686
|
+
checkpointRole: "none"
|
|
5687
|
+
}
|
|
5688
|
+
];
|
|
5689
|
+
var LEGACY_SPLIT_POST_TOOL_HOOK_MARKERS = [
|
|
5690
|
+
"dist/hooks/ingest.js",
|
|
5691
|
+
"dist/hooks/error-recall.js",
|
|
5692
|
+
"dist/hooks/ingest-worker.js"
|
|
5693
|
+
];
|
|
5694
|
+
function manifestEntryForCommand(command) {
|
|
5695
|
+
return EXE_HOOK_MANIFEST.find((entry) => command.includes(entry.commandMarker));
|
|
5696
|
+
}
|
|
5697
|
+
|
|
4232
5698
|
// src/bin/exe-doctor.ts
|
|
4233
5699
|
function parseFlags(argv) {
|
|
4234
5700
|
const flags = { fix: false, dryRun: false, verbose: false, conflicts: false };
|
|
@@ -4380,7 +5846,7 @@ async function auditOrphanedProjects(client) {
|
|
|
4380
5846
|
for (const row of result.rows) {
|
|
4381
5847
|
const name = row.project_name;
|
|
4382
5848
|
const count = Number(row.cnt);
|
|
4383
|
-
const exists =
|
|
5849
|
+
const exists = existsSync11(path11.join(home, name)) || existsSync11(path11.join(home, "..", name)) || existsSync11(path11.join(process.cwd(), "..", name));
|
|
4384
5850
|
if (!exists) {
|
|
4385
5851
|
orphans.push({ project_name: name, count });
|
|
4386
5852
|
}
|
|
@@ -4388,18 +5854,18 @@ async function auditOrphanedProjects(client) {
|
|
|
4388
5854
|
return orphans;
|
|
4389
5855
|
}
|
|
4390
5856
|
function auditHookHealth() {
|
|
4391
|
-
const logPath =
|
|
5857
|
+
const logPath = path11.join(
|
|
4392
5858
|
process.env.HOME ?? process.env.USERPROFILE ?? "",
|
|
4393
5859
|
".exe-os",
|
|
4394
5860
|
"logs",
|
|
4395
5861
|
"hooks.log"
|
|
4396
5862
|
);
|
|
4397
|
-
if (!
|
|
5863
|
+
if (!existsSync11(logPath)) {
|
|
4398
5864
|
return { logExists: false, totalLines: 0, errorsLastHour: 0, topPatterns: [] };
|
|
4399
5865
|
}
|
|
4400
5866
|
let content;
|
|
4401
5867
|
try {
|
|
4402
|
-
content =
|
|
5868
|
+
content = readFileSync6(logPath, "utf-8");
|
|
4403
5869
|
} catch {
|
|
4404
5870
|
return { logExists: false, totalLines: 0, errorsLastHour: 0, topPatterns: [] };
|
|
4405
5871
|
}
|
|
@@ -4427,6 +5893,121 @@ function auditHookHealth() {
|
|
|
4427
5893
|
const topPatterns = [...patternCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([pattern, count]) => ({ pattern, count }));
|
|
4428
5894
|
return { logExists: true, totalLines, errorsLastHour, topPatterns };
|
|
4429
5895
|
}
|
|
5896
|
+
function safeReadJson(filePath) {
|
|
5897
|
+
if (!existsSync11(filePath)) return null;
|
|
5898
|
+
try {
|
|
5899
|
+
return JSON.parse(readFileSync6(filePath, "utf-8"));
|
|
5900
|
+
} catch {
|
|
5901
|
+
return null;
|
|
5902
|
+
}
|
|
5903
|
+
}
|
|
5904
|
+
function collectHookCommandsFromClaudeSettings(settings) {
|
|
5905
|
+
const hooks = settings?.hooks;
|
|
5906
|
+
if (!hooks || typeof hooks !== "object") return [];
|
|
5907
|
+
const commands = [];
|
|
5908
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
5909
|
+
if (!Array.isArray(groups)) continue;
|
|
5910
|
+
for (const group of groups) {
|
|
5911
|
+
const hooksForGroup = group?.hooks;
|
|
5912
|
+
if (!Array.isArray(hooksForGroup)) continue;
|
|
5913
|
+
for (const hook of hooksForGroup) {
|
|
5914
|
+
const command = hook?.command;
|
|
5915
|
+
if (typeof command === "string") commands.push({ event, command });
|
|
5916
|
+
}
|
|
5917
|
+
}
|
|
5918
|
+
}
|
|
5919
|
+
return commands;
|
|
5920
|
+
}
|
|
5921
|
+
function collectHookCommandsFromCodexHooks(config) {
|
|
5922
|
+
const commands = [];
|
|
5923
|
+
if (!config || typeof config !== "object") return commands;
|
|
5924
|
+
const root = config;
|
|
5925
|
+
for (const [event, value] of Object.entries(root)) {
|
|
5926
|
+
const entries = Array.isArray(value) ? value : value && typeof value === "object" ? Object.values(value) : [];
|
|
5927
|
+
for (const entry of entries) {
|
|
5928
|
+
if (typeof entry === "string") commands.push({ event, command: entry });
|
|
5929
|
+
const command = entry?.command;
|
|
5930
|
+
if (typeof command === "string") commands.push({ event, command });
|
|
5931
|
+
}
|
|
5932
|
+
}
|
|
5933
|
+
return commands;
|
|
5934
|
+
}
|
|
5935
|
+
function buildHookOwnershipIssues(runtime, commands) {
|
|
5936
|
+
const issues = [];
|
|
5937
|
+
for (const marker of LEGACY_SPLIT_POST_TOOL_HOOK_MARKERS) {
|
|
5938
|
+
const count = commands.filter((cmd) => cmd.command.includes(marker)).length;
|
|
5939
|
+
if (count > 0) {
|
|
5940
|
+
issues.push({
|
|
5941
|
+
runtime,
|
|
5942
|
+
event: "PostToolUse",
|
|
5943
|
+
marker,
|
|
5944
|
+
count,
|
|
5945
|
+
message: `Legacy split PostToolUse hook still installed: ${marker}`
|
|
5946
|
+
});
|
|
5947
|
+
}
|
|
5948
|
+
}
|
|
5949
|
+
for (const entry of EXE_HOOK_MANIFEST.filter((hook) => hook.runtimes.includes(runtime))) {
|
|
5950
|
+
const matching = commands.filter((cmd) => cmd.command.includes(entry.commandMarker));
|
|
5951
|
+
if (matching.length > 1) {
|
|
5952
|
+
issues.push({
|
|
5953
|
+
runtime,
|
|
5954
|
+
event: entry.event,
|
|
5955
|
+
marker: entry.commandMarker,
|
|
5956
|
+
count: matching.length,
|
|
5957
|
+
message: `Duplicate exe-os hook owner for ${entry.event}: ${entry.commandMarker}`
|
|
5958
|
+
});
|
|
5959
|
+
}
|
|
5960
|
+
for (const cmd of matching) {
|
|
5961
|
+
if (cmd.event !== entry.event) {
|
|
5962
|
+
issues.push({
|
|
5963
|
+
runtime,
|
|
5964
|
+
event: cmd.event,
|
|
5965
|
+
marker: entry.commandMarker,
|
|
5966
|
+
count: 1,
|
|
5967
|
+
message: `exe-os hook ${entry.commandMarker} is registered under ${cmd.event}, expected ${entry.event}`
|
|
5968
|
+
});
|
|
5969
|
+
}
|
|
5970
|
+
}
|
|
5971
|
+
}
|
|
5972
|
+
for (const cmd of commands) {
|
|
5973
|
+
if (!cmd.command.includes("dist/hooks/")) continue;
|
|
5974
|
+
if (!cmd.command.includes("exe-os")) continue;
|
|
5975
|
+
const entry = manifestEntryForCommand(cmd.command);
|
|
5976
|
+
const isLegacy = LEGACY_SPLIT_POST_TOOL_HOOK_MARKERS.some((marker) => cmd.command.includes(marker));
|
|
5977
|
+
if (!entry && !isLegacy) {
|
|
5978
|
+
issues.push({
|
|
5979
|
+
runtime,
|
|
5980
|
+
event: cmd.event,
|
|
5981
|
+
marker: "dist/hooks/",
|
|
5982
|
+
count: 1,
|
|
5983
|
+
message: `Unknown exe-os hook command not present in ownership manifest: ${cmd.command.slice(0, 160)}`
|
|
5984
|
+
});
|
|
5985
|
+
}
|
|
5986
|
+
}
|
|
5987
|
+
return issues;
|
|
5988
|
+
}
|
|
5989
|
+
function auditHookOwnership() {
|
|
5990
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
5991
|
+
const checkedFiles = [];
|
|
5992
|
+
const issues = [];
|
|
5993
|
+
const claudeSettingsPath = path11.join(home, ".claude", "settings.json");
|
|
5994
|
+
const claudeSettings = safeReadJson(claudeSettingsPath);
|
|
5995
|
+
if (claudeSettings) {
|
|
5996
|
+
checkedFiles.push(claudeSettingsPath);
|
|
5997
|
+
issues.push(...buildHookOwnershipIssues("claude", collectHookCommandsFromClaudeSettings(claudeSettings)));
|
|
5998
|
+
}
|
|
5999
|
+
const codexHooksPath = path11.join(home, ".codex", "hooks.json");
|
|
6000
|
+
const codexHooks = safeReadJson(codexHooksPath);
|
|
6001
|
+
if (codexHooks) {
|
|
6002
|
+
checkedFiles.push(codexHooksPath);
|
|
6003
|
+
issues.push(...buildHookOwnershipIssues("codex", collectHookCommandsFromCodexHooks(codexHooks)));
|
|
6004
|
+
}
|
|
6005
|
+
return {
|
|
6006
|
+
checkedFiles,
|
|
6007
|
+
issues,
|
|
6008
|
+
staleLegacyHooks: issues.filter((issue) => issue.message.includes("Legacy split"))
|
|
6009
|
+
};
|
|
6010
|
+
}
|
|
4430
6011
|
async function auditShards() {
|
|
4431
6012
|
try {
|
|
4432
6013
|
const { auditShardHealth: auditShardHealth2 } = await Promise.resolve().then(() => (init_shard_manager(), shard_manager_exports));
|
|
@@ -4442,16 +6023,33 @@ async function auditShards() {
|
|
|
4442
6023
|
return { total: 0, ok: 0, unreadable: 0, archived: 0, unreadableNames: [] };
|
|
4443
6024
|
}
|
|
4444
6025
|
}
|
|
6026
|
+
async function auditKeyHealth() {
|
|
6027
|
+
try {
|
|
6028
|
+
const { getMasterKey: getMasterKey2 } = await Promise.resolve().then(() => (init_keychain(), keychain_exports));
|
|
6029
|
+
const { getKeyBackupStatus: getKeyBackupStatus2 } = await Promise.resolve().then(() => (init_key_backup_status(), key_backup_status_exports));
|
|
6030
|
+
const key = await getMasterKey2();
|
|
6031
|
+
const backup = getKeyBackupStatus2();
|
|
6032
|
+
return {
|
|
6033
|
+
masterKeyPresent: Boolean(key),
|
|
6034
|
+
recoveryBackupMarked: backup.exists,
|
|
6035
|
+
recoveryBackupConfirmedAt: backup.confirmedAt,
|
|
6036
|
+
recoveryBackupSource: backup.source
|
|
6037
|
+
};
|
|
6038
|
+
} catch {
|
|
6039
|
+
return { masterKeyPresent: false, recoveryBackupMarked: false };
|
|
6040
|
+
}
|
|
6041
|
+
}
|
|
4445
6042
|
async function runAudit(client, flags) {
|
|
4446
6043
|
const runConflicts = flags.conflicts || process.env.EXE_AUDIT_CONFLICTS === "1";
|
|
4447
|
-
const [stats, nullVectors, duplicates, bloated, fts, orphanedProjects, shards] = await Promise.all([
|
|
6044
|
+
const [stats, nullVectors, duplicates, bloated, fts, orphanedProjects, shards, keyHealth] = await Promise.all([
|
|
4448
6045
|
auditStats(client, flags),
|
|
4449
6046
|
auditNullVectors(client, flags),
|
|
4450
6047
|
auditDuplicates(client, flags),
|
|
4451
6048
|
auditBloated(client, flags),
|
|
4452
6049
|
auditFts(client),
|
|
4453
6050
|
auditOrphanedProjects(client),
|
|
4454
|
-
auditShards()
|
|
6051
|
+
auditShards(),
|
|
6052
|
+
auditKeyHealth()
|
|
4455
6053
|
]);
|
|
4456
6054
|
let conflicts;
|
|
4457
6055
|
if (runConflicts) {
|
|
@@ -4471,7 +6069,8 @@ async function runAudit(client, flags) {
|
|
|
4471
6069
|
}
|
|
4472
6070
|
const duplicateCount = duplicates.reduce((sum, d) => sum + d.delete_ids.length, 0);
|
|
4473
6071
|
const hookHealth = auditHookHealth();
|
|
4474
|
-
|
|
6072
|
+
const hookOwnership = auditHookOwnership();
|
|
6073
|
+
return { stats, nullVectors, duplicates, duplicateCount, bloated, fts, orphanedProjects, conflicts, hookHealth, hookOwnership, shards, keyHealth };
|
|
4475
6074
|
}
|
|
4476
6075
|
function indicator(value, warn) {
|
|
4477
6076
|
if (value === 0) return "\u{1F7E2}";
|
|
@@ -4519,6 +6118,15 @@ function formatReport(report, flags) {
|
|
|
4519
6118
|
lines.push(`${indicator(report.bloated.length, 20)} Bloated (>5KB): ${fmtNum(report.bloated.length)} / ${fmtNum(s.total)} (${pct(report.bloated.length, s.total)})`);
|
|
4520
6119
|
const ftsIndicator = report.fts.inSync ? "\u{1F7E2}" : "\u{1F534}";
|
|
4521
6120
|
lines.push(`${ftsIndicator} FTS index: ${report.fts.inSync ? "in sync" : "OUT OF SYNC"} (${fmtNum(report.fts.memoryCount)} / ${fmtNum(report.fts.ftsCount)})`);
|
|
6121
|
+
const kh = report.keyHealth;
|
|
6122
|
+
if (!kh.masterKeyPresent) {
|
|
6123
|
+
lines.push("\u{1F534} Recovery key: master key missing \u2014 import phrase or run setup before syncing");
|
|
6124
|
+
} else if (!kh.recoveryBackupMarked) {
|
|
6125
|
+
lines.push("\u{1F534} Recovery backup: not confirmed \u2014 run `exe-os link export --local-terminal-only` and save the 24-word phrase");
|
|
6126
|
+
} else {
|
|
6127
|
+
const suffix = kh.recoveryBackupConfirmedAt ? ` (${kh.recoveryBackupConfirmedAt.slice(0, 10)}${kh.recoveryBackupSource ? ` via ${kh.recoveryBackupSource}` : ""})` : "";
|
|
6128
|
+
lines.push(`\u{1F7E2} Recovery backup: confirmed${suffix}`);
|
|
6129
|
+
}
|
|
4522
6130
|
if (report.orphanedProjects.length > 0) {
|
|
4523
6131
|
const orphanList = report.orphanedProjects.map((o) => `${o.project_name} \u2014 ${o.count} memories`).join(", ");
|
|
4524
6132
|
lines.push(`\u2139\uFE0F Orphaned projects: ${report.orphanedProjects.length} (${orphanList})`);
|
|
@@ -4550,6 +6158,15 @@ function formatReport(report, flags) {
|
|
|
4550
6158
|
lines.push(` ${p.count}x: ${p.pattern}`);
|
|
4551
6159
|
}
|
|
4552
6160
|
}
|
|
6161
|
+
const ho = report.hookOwnership;
|
|
6162
|
+
if (ho.issues.length === 0) {
|
|
6163
|
+
lines.push(`\u{1F7E2} Hook ownership: ${ho.checkedFiles.length > 0 ? "manifest clean" : "no local hook config found"}`);
|
|
6164
|
+
} else {
|
|
6165
|
+
lines.push(`\u{1F534} Hook ownership: ${fmtNum(ho.issues.length)} issue(s)`);
|
|
6166
|
+
for (const issue of ho.issues.slice(0, 5)) {
|
|
6167
|
+
lines.push(` [${issue.runtime}/${issue.event}] ${issue.message}`);
|
|
6168
|
+
}
|
|
6169
|
+
}
|
|
4553
6170
|
const sh = report.shards;
|
|
4554
6171
|
if (sh.total > 0) {
|
|
4555
6172
|
if (sh.unreadable === 0) {
|
|
@@ -4619,6 +6236,9 @@ function formatReport(report, flags) {
|
|
|
4619
6236
|
if (report.conflicts.superseded > 0) {
|
|
4620
6237
|
recs.push(`${fmtNum(report.conflicts.superseded)} superseded memories can be deactivated`);
|
|
4621
6238
|
}
|
|
6239
|
+
if (report.hookOwnership.issues.length > 0) {
|
|
6240
|
+
recs.push(`Run exe-os install to refresh hook config; remove stale exe-os hook commands if they remain`);
|
|
6241
|
+
}
|
|
4622
6242
|
if (recs.length > 0) {
|
|
4623
6243
|
lines.push("Recommendations:");
|
|
4624
6244
|
for (const r of recs) {
|
|
@@ -4640,7 +6260,7 @@ async function fixNullVectors() {
|
|
|
4640
6260
|
}
|
|
4641
6261
|
}
|
|
4642
6262
|
const npmRoot = (await import("child_process")).execSync("npm root -g", { encoding: "utf8" }).trim();
|
|
4643
|
-
const backfillPath =
|
|
6263
|
+
const backfillPath = path11.join(npmRoot, "exe-os", "dist", "bin", "backfill-vectors.js");
|
|
4644
6264
|
return new Promise((resolve, reject) => {
|
|
4645
6265
|
const child = spawn2("node", [backfillPath], { stdio: "inherit" });
|
|
4646
6266
|
if (child.pid) registerWorkerPid2(child.pid);
|
|
@@ -4752,8 +6372,8 @@ function splitAtSentences(text, maxChunkSize) {
|
|
|
4752
6372
|
}
|
|
4753
6373
|
async function main(argv = process.argv.slice(2)) {
|
|
4754
6374
|
const flags = parseFlags(argv);
|
|
4755
|
-
await
|
|
4756
|
-
const client =
|
|
6375
|
+
const { fastDbInit: fastDbInit2 } = await Promise.resolve().then(() => (init_fast_db_init(), fast_db_init_exports));
|
|
6376
|
+
const client = await fastDbInit2();
|
|
4757
6377
|
const report = await runAudit(client, flags);
|
|
4758
6378
|
console.log(formatReport(report, flags));
|
|
4759
6379
|
if (flags.fix || flags.dryRun) {
|
|
@@ -4821,6 +6441,8 @@ export {
|
|
|
4821
6441
|
auditDuplicates,
|
|
4822
6442
|
auditFts,
|
|
4823
6443
|
auditHookHealth,
|
|
6444
|
+
auditHookOwnership,
|
|
6445
|
+
auditKeyHealth,
|
|
4824
6446
|
auditNullVectors,
|
|
4825
6447
|
auditOrphanedProjects,
|
|
4826
6448
|
auditShards,
|