engrm 0.4.42 → 0.4.44
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/README.md +4 -1
- package/dist/cli.js +300 -28
- package/dist/hooks/elicitation-result.js +31 -12
- package/dist/hooks/post-tool-use.js +108 -38
- package/dist/hooks/pre-compact.js +44 -13
- package/dist/hooks/sentinel.js +31 -12
- package/dist/hooks/session-start.js +170 -16
- package/dist/hooks/stop.js +258 -152
- package/dist/hooks/user-prompt-submit.js +57 -14
- package/dist/server.js +248 -31
- package/opencode/README.md +6 -6
- package/opencode/install-or-update-opencode-plugin.sh +7 -1
- package/opencode/opencode.example.json +2 -2
- package/package.json +1 -1
package/dist/hooks/stop.js
CHANGED
|
@@ -399,7 +399,8 @@ function createDefaultConfig() {
|
|
|
399
399
|
project_name: "shared-experience",
|
|
400
400
|
namespace: "",
|
|
401
401
|
api_key: ""
|
|
402
|
-
}
|
|
402
|
+
},
|
|
403
|
+
tool_profile: "full"
|
|
403
404
|
};
|
|
404
405
|
}
|
|
405
406
|
function loadConfig() {
|
|
@@ -470,7 +471,8 @@ function loadConfig() {
|
|
|
470
471
|
project_name: asString(config["fleet"]?.["project_name"], defaults.fleet.project_name),
|
|
471
472
|
namespace: asString(config["fleet"]?.["namespace"], defaults.fleet.namespace),
|
|
472
473
|
api_key: asString(config["fleet"]?.["api_key"], defaults.fleet.api_key)
|
|
473
|
-
}
|
|
474
|
+
},
|
|
475
|
+
tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
|
|
474
476
|
};
|
|
475
477
|
}
|
|
476
478
|
function saveConfig(config) {
|
|
@@ -525,6 +527,11 @@ function asObserverMode(value, fallback) {
|
|
|
525
527
|
return value;
|
|
526
528
|
return fallback;
|
|
527
529
|
}
|
|
530
|
+
function asToolProfile(value, fallback) {
|
|
531
|
+
if (value === "full" || value === "memory")
|
|
532
|
+
return value;
|
|
533
|
+
return fallback;
|
|
534
|
+
}
|
|
528
535
|
function asTeams(value, fallback) {
|
|
529
536
|
if (!Array.isArray(value))
|
|
530
537
|
return fallback;
|
|
@@ -1158,6 +1165,7 @@ function ensureObservationTypes(db) {
|
|
|
1158
1165
|
DROP TABLE observations;
|
|
1159
1166
|
ALTER TABLE observations_repair RENAME TO observations;
|
|
1160
1167
|
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
|
|
1168
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
|
|
1161
1169
|
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
1162
1170
|
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
1163
1171
|
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
@@ -1358,6 +1366,7 @@ class MemDatabase {
|
|
|
1358
1366
|
this.db = openDatabase(dbPath);
|
|
1359
1367
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1360
1368
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
1369
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
1361
1370
|
this.vecAvailable = this.loadVecExtension();
|
|
1362
1371
|
runMigrations(this.db);
|
|
1363
1372
|
ensureObservationTypes(this.db);
|
|
@@ -1379,8 +1388,16 @@ class MemDatabase {
|
|
|
1379
1388
|
this.db.close();
|
|
1380
1389
|
}
|
|
1381
1390
|
upsertProject(project) {
|
|
1391
|
+
const canonicalId = project.canonical_id?.trim();
|
|
1392
|
+
const name = project.name?.trim();
|
|
1393
|
+
if (!canonicalId) {
|
|
1394
|
+
throw new Error("Project canonical_id is required");
|
|
1395
|
+
}
|
|
1396
|
+
if (!name) {
|
|
1397
|
+
throw new Error("Project name is required");
|
|
1398
|
+
}
|
|
1382
1399
|
const now = Math.floor(Date.now() / 1000);
|
|
1383
|
-
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(
|
|
1400
|
+
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
|
|
1384
1401
|
if (existing) {
|
|
1385
1402
|
this.db.query(`UPDATE projects SET
|
|
1386
1403
|
local_path = COALESCE(?, local_path),
|
|
@@ -1395,7 +1412,7 @@ class MemDatabase {
|
|
|
1395
1412
|
};
|
|
1396
1413
|
}
|
|
1397
1414
|
const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
|
|
1398
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
|
1415
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
|
|
1399
1416
|
return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
|
|
1400
1417
|
}
|
|
1401
1418
|
getProjectByCanonicalId(canonicalId) {
|
|
@@ -2207,6 +2224,96 @@ function formatRiskTrafficLight(result) {
|
|
|
2207
2224
|
return `${icon} Risk: ${result.score}/100 (${result.level})`;
|
|
2208
2225
|
}
|
|
2209
2226
|
|
|
2227
|
+
// src/sync/auth.ts
|
|
2228
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2229
|
+
|
|
2230
|
+
// src/storage/outbox.ts
|
|
2231
|
+
function getPendingEntries(db, limit = 50) {
|
|
2232
|
+
const now = Math.floor(Date.now() / 1000);
|
|
2233
|
+
return db.db.query(`SELECT * FROM sync_outbox
|
|
2234
|
+
WHERE (status = 'pending')
|
|
2235
|
+
OR (status = 'failed' AND retry_count < max_retries AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?))
|
|
2236
|
+
ORDER BY created_at_epoch ASC
|
|
2237
|
+
LIMIT ?`).all(now, limit);
|
|
2238
|
+
}
|
|
2239
|
+
function markSyncing(db, entryId) {
|
|
2240
|
+
const now = Math.floor(Date.now() / 1000);
|
|
2241
|
+
db.db.query("UPDATE sync_outbox SET status = 'syncing', next_retry_epoch = ? WHERE id = ?").run(now, entryId);
|
|
2242
|
+
}
|
|
2243
|
+
function markSynced(db, entryId) {
|
|
2244
|
+
const now = Math.floor(Date.now() / 1000);
|
|
2245
|
+
db.db.query("UPDATE sync_outbox SET status = 'synced', synced_at_epoch = ?, next_retry_epoch = NULL, last_error = NULL WHERE id = ?").run(now, entryId);
|
|
2246
|
+
}
|
|
2247
|
+
function markFailed(db, entryId, error) {
|
|
2248
|
+
const now = Math.floor(Date.now() / 1000);
|
|
2249
|
+
db.db.query(`UPDATE sync_outbox SET
|
|
2250
|
+
status = 'failed',
|
|
2251
|
+
retry_count = retry_count + 1,
|
|
2252
|
+
last_error = ?,
|
|
2253
|
+
next_retry_epoch = ? + MIN(30 * (1 << retry_count), 3600)
|
|
2254
|
+
WHERE id = ?`).run(error, now, entryId);
|
|
2255
|
+
}
|
|
2256
|
+
function classifyOutboxFailure(error) {
|
|
2257
|
+
const normalized = error.toLowerCase();
|
|
2258
|
+
if (normalized.includes("401") || normalized.includes("invalid or missing credentials")) {
|
|
2259
|
+
return "auth";
|
|
2260
|
+
}
|
|
2261
|
+
if (normalized.includes("429") || normalized.includes("rate limit")) {
|
|
2262
|
+
return "rate_limit";
|
|
2263
|
+
}
|
|
2264
|
+
if (normalized.includes("timeout") || normalized.includes("abort")) {
|
|
2265
|
+
return "timeout";
|
|
2266
|
+
}
|
|
2267
|
+
if (normalized.includes("network") || normalized.includes("fetch") || normalized.includes("econn")) {
|
|
2268
|
+
return "network";
|
|
2269
|
+
}
|
|
2270
|
+
if (normalized.includes("400") || normalized.includes("422") || normalized.includes("validation")) {
|
|
2271
|
+
return "validation";
|
|
2272
|
+
}
|
|
2273
|
+
return "other";
|
|
2274
|
+
}
|
|
2275
|
+
function resetFailedEntries(db) {
|
|
2276
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
2277
|
+
SET status = 'pending',
|
|
2278
|
+
retry_count = 0,
|
|
2279
|
+
last_error = NULL,
|
|
2280
|
+
next_retry_epoch = NULL
|
|
2281
|
+
WHERE status = 'failed'`).run();
|
|
2282
|
+
return result.changes;
|
|
2283
|
+
}
|
|
2284
|
+
function resetFailedEntriesMatching(db, predicate) {
|
|
2285
|
+
const rows = db.db.query(`SELECT id, last_error
|
|
2286
|
+
FROM sync_outbox
|
|
2287
|
+
WHERE status = 'failed'`).all();
|
|
2288
|
+
const matchingIds = rows.filter((row) => row.last_error && predicate(row.last_error)).map((row) => row.id);
|
|
2289
|
+
if (matchingIds.length === 0)
|
|
2290
|
+
return 0;
|
|
2291
|
+
const placeholders = matchingIds.map(() => "?").join(", ");
|
|
2292
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
2293
|
+
SET status = 'pending',
|
|
2294
|
+
retry_count = 0,
|
|
2295
|
+
last_error = NULL,
|
|
2296
|
+
next_retry_epoch = NULL
|
|
2297
|
+
WHERE id IN (${placeholders})`).run(...matchingIds);
|
|
2298
|
+
return result.changes;
|
|
2299
|
+
}
|
|
2300
|
+
function resetSyncingEntries(db) {
|
|
2301
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
2302
|
+
SET status = 'pending',
|
|
2303
|
+
next_retry_epoch = NULL
|
|
2304
|
+
WHERE status = 'syncing'`).run();
|
|
2305
|
+
return result.changes;
|
|
2306
|
+
}
|
|
2307
|
+
function resetStaleSyncingEntries(db, maxAgeSeconds = 300) {
|
|
2308
|
+
const cutoff = Math.floor(Date.now() / 1000) - maxAgeSeconds;
|
|
2309
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
2310
|
+
SET status = 'pending',
|
|
2311
|
+
next_retry_epoch = NULL
|
|
2312
|
+
WHERE status = 'syncing'
|
|
2313
|
+
AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?)`).run(cutoff);
|
|
2314
|
+
return result.changes;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2210
2317
|
// src/sync/auth.ts
|
|
2211
2318
|
var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
|
|
2212
2319
|
function normalizeBaseUrl(url) {
|
|
@@ -2238,6 +2345,36 @@ function getBaseUrl(config) {
|
|
|
2238
2345
|
}
|
|
2239
2346
|
return null;
|
|
2240
2347
|
}
|
|
2348
|
+
function getAuthFingerprint(config) {
|
|
2349
|
+
const apiKey = getApiKey(config);
|
|
2350
|
+
const baseUrl = getBaseUrl(config);
|
|
2351
|
+
if (!apiKey || !baseUrl)
|
|
2352
|
+
return null;
|
|
2353
|
+
return createHash3("sha256").update(`${baseUrl}
|
|
2354
|
+
${apiKey}
|
|
2355
|
+
${config.namespace}
|
|
2356
|
+
${config.site_id}`).digest("hex");
|
|
2357
|
+
}
|
|
2358
|
+
function recoverOutboxAfterAuthChange(db, config) {
|
|
2359
|
+
const fingerprint = getAuthFingerprint(config);
|
|
2360
|
+
if (!fingerprint) {
|
|
2361
|
+
const staleSyncingReset2 = resetStaleSyncingEntries(db);
|
|
2362
|
+
return { fingerprintChanged: false, failedReset: 0, authFailedReset: 0, syncingReset: 0, staleSyncingReset: staleSyncingReset2 };
|
|
2363
|
+
}
|
|
2364
|
+
const key = "sync_auth_fingerprint";
|
|
2365
|
+
const previous = db.getSyncState(key);
|
|
2366
|
+
const fingerprintChanged = previous !== fingerprint;
|
|
2367
|
+
if (!fingerprintChanged) {
|
|
2368
|
+
const authFailedReset = resetFailedEntriesMatching(db, (error) => classifyOutboxFailure(error) === "auth");
|
|
2369
|
+
const staleSyncingReset2 = resetStaleSyncingEntries(db);
|
|
2370
|
+
return { fingerprintChanged: false, failedReset: 0, authFailedReset, syncingReset: 0, staleSyncingReset: staleSyncingReset2 };
|
|
2371
|
+
}
|
|
2372
|
+
const failedReset = resetFailedEntries(db);
|
|
2373
|
+
const syncingReset = resetSyncingEntries(db);
|
|
2374
|
+
const staleSyncingReset = 0;
|
|
2375
|
+
db.setSyncState(key, fingerprint);
|
|
2376
|
+
return { fingerprintChanged: true, failedReset, authFailedReset: 0, syncingReset, staleSyncingReset };
|
|
2377
|
+
}
|
|
2241
2378
|
function buildSourceId(config, localId, type = "obs") {
|
|
2242
2379
|
return `${config.user_id}-${config.device_id}-${type}-${localId}`;
|
|
2243
2380
|
}
|
|
@@ -2271,6 +2408,7 @@ class VectorClient {
|
|
|
2271
2408
|
apiKey;
|
|
2272
2409
|
siteId;
|
|
2273
2410
|
namespace;
|
|
2411
|
+
timeoutMs;
|
|
2274
2412
|
constructor(config, overrides = {}) {
|
|
2275
2413
|
const baseUrl = getBaseUrl(config);
|
|
2276
2414
|
const apiKey = overrides.apiKey ?? getApiKey(config);
|
|
@@ -2281,6 +2419,7 @@ class VectorClient {
|
|
|
2281
2419
|
this.apiKey = apiKey;
|
|
2282
2420
|
this.siteId = overrides.siteId ?? config.site_id;
|
|
2283
2421
|
this.namespace = overrides.namespace ?? config.namespace;
|
|
2422
|
+
this.timeoutMs = overrides.timeoutMs ?? 1e4;
|
|
2284
2423
|
}
|
|
2285
2424
|
static isConfigured(config) {
|
|
2286
2425
|
return getApiKey(config) !== null && getBaseUrl(config) !== null;
|
|
@@ -2343,6 +2482,7 @@ class VectorClient {
|
|
|
2343
2482
|
if (body && method !== "GET") {
|
|
2344
2483
|
init.body = JSON.stringify(body);
|
|
2345
2484
|
}
|
|
2485
|
+
init.signal = AbortSignal.timeout(this.timeoutMs);
|
|
2346
2486
|
const response = await fetch(url, init);
|
|
2347
2487
|
if (!response.ok) {
|
|
2348
2488
|
const text = await response.text().catch(() => "");
|
|
@@ -2368,32 +2508,6 @@ class VectorApiError extends Error {
|
|
|
2368
2508
|
}
|
|
2369
2509
|
}
|
|
2370
2510
|
|
|
2371
|
-
// src/storage/outbox.ts
|
|
2372
|
-
function getPendingEntries(db, limit = 50) {
|
|
2373
|
-
const now = Math.floor(Date.now() / 1000);
|
|
2374
|
-
return db.db.query(`SELECT * FROM sync_outbox
|
|
2375
|
-
WHERE (status = 'pending')
|
|
2376
|
-
OR (status = 'failed' AND retry_count < max_retries AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?))
|
|
2377
|
-
ORDER BY created_at_epoch ASC
|
|
2378
|
-
LIMIT ?`).all(now, limit);
|
|
2379
|
-
}
|
|
2380
|
-
function markSyncing(db, entryId) {
|
|
2381
|
-
db.db.query("UPDATE sync_outbox SET status = 'syncing' WHERE id = ?").run(entryId);
|
|
2382
|
-
}
|
|
2383
|
-
function markSynced(db, entryId) {
|
|
2384
|
-
const now = Math.floor(Date.now() / 1000);
|
|
2385
|
-
db.db.query("UPDATE sync_outbox SET status = 'synced', synced_at_epoch = ? WHERE id = ?").run(now, entryId);
|
|
2386
|
-
}
|
|
2387
|
-
function markFailed(db, entryId, error) {
|
|
2388
|
-
const now = Math.floor(Date.now() / 1000);
|
|
2389
|
-
db.db.query(`UPDATE sync_outbox SET
|
|
2390
|
-
status = 'failed',
|
|
2391
|
-
retry_count = retry_count + 1,
|
|
2392
|
-
last_error = ?,
|
|
2393
|
-
next_retry_epoch = ? + MIN(30 * (1 << retry_count), 3600)
|
|
2394
|
-
WHERE id = ?`).run(error, now, entryId);
|
|
2395
|
-
}
|
|
2396
|
-
|
|
2397
2511
|
// src/intelligence/value-signals.ts
|
|
2398
2512
|
var LESSON_TYPES = new Set(["bugfix", "decision", "pattern"]);
|
|
2399
2513
|
function computeSessionValueSignals(observations, securityFindings = []) {
|
|
@@ -2474,7 +2588,19 @@ function buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolN
|
|
|
2474
2588
|
return null;
|
|
2475
2589
|
}
|
|
2476
2590
|
function compactLine(value) {
|
|
2477
|
-
|
|
2591
|
+
if (value === null || value === undefined)
|
|
2592
|
+
return null;
|
|
2593
|
+
let text;
|
|
2594
|
+
if (typeof value === "string") {
|
|
2595
|
+
text = value;
|
|
2596
|
+
} else {
|
|
2597
|
+
try {
|
|
2598
|
+
text = JSON.stringify(value);
|
|
2599
|
+
} catch {
|
|
2600
|
+
text = String(value);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
2478
2604
|
if (!trimmed)
|
|
2479
2605
|
return null;
|
|
2480
2606
|
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
@@ -2811,7 +2937,7 @@ function buildSummaryVectorDocument(summary, config, project, targetOrObservatio
|
|
|
2811
2937
|
}
|
|
2812
2938
|
};
|
|
2813
2939
|
}
|
|
2814
|
-
async function pushOutbox(db, config, batchSize = 50) {
|
|
2940
|
+
async function pushOutbox(db, config, batchSize = 50, options = {}) {
|
|
2815
2941
|
const entries = getPendingEntries(db, batchSize);
|
|
2816
2942
|
let pushed = 0;
|
|
2817
2943
|
let failed = 0;
|
|
@@ -2922,7 +3048,8 @@ async function pushOutbox(db, config, batchSize = 50) {
|
|
|
2922
3048
|
const client = new VectorClient(config, {
|
|
2923
3049
|
apiKey: target.apiKey,
|
|
2924
3050
|
namespace: target.namespace,
|
|
2925
|
-
siteId: target.siteId
|
|
3051
|
+
siteId: target.siteId,
|
|
3052
|
+
timeoutMs: options.timeoutMs
|
|
2926
3053
|
});
|
|
2927
3054
|
try {
|
|
2928
3055
|
await client.batchIngest(items.map((b) => b.doc));
|
|
@@ -3003,118 +3130,21 @@ function summarizeObservationSourceTools(observations) {
|
|
|
3003
3130
|
});
|
|
3004
3131
|
}
|
|
3005
3132
|
|
|
3006
|
-
// src/embeddings/embedder.ts
|
|
3007
|
-
var _available = null;
|
|
3008
|
-
var _pipeline = null;
|
|
3009
|
-
var MODEL_NAME = "Xenova/all-MiniLM-L6-v2";
|
|
3010
|
-
async function embedText(text) {
|
|
3011
|
-
const pipe = await getPipeline();
|
|
3012
|
-
if (!pipe)
|
|
3013
|
-
return null;
|
|
3014
|
-
try {
|
|
3015
|
-
const output = await pipe(text, { pooling: "mean", normalize: true });
|
|
3016
|
-
return new Float32Array(output.data);
|
|
3017
|
-
} catch {
|
|
3018
|
-
return null;
|
|
3019
|
-
}
|
|
3020
|
-
}
|
|
3021
|
-
function composeEmbeddingText(obs) {
|
|
3022
|
-
const parts = [obs.title];
|
|
3023
|
-
if (obs.narrative)
|
|
3024
|
-
parts.push(obs.narrative);
|
|
3025
|
-
if (obs.facts) {
|
|
3026
|
-
try {
|
|
3027
|
-
const facts = JSON.parse(obs.facts);
|
|
3028
|
-
if (Array.isArray(facts) && facts.length > 0) {
|
|
3029
|
-
parts.push(facts.map((f) => `- ${f}`).join(`
|
|
3030
|
-
`));
|
|
3031
|
-
}
|
|
3032
|
-
} catch {
|
|
3033
|
-
parts.push(obs.facts);
|
|
3034
|
-
}
|
|
3035
|
-
}
|
|
3036
|
-
if (obs.concepts) {
|
|
3037
|
-
try {
|
|
3038
|
-
const concepts = JSON.parse(obs.concepts);
|
|
3039
|
-
if (Array.isArray(concepts) && concepts.length > 0) {
|
|
3040
|
-
parts.push(concepts.join(", "));
|
|
3041
|
-
}
|
|
3042
|
-
} catch {}
|
|
3043
|
-
}
|
|
3044
|
-
return parts.join(`
|
|
3045
|
-
|
|
3046
|
-
`);
|
|
3047
|
-
}
|
|
3048
|
-
function composeChatEmbeddingText(text) {
|
|
3049
|
-
return text.replace(/\s+/g, " ").trim().slice(0, 2000);
|
|
3050
|
-
}
|
|
3051
|
-
async function getPipeline() {
|
|
3052
|
-
if (_pipeline)
|
|
3053
|
-
return _pipeline;
|
|
3054
|
-
if (_available === false)
|
|
3055
|
-
return null;
|
|
3056
|
-
try {
|
|
3057
|
-
const { pipeline } = await import("@xenova/transformers");
|
|
3058
|
-
_pipeline = await pipeline("feature-extraction", MODEL_NAME);
|
|
3059
|
-
_available = true;
|
|
3060
|
-
return _pipeline;
|
|
3061
|
-
} catch (err) {
|
|
3062
|
-
_available = false;
|
|
3063
|
-
console.error(`[engrm] Local embedding model unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
3064
|
-
return null;
|
|
3065
|
-
}
|
|
3066
|
-
}
|
|
3067
|
-
|
|
3068
|
-
// src/sync/pull.ts
|
|
3069
|
-
async function pullSettings(client, config) {
|
|
3070
|
-
try {
|
|
3071
|
-
const settings = await client.fetchSettings();
|
|
3072
|
-
if (!settings)
|
|
3073
|
-
return false;
|
|
3074
|
-
let changed = false;
|
|
3075
|
-
if (settings.transcript_analysis !== undefined) {
|
|
3076
|
-
const ta = settings.transcript_analysis;
|
|
3077
|
-
if (typeof ta === "object" && ta !== null) {
|
|
3078
|
-
const taObj = ta;
|
|
3079
|
-
if (taObj.enabled !== undefined && taObj.enabled !== config.transcript_analysis.enabled) {
|
|
3080
|
-
config.transcript_analysis.enabled = !!taObj.enabled;
|
|
3081
|
-
changed = true;
|
|
3082
|
-
}
|
|
3083
|
-
}
|
|
3084
|
-
}
|
|
3085
|
-
if (settings.observer !== undefined) {
|
|
3086
|
-
const obs = settings.observer;
|
|
3087
|
-
if (typeof obs === "object" && obs !== null) {
|
|
3088
|
-
const obsObj = obs;
|
|
3089
|
-
if (obsObj.enabled !== undefined && obsObj.enabled !== config.observer.enabled) {
|
|
3090
|
-
config.observer.enabled = !!obsObj.enabled;
|
|
3091
|
-
changed = true;
|
|
3092
|
-
}
|
|
3093
|
-
if (obsObj.model !== undefined && typeof obsObj.model === "string" && obsObj.model !== config.observer.model) {
|
|
3094
|
-
config.observer.model = obsObj.model;
|
|
3095
|
-
changed = true;
|
|
3096
|
-
}
|
|
3097
|
-
}
|
|
3098
|
-
}
|
|
3099
|
-
if (changed) {
|
|
3100
|
-
saveConfig(config);
|
|
3101
|
-
}
|
|
3102
|
-
return changed;
|
|
3103
|
-
} catch {
|
|
3104
|
-
return false;
|
|
3105
|
-
}
|
|
3106
|
-
}
|
|
3107
|
-
|
|
3108
3133
|
// src/sync/push-once.ts
|
|
3109
|
-
|
|
3134
|
+
function recordNowEpoch(db, key) {
|
|
3135
|
+
db.setSyncState(key, String(Math.floor(Date.now() / 1000)));
|
|
3136
|
+
}
|
|
3137
|
+
async function pushOnce(db, config, options = {}) {
|
|
3110
3138
|
if (!config.sync.enabled)
|
|
3111
3139
|
return 0;
|
|
3112
3140
|
if (!VectorClient.isConfigured(config))
|
|
3113
3141
|
return 0;
|
|
3114
3142
|
try {
|
|
3115
|
-
|
|
3116
|
-
const result = await pushOutbox(db, config, config.sync.batch_size);
|
|
3117
|
-
|
|
3143
|
+
recoverOutboxAfterAuthChange(db, config);
|
|
3144
|
+
const result = await pushOutbox(db, config, config.sync.batch_size, { timeoutMs: options.timeoutMs ?? 4000 });
|
|
3145
|
+
if (result.pushed > 0) {
|
|
3146
|
+
recordNowEpoch(db, "last_push_epoch");
|
|
3147
|
+
}
|
|
3118
3148
|
return result.pushed;
|
|
3119
3149
|
} catch {
|
|
3120
3150
|
return 0;
|
|
@@ -3338,7 +3368,7 @@ function buildBeacon(db, config, sessionId, metrics) {
|
|
|
3338
3368
|
sentinel_used: valueSignals.security_findings_count > 0,
|
|
3339
3369
|
risk_score: riskScore,
|
|
3340
3370
|
stacks_detected: stacks,
|
|
3341
|
-
client_version: "0.4.
|
|
3371
|
+
client_version: "0.4.44",
|
|
3342
3372
|
context_observations_injected: metrics?.contextObsInjected ?? 0,
|
|
3343
3373
|
context_total_available: metrics?.contextTotalAvailable ?? 0,
|
|
3344
3374
|
recall_attempts: metrics?.recallAttempts ?? 0,
|
|
@@ -3470,10 +3500,11 @@ function readProjectConfigFile(directory) {
|
|
|
3470
3500
|
}
|
|
3471
3501
|
}
|
|
3472
3502
|
function detectProject(directory) {
|
|
3473
|
-
const
|
|
3503
|
+
const resolvedDirectory = resolve(directory);
|
|
3504
|
+
const remoteUrl = getGitRemoteUrl(resolvedDirectory);
|
|
3474
3505
|
if (remoteUrl) {
|
|
3475
3506
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
3476
|
-
const repoRoot = getGitTopLevel(
|
|
3507
|
+
const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
|
|
3477
3508
|
return {
|
|
3478
3509
|
canonical_id: canonicalId,
|
|
3479
3510
|
name: projectNameFromCanonicalId(canonicalId),
|
|
@@ -3481,21 +3512,22 @@ function detectProject(directory) {
|
|
|
3481
3512
|
local_path: repoRoot
|
|
3482
3513
|
};
|
|
3483
3514
|
}
|
|
3484
|
-
const configFile = readProjectConfigFile(
|
|
3515
|
+
const configFile = readProjectConfigFile(resolvedDirectory);
|
|
3485
3516
|
if (configFile) {
|
|
3486
3517
|
return {
|
|
3487
3518
|
canonical_id: configFile.project_id,
|
|
3488
3519
|
name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
|
|
3489
3520
|
remote_url: null,
|
|
3490
|
-
local_path:
|
|
3521
|
+
local_path: resolvedDirectory
|
|
3491
3522
|
};
|
|
3492
3523
|
}
|
|
3493
|
-
const dirName = basename2(
|
|
3524
|
+
const dirName = basename2(resolvedDirectory);
|
|
3525
|
+
const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
|
|
3494
3526
|
return {
|
|
3495
|
-
canonical_id: `local/${
|
|
3496
|
-
name:
|
|
3527
|
+
canonical_id: `local/${safeDirName}`,
|
|
3528
|
+
name: safeDirName,
|
|
3497
3529
|
remote_url: null,
|
|
3498
|
-
local_path:
|
|
3530
|
+
local_path: resolvedDirectory
|
|
3499
3531
|
};
|
|
3500
3532
|
}
|
|
3501
3533
|
function detectProjectForPath(filePath, fallbackCwd) {
|
|
@@ -3526,11 +3558,73 @@ function detectProjectFromTouchedPaths(paths, fallbackCwd) {
|
|
|
3526
3558
|
}
|
|
3527
3559
|
|
|
3528
3560
|
// src/capture/transcript.ts
|
|
3529
|
-
import { createHash as
|
|
3561
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3530
3562
|
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
|
|
3531
3563
|
import { join as join5 } from "node:path";
|
|
3532
3564
|
import { homedir as homedir3 } from "node:os";
|
|
3533
3565
|
|
|
3566
|
+
// src/embeddings/embedder.ts
|
|
3567
|
+
var _available = null;
|
|
3568
|
+
var _pipeline = null;
|
|
3569
|
+
var MODEL_NAME = "Xenova/all-MiniLM-L6-v2";
|
|
3570
|
+
async function embedText(text) {
|
|
3571
|
+
const pipe = await getPipeline();
|
|
3572
|
+
if (!pipe)
|
|
3573
|
+
return null;
|
|
3574
|
+
try {
|
|
3575
|
+
const output = await pipe(text, { pooling: "mean", normalize: true });
|
|
3576
|
+
return new Float32Array(output.data);
|
|
3577
|
+
} catch {
|
|
3578
|
+
return null;
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
function composeEmbeddingText(obs) {
|
|
3582
|
+
const parts = [obs.title];
|
|
3583
|
+
if (obs.narrative)
|
|
3584
|
+
parts.push(obs.narrative);
|
|
3585
|
+
if (obs.facts) {
|
|
3586
|
+
try {
|
|
3587
|
+
const facts = JSON.parse(obs.facts);
|
|
3588
|
+
if (Array.isArray(facts) && facts.length > 0) {
|
|
3589
|
+
parts.push(facts.map((f) => `- ${f}`).join(`
|
|
3590
|
+
`));
|
|
3591
|
+
}
|
|
3592
|
+
} catch {
|
|
3593
|
+
parts.push(obs.facts);
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
if (obs.concepts) {
|
|
3597
|
+
try {
|
|
3598
|
+
const concepts = JSON.parse(obs.concepts);
|
|
3599
|
+
if (Array.isArray(concepts) && concepts.length > 0) {
|
|
3600
|
+
parts.push(concepts.join(", "));
|
|
3601
|
+
}
|
|
3602
|
+
} catch {}
|
|
3603
|
+
}
|
|
3604
|
+
return parts.join(`
|
|
3605
|
+
|
|
3606
|
+
`);
|
|
3607
|
+
}
|
|
3608
|
+
function composeChatEmbeddingText(text) {
|
|
3609
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 2000);
|
|
3610
|
+
}
|
|
3611
|
+
async function getPipeline() {
|
|
3612
|
+
if (_pipeline)
|
|
3613
|
+
return _pipeline;
|
|
3614
|
+
if (_available === false)
|
|
3615
|
+
return null;
|
|
3616
|
+
try {
|
|
3617
|
+
const { pipeline } = await import("@xenova/transformers");
|
|
3618
|
+
_pipeline = await pipeline("feature-extraction", MODEL_NAME);
|
|
3619
|
+
_available = true;
|
|
3620
|
+
return _pipeline;
|
|
3621
|
+
} catch (err) {
|
|
3622
|
+
_available = false;
|
|
3623
|
+
console.error(`[engrm] Local embedding model unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
3624
|
+
return null;
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3534
3628
|
// src/tools/save.ts
|
|
3535
3629
|
import { relative, isAbsolute } from "node:path";
|
|
3536
3630
|
|
|
@@ -4261,7 +4355,7 @@ function dedupeHistoryMessages(messages) {
|
|
|
4261
4355
|
return deduped;
|
|
4262
4356
|
}
|
|
4263
4357
|
function buildHistorySourceId(sessionId, createdAtEpoch, text) {
|
|
4264
|
-
const digest =
|
|
4358
|
+
const digest = createHash4("sha1").update(text).digest("hex").slice(0, 12);
|
|
4265
4359
|
return `history:${sessionId}:${createdAtEpoch}:${digest}`;
|
|
4266
4360
|
}
|
|
4267
4361
|
function truncateTranscript(messages, maxBytes = 50000) {
|
|
@@ -4772,7 +4866,19 @@ function parseJsonArray4(value) {
|
|
|
4772
4866
|
}
|
|
4773
4867
|
}
|
|
4774
4868
|
function compactLine2(value) {
|
|
4775
|
-
|
|
4869
|
+
if (value === null || value === undefined)
|
|
4870
|
+
return null;
|
|
4871
|
+
let text;
|
|
4872
|
+
if (typeof value === "string") {
|
|
4873
|
+
text = value;
|
|
4874
|
+
} else {
|
|
4875
|
+
try {
|
|
4876
|
+
text = JSON.stringify(value);
|
|
4877
|
+
} catch {
|
|
4878
|
+
text = String(value);
|
|
4879
|
+
}
|
|
4880
|
+
}
|
|
4881
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
4776
4882
|
if (!trimmed)
|
|
4777
4883
|
return null;
|
|
4778
4884
|
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
@@ -4924,7 +5030,7 @@ async function main() {
|
|
|
4924
5030
|
}
|
|
4925
5031
|
} catch {}
|
|
4926
5032
|
}
|
|
4927
|
-
await pushOnce(db, config);
|
|
5033
|
+
await pushOnce(db, config, { timeoutMs: 4000 });
|
|
4928
5034
|
try {
|
|
4929
5035
|
if (event.session_id) {
|
|
4930
5036
|
const metrics = readSessionMetrics(event.session_id);
|