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.
@@ -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(project.canonical_id);
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(project.canonical_id, project.name, project.local_path ?? null, project.remote_url ?? null, now, now);
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
- const trimmed = value?.replace(/\s+/g, " ").trim();
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
- async function pushOnce(db, config) {
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
- const client = new VectorClient(config);
3116
- const result = await pushOutbox(db, config, config.sync.batch_size);
3117
- await pullSettings(client, config);
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.42",
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 remoteUrl = getGitRemoteUrl(directory);
3503
+ const resolvedDirectory = resolve(directory);
3504
+ const remoteUrl = getGitRemoteUrl(resolvedDirectory);
3474
3505
  if (remoteUrl) {
3475
3506
  const canonicalId = normaliseGitRemoteUrl(remoteUrl);
3476
- const repoRoot = getGitTopLevel(directory) ?? directory;
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(directory);
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: directory
3521
+ local_path: resolvedDirectory
3491
3522
  };
3492
3523
  }
3493
- const dirName = basename2(directory);
3524
+ const dirName = basename2(resolvedDirectory);
3525
+ const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
3494
3526
  return {
3495
- canonical_id: `local/${dirName}`,
3496
- name: dirName,
3527
+ canonical_id: `local/${safeDirName}`,
3528
+ name: safeDirName,
3497
3529
  remote_url: null,
3498
- local_path: directory
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 createHash3 } from "node:crypto";
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 = createHash3("sha1").update(text).digest("hex").slice(0, 12);
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
- const trimmed = value?.replace(/\s+/g, " ").trim();
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);