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.
@@ -91,10 +91,11 @@ function readProjectConfigFile(directory) {
91
91
  }
92
92
  }
93
93
  function detectProject(directory) {
94
- const remoteUrl = getGitRemoteUrl(directory);
94
+ const resolvedDirectory = resolve(directory);
95
+ const remoteUrl = getGitRemoteUrl(resolvedDirectory);
95
96
  if (remoteUrl) {
96
97
  const canonicalId = normaliseGitRemoteUrl(remoteUrl);
97
- const repoRoot = getGitTopLevel(directory) ?? directory;
98
+ const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
98
99
  return {
99
100
  canonical_id: canonicalId,
100
101
  name: projectNameFromCanonicalId(canonicalId),
@@ -102,21 +103,22 @@ function detectProject(directory) {
102
103
  local_path: repoRoot
103
104
  };
104
105
  }
105
- const configFile = readProjectConfigFile(directory);
106
+ const configFile = readProjectConfigFile(resolvedDirectory);
106
107
  if (configFile) {
107
108
  return {
108
109
  canonical_id: configFile.project_id,
109
110
  name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
110
111
  remote_url: null,
111
- local_path: directory
112
+ local_path: resolvedDirectory
112
113
  };
113
114
  }
114
- const dirName = basename(directory);
115
+ const dirName = basename(resolvedDirectory);
116
+ const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
115
117
  return {
116
- canonical_id: `local/${dirName}`,
117
- name: dirName,
118
+ canonical_id: `local/${safeDirName}`,
119
+ name: safeDirName,
118
120
  remote_url: null,
119
- local_path: directory
121
+ local_path: resolvedDirectory
120
122
  };
121
123
  }
122
124
  function detectProjectForPath(filePath, fallbackCwd) {
@@ -230,7 +232,8 @@ function createDefaultConfig() {
230
232
  project_name: "shared-experience",
231
233
  namespace: "",
232
234
  api_key: ""
233
- }
235
+ },
236
+ tool_profile: "full"
234
237
  };
235
238
  }
236
239
  function loadConfig() {
@@ -301,7 +304,8 @@ function loadConfig() {
301
304
  project_name: asString(config["fleet"]?.["project_name"], defaults.fleet.project_name),
302
305
  namespace: asString(config["fleet"]?.["namespace"], defaults.fleet.namespace),
303
306
  api_key: asString(config["fleet"]?.["api_key"], defaults.fleet.api_key)
304
- }
307
+ },
308
+ tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
305
309
  };
306
310
  }
307
311
  function saveConfig(config) {
@@ -356,6 +360,11 @@ function asObserverMode(value, fallback) {
356
360
  return value;
357
361
  return fallback;
358
362
  }
363
+ function asToolProfile(value, fallback) {
364
+ if (value === "full" || value === "memory")
365
+ return value;
366
+ return fallback;
367
+ }
359
368
  function asTeams(value, fallback) {
360
369
  if (!Array.isArray(value))
361
370
  return fallback;
@@ -989,6 +998,7 @@ function ensureObservationTypes(db) {
989
998
  DROP TABLE observations;
990
999
  ALTER TABLE observations_repair RENAME TO observations;
991
1000
  CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
1001
+ CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
992
1002
  CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
993
1003
  CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
994
1004
  CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
@@ -1269,6 +1279,7 @@ class MemDatabase {
1269
1279
  this.db = openDatabase(dbPath);
1270
1280
  this.db.exec("PRAGMA journal_mode = WAL");
1271
1281
  this.db.exec("PRAGMA foreign_keys = ON");
1282
+ this.db.exec("PRAGMA busy_timeout = 5000");
1272
1283
  this.vecAvailable = this.loadVecExtension();
1273
1284
  runMigrations(this.db);
1274
1285
  ensureObservationTypes(this.db);
@@ -1290,8 +1301,16 @@ class MemDatabase {
1290
1301
  this.db.close();
1291
1302
  }
1292
1303
  upsertProject(project) {
1304
+ const canonicalId = project.canonical_id?.trim();
1305
+ const name = project.name?.trim();
1306
+ if (!canonicalId) {
1307
+ throw new Error("Project canonical_id is required");
1308
+ }
1309
+ if (!name) {
1310
+ throw new Error("Project name is required");
1311
+ }
1293
1312
  const now = Math.floor(Date.now() / 1000);
1294
- const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(project.canonical_id);
1313
+ const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
1295
1314
  if (existing) {
1296
1315
  this.db.query(`UPDATE projects SET
1297
1316
  local_path = COALESCE(?, local_path),
@@ -1306,7 +1325,7 @@ class MemDatabase {
1306
1325
  };
1307
1326
  }
1308
1327
  const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
1309
- VALUES (?, ?, ?, ?, ?, ?)`).run(project.canonical_id, project.name, project.local_path ?? null, project.remote_url ?? null, now, now);
1328
+ VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
1310
1329
  return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
1311
1330
  }
1312
1331
  getProjectByCanonicalId(canonicalId) {
@@ -2094,7 +2113,19 @@ function buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolN
2094
2113
  return null;
2095
2114
  }
2096
2115
  function compactLine(value) {
2097
- const trimmed = value?.replace(/\s+/g, " ").trim();
2116
+ if (value === null || value === undefined)
2117
+ return null;
2118
+ let text;
2119
+ if (typeof value === "string") {
2120
+ text = value;
2121
+ } else {
2122
+ try {
2123
+ text = JSON.stringify(value);
2124
+ } catch {
2125
+ text = String(value);
2126
+ }
2127
+ }
2128
+ const trimmed = text.replace(/\s+/g, " ").trim();
2098
2129
  if (!trimmed)
2099
2130
  return null;
2100
2131
  return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
@@ -3593,7 +3624,19 @@ function parseJsonArray3(value) {
3593
3624
  }
3594
3625
  }
3595
3626
  function compactLine2(value) {
3596
- const trimmed = value?.replace(/\s+/g, " ").trim();
3627
+ if (value === null || value === undefined)
3628
+ return null;
3629
+ let text;
3630
+ if (typeof value === "string") {
3631
+ text = value;
3632
+ } else {
3633
+ try {
3634
+ text = JSON.stringify(value);
3635
+ } catch {
3636
+ text = String(value);
3637
+ }
3638
+ }
3639
+ const trimmed = text.replace(/\s+/g, " ").trim();
3597
3640
  if (!trimmed)
3598
3641
  return null;
3599
3642
  return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
package/dist/server.js CHANGED
@@ -13643,7 +13643,8 @@ function createDefaultConfig() {
13643
13643
  project_name: "shared-experience",
13644
13644
  namespace: "",
13645
13645
  api_key: ""
13646
- }
13646
+ },
13647
+ tool_profile: "full"
13647
13648
  };
13648
13649
  }
13649
13650
  function loadConfig() {
@@ -13714,7 +13715,8 @@ function loadConfig() {
13714
13715
  project_name: asString(config2["fleet"]?.["project_name"], defaults.fleet.project_name),
13715
13716
  namespace: asString(config2["fleet"]?.["namespace"], defaults.fleet.namespace),
13716
13717
  api_key: asString(config2["fleet"]?.["api_key"], defaults.fleet.api_key)
13717
- }
13718
+ },
13719
+ tool_profile: asToolProfile(config2["tool_profile"], defaults.tool_profile)
13718
13720
  };
13719
13721
  }
13720
13722
  function saveConfig(config2) {
@@ -13769,6 +13771,11 @@ function asObserverMode(value, fallback) {
13769
13771
  return value;
13770
13772
  return fallback;
13771
13773
  }
13774
+ function asToolProfile(value, fallback) {
13775
+ if (value === "full" || value === "memory")
13776
+ return value;
13777
+ return fallback;
13778
+ }
13772
13779
  function asTeams(value, fallback) {
13773
13780
  if (!Array.isArray(value))
13774
13781
  return fallback;
@@ -14402,6 +14409,7 @@ function ensureObservationTypes(db) {
14402
14409
  DROP TABLE observations;
14403
14410
  ALTER TABLE observations_repair RENAME TO observations;
14404
14411
  CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
14412
+ CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
14405
14413
  CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
14406
14414
  CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
14407
14415
  CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
@@ -14682,6 +14690,7 @@ class MemDatabase {
14682
14690
  this.db = openDatabase(dbPath);
14683
14691
  this.db.exec("PRAGMA journal_mode = WAL");
14684
14692
  this.db.exec("PRAGMA foreign_keys = ON");
14693
+ this.db.exec("PRAGMA busy_timeout = 5000");
14685
14694
  this.vecAvailable = this.loadVecExtension();
14686
14695
  runMigrations(this.db);
14687
14696
  ensureObservationTypes(this.db);
@@ -14703,8 +14712,16 @@ class MemDatabase {
14703
14712
  this.db.close();
14704
14713
  }
14705
14714
  upsertProject(project) {
14715
+ const canonicalId = project.canonical_id?.trim();
14716
+ const name = project.name?.trim();
14717
+ if (!canonicalId) {
14718
+ throw new Error("Project canonical_id is required");
14719
+ }
14720
+ if (!name) {
14721
+ throw new Error("Project name is required");
14722
+ }
14706
14723
  const now = Math.floor(Date.now() / 1000);
14707
- const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(project.canonical_id);
14724
+ const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
14708
14725
  if (existing) {
14709
14726
  this.db.query(`UPDATE projects SET
14710
14727
  local_path = COALESCE(?, local_path),
@@ -14719,7 +14736,7 @@ class MemDatabase {
14719
14736
  };
14720
14737
  }
14721
14738
  const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
14722
- VALUES (?, ?, ?, ?, ?, ?)`).run(project.canonical_id, project.name, project.local_path ?? null, project.remote_url ?? null, now, now);
14739
+ VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
14723
14740
  return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
14724
14741
  }
14725
14742
  getProjectByCanonicalId(canonicalId) {
@@ -15815,10 +15832,11 @@ function readProjectConfigFile(directory) {
15815
15832
  }
15816
15833
  }
15817
15834
  function detectProject(directory) {
15818
- const remoteUrl = getGitRemoteUrl(directory);
15835
+ const resolvedDirectory = resolve(directory);
15836
+ const remoteUrl = getGitRemoteUrl(resolvedDirectory);
15819
15837
  if (remoteUrl) {
15820
15838
  const canonicalId = normaliseGitRemoteUrl(remoteUrl);
15821
- const repoRoot = getGitTopLevel(directory) ?? directory;
15839
+ const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
15822
15840
  return {
15823
15841
  canonical_id: canonicalId,
15824
15842
  name: projectNameFromCanonicalId(canonicalId),
@@ -15826,21 +15844,22 @@ function detectProject(directory) {
15826
15844
  local_path: repoRoot
15827
15845
  };
15828
15846
  }
15829
- const configFile = readProjectConfigFile(directory);
15847
+ const configFile = readProjectConfigFile(resolvedDirectory);
15830
15848
  if (configFile) {
15831
15849
  return {
15832
15850
  canonical_id: configFile.project_id,
15833
15851
  name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
15834
15852
  remote_url: null,
15835
- local_path: directory
15853
+ local_path: resolvedDirectory
15836
15854
  };
15837
15855
  }
15838
- const dirName = basename(directory);
15856
+ const dirName = basename(resolvedDirectory);
15857
+ const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
15839
15858
  return {
15840
- canonical_id: `local/${dirName}`,
15841
- name: dirName,
15859
+ canonical_id: `local/${safeDirName}`,
15860
+ name: safeDirName,
15842
15861
  remote_url: null,
15843
- local_path: directory
15862
+ local_path: resolvedDirectory
15844
15863
  };
15845
15864
  }
15846
15865
  function detectProjectForPath(filePath, fallbackCwd) {
@@ -17530,7 +17549,19 @@ function formatTimestamp(nowMs) {
17530
17549
  return `${yyyy}-${mm}-${dd} ${hh}:${mi}Z`;
17531
17550
  }
17532
17551
  function compactLine(value) {
17533
- const trimmed = value?.replace(/\s+/g, " ").trim();
17552
+ if (value === null || value === undefined)
17553
+ return null;
17554
+ let text;
17555
+ if (typeof value === "string") {
17556
+ text = value;
17557
+ } else {
17558
+ try {
17559
+ text = JSON.stringify(value);
17560
+ } catch {
17561
+ text = String(value);
17562
+ }
17563
+ }
17564
+ const trimmed = text.replace(/\s+/g, " ").trim();
17534
17565
  if (!trimmed)
17535
17566
  return null;
17536
17567
  return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
@@ -19471,6 +19502,26 @@ function getActivityFeed(db, input) {
19471
19502
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
19472
19503
  import { homedir as homedir2 } from "node:os";
19473
19504
  import { join as join3 } from "node:path";
19505
+
19506
+ // src/tool-profiles.ts
19507
+ var MEMORY_PROFILE_TOOLS = [
19508
+ "save_observation",
19509
+ "search_recall",
19510
+ "resume_thread",
19511
+ "list_recall_items",
19512
+ "load_recall_item",
19513
+ "recent_chat",
19514
+ "search_chat",
19515
+ "refresh_chat_recall",
19516
+ "repair_recall"
19517
+ ];
19518
+ function getEnabledToolNames(profile) {
19519
+ if (!profile || profile === "full")
19520
+ return null;
19521
+ return new Set(MEMORY_PROFILE_TOOLS);
19522
+ }
19523
+
19524
+ // src/tools/capture-status.ts
19474
19525
  var LEGACY_CODEX_SERVER_NAME = `candengo-${"mem"}`;
19475
19526
  function getCaptureStatus(db, input = {}) {
19476
19527
  const hours = Math.max(1, Math.min(input.lookback_hours ?? 24, 24 * 30));
@@ -19574,6 +19625,8 @@ function getCaptureStatus(db, input = {}) {
19574
19625
  http_enabled: Boolean(config2?.http?.enabled || process.env.ENGRM_HTTP_PORT),
19575
19626
  http_port: config2?.http?.port ?? (process.env.ENGRM_HTTP_PORT ? Number(process.env.ENGRM_HTTP_PORT) : null),
19576
19627
  http_bearer_token_count: config2?.http?.bearer_tokens?.length ?? 0,
19628
+ tool_profile: config2?.tool_profile ?? "full",
19629
+ enabled_tool_count: config2 ? getEnabledToolNames(config2.tool_profile)?.size ?? null : null,
19577
19630
  fleet_project_name: config2?.fleet?.project_name ?? null,
19578
19631
  fleet_configured: Boolean(config2?.fleet?.namespace && config2?.fleet?.api_key),
19579
19632
  claude_mcp_registered: claudeMcpRegistered,
@@ -21188,11 +21241,12 @@ function getPendingEntries(db, limit = 50) {
21188
21241
  LIMIT ?`).all(now, limit);
21189
21242
  }
21190
21243
  function markSyncing(db, entryId) {
21191
- db.db.query("UPDATE sync_outbox SET status = 'syncing' WHERE id = ?").run(entryId);
21244
+ const now = Math.floor(Date.now() / 1000);
21245
+ db.db.query("UPDATE sync_outbox SET status = 'syncing', next_retry_epoch = ? WHERE id = ?").run(now, entryId);
21192
21246
  }
21193
21247
  function markSynced(db, entryId) {
21194
21248
  const now = Math.floor(Date.now() / 1000);
21195
- db.db.query("UPDATE sync_outbox SET status = 'synced', synced_at_epoch = ? WHERE id = ?").run(now, entryId);
21249
+ db.db.query("UPDATE sync_outbox SET status = 'synced', synced_at_epoch = ?, next_retry_epoch = NULL, last_error = NULL WHERE id = ?").run(now, entryId);
21196
21250
  }
21197
21251
  function markFailed(db, entryId, error48) {
21198
21252
  const now = Math.floor(Date.now() / 1000);
@@ -21216,6 +21270,74 @@ function getOutboxStats(db) {
21216
21270
  }
21217
21271
  return stats;
21218
21272
  }
21273
+ function getOutboxFailureSummaries(db, limit = 5) {
21274
+ return db.db.query(`SELECT COALESCE(last_error, '') as error, COUNT(*) as count
21275
+ FROM sync_outbox
21276
+ WHERE status = 'failed'
21277
+ GROUP BY COALESCE(last_error, '')
21278
+ ORDER BY count DESC, error ASC
21279
+ LIMIT ?`).all(limit).filter((row) => row.error.length > 0);
21280
+ }
21281
+ function classifyOutboxFailure(error48) {
21282
+ const normalized = error48.toLowerCase();
21283
+ if (normalized.includes("401") || normalized.includes("invalid or missing credentials")) {
21284
+ return "auth";
21285
+ }
21286
+ if (normalized.includes("429") || normalized.includes("rate limit")) {
21287
+ return "rate_limit";
21288
+ }
21289
+ if (normalized.includes("timeout") || normalized.includes("abort")) {
21290
+ return "timeout";
21291
+ }
21292
+ if (normalized.includes("network") || normalized.includes("fetch") || normalized.includes("econn")) {
21293
+ return "network";
21294
+ }
21295
+ if (normalized.includes("400") || normalized.includes("422") || normalized.includes("validation")) {
21296
+ return "validation";
21297
+ }
21298
+ return "other";
21299
+ }
21300
+ function resetFailedEntries(db) {
21301
+ const result = db.db.query(`UPDATE sync_outbox
21302
+ SET status = 'pending',
21303
+ retry_count = 0,
21304
+ last_error = NULL,
21305
+ next_retry_epoch = NULL
21306
+ WHERE status = 'failed'`).run();
21307
+ return result.changes;
21308
+ }
21309
+ function resetFailedEntriesMatching(db, predicate) {
21310
+ const rows = db.db.query(`SELECT id, last_error
21311
+ FROM sync_outbox
21312
+ WHERE status = 'failed'`).all();
21313
+ const matchingIds = rows.filter((row) => row.last_error && predicate(row.last_error)).map((row) => row.id);
21314
+ if (matchingIds.length === 0)
21315
+ return 0;
21316
+ const placeholders = matchingIds.map(() => "?").join(", ");
21317
+ const result = db.db.query(`UPDATE sync_outbox
21318
+ SET status = 'pending',
21319
+ retry_count = 0,
21320
+ last_error = NULL,
21321
+ next_retry_epoch = NULL
21322
+ WHERE id IN (${placeholders})`).run(...matchingIds);
21323
+ return result.changes;
21324
+ }
21325
+ function resetSyncingEntries(db) {
21326
+ const result = db.db.query(`UPDATE sync_outbox
21327
+ SET status = 'pending',
21328
+ next_retry_epoch = NULL
21329
+ WHERE status = 'syncing'`).run();
21330
+ return result.changes;
21331
+ }
21332
+ function resetStaleSyncingEntries(db, maxAgeSeconds = 300) {
21333
+ const cutoff = Math.floor(Date.now() / 1000) - maxAgeSeconds;
21334
+ const result = db.db.query(`UPDATE sync_outbox
21335
+ SET status = 'pending',
21336
+ next_retry_epoch = NULL
21337
+ WHERE status = 'syncing'
21338
+ AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?)`).run(cutoff);
21339
+ return result.changes;
21340
+ }
21219
21341
 
21220
21342
  // src/intelligence/value-signals.ts
21221
21343
  var LESSON_TYPES = new Set(["bugfix", "decision", "pattern"]);
@@ -21352,7 +21474,12 @@ function getMemoryStats(db) {
21352
21474
  recent_completed: insights.recent_completed,
21353
21475
  next_steps: insights.next_steps,
21354
21476
  installed_packs: db.getInstalledPacks(),
21355
- outbox: getOutboxStats(db)
21477
+ outbox: getOutboxStats(db),
21478
+ outbox_failure_summary: getOutboxFailureSummaries(db).map((row) => ({
21479
+ category: classifyOutboxFailure(row.error),
21480
+ error: row.error,
21481
+ count: row.count
21482
+ }))
21356
21483
  };
21357
21484
  }
21358
21485
 
@@ -21535,6 +21662,7 @@ function isDue(db, key, interval, now) {
21535
21662
  }
21536
21663
 
21537
21664
  // src/sync/auth.ts
21665
+ import { createHash as createHash4 } from "node:crypto";
21538
21666
  var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
21539
21667
  function normalizeBaseUrl(url2) {
21540
21668
  const trimmed = url2.trim();
@@ -21565,6 +21693,36 @@ function getBaseUrl(config2) {
21565
21693
  }
21566
21694
  return null;
21567
21695
  }
21696
+ function getAuthFingerprint(config2) {
21697
+ const apiKey = getApiKey(config2);
21698
+ const baseUrl = getBaseUrl(config2);
21699
+ if (!apiKey || !baseUrl)
21700
+ return null;
21701
+ return createHash4("sha256").update(`${baseUrl}
21702
+ ${apiKey}
21703
+ ${config2.namespace}
21704
+ ${config2.site_id}`).digest("hex");
21705
+ }
21706
+ function recoverOutboxAfterAuthChange(db, config2) {
21707
+ const fingerprint = getAuthFingerprint(config2);
21708
+ if (!fingerprint) {
21709
+ const staleSyncingReset2 = resetStaleSyncingEntries(db);
21710
+ return { fingerprintChanged: false, failedReset: 0, authFailedReset: 0, syncingReset: 0, staleSyncingReset: staleSyncingReset2 };
21711
+ }
21712
+ const key = "sync_auth_fingerprint";
21713
+ const previous = db.getSyncState(key);
21714
+ const fingerprintChanged = previous !== fingerprint;
21715
+ if (!fingerprintChanged) {
21716
+ const authFailedReset = resetFailedEntriesMatching(db, (error48) => classifyOutboxFailure(error48) === "auth");
21717
+ const staleSyncingReset2 = resetStaleSyncingEntries(db);
21718
+ return { fingerprintChanged: false, failedReset: 0, authFailedReset, syncingReset: 0, staleSyncingReset: staleSyncingReset2 };
21719
+ }
21720
+ const failedReset = resetFailedEntries(db);
21721
+ const syncingReset = resetSyncingEntries(db);
21722
+ const staleSyncingReset = 0;
21723
+ db.setSyncState(key, fingerprint);
21724
+ return { fingerprintChanged: true, failedReset, authFailedReset: 0, syncingReset, staleSyncingReset };
21725
+ }
21568
21726
  function buildSourceId(config2, localId, type = "obs") {
21569
21727
  return `${config2.user_id}-${config2.device_id}-${type}-${localId}`;
21570
21728
  }
@@ -21598,6 +21756,7 @@ class VectorClient {
21598
21756
  apiKey;
21599
21757
  siteId;
21600
21758
  namespace;
21759
+ timeoutMs;
21601
21760
  constructor(config2, overrides = {}) {
21602
21761
  const baseUrl = getBaseUrl(config2);
21603
21762
  const apiKey = overrides.apiKey ?? getApiKey(config2);
@@ -21608,6 +21767,7 @@ class VectorClient {
21608
21767
  this.apiKey = apiKey;
21609
21768
  this.siteId = overrides.siteId ?? config2.site_id;
21610
21769
  this.namespace = overrides.namespace ?? config2.namespace;
21770
+ this.timeoutMs = overrides.timeoutMs ?? 1e4;
21611
21771
  }
21612
21772
  static isConfigured(config2) {
21613
21773
  return getApiKey(config2) !== null && getBaseUrl(config2) !== null;
@@ -21670,6 +21830,7 @@ class VectorClient {
21670
21830
  if (body && method !== "GET") {
21671
21831
  init.body = JSON.stringify(body);
21672
21832
  }
21833
+ init.signal = AbortSignal.timeout(this.timeoutMs);
21673
21834
  const response = await fetch(url2, init);
21674
21835
  if (!response.ok) {
21675
21836
  const text = await response.text().catch(() => "");
@@ -21750,7 +21911,19 @@ function buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolN
21750
21911
  return null;
21751
21912
  }
21752
21913
  function compactLine2(value) {
21753
- const trimmed = value?.replace(/\s+/g, " ").trim();
21914
+ if (value === null || value === undefined)
21915
+ return null;
21916
+ let text;
21917
+ if (typeof value === "string") {
21918
+ text = value;
21919
+ } else {
21920
+ try {
21921
+ text = JSON.stringify(value);
21922
+ } catch {
21923
+ text = String(value);
21924
+ }
21925
+ }
21926
+ const trimmed = text.replace(/\s+/g, " ").trim();
21754
21927
  if (!trimmed)
21755
21928
  return null;
21756
21929
  return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
@@ -21919,7 +22092,7 @@ function buildSummaryVectorDocument(summary, config2, project, targetOrObservati
21919
22092
  }
21920
22093
  };
21921
22094
  }
21922
- async function pushOutbox(db, config2, batchSize = 50) {
22095
+ async function pushOutbox(db, config2, batchSize = 50, options = {}) {
21923
22096
  const entries = getPendingEntries(db, batchSize);
21924
22097
  let pushed = 0;
21925
22098
  let failed = 0;
@@ -22030,7 +22203,8 @@ async function pushOutbox(db, config2, batchSize = 50) {
22030
22203
  const client = new VectorClient(config2, {
22031
22204
  apiKey: target.apiKey,
22032
22205
  namespace: target.namespace,
22033
- siteId: target.siteId
22206
+ siteId: target.siteId,
22207
+ timeoutMs: options.timeoutMs
22034
22208
  });
22035
22209
  try {
22036
22210
  await client.batchIngest(items.map((b) => b.doc));
@@ -22342,6 +22516,9 @@ async function pullSettings(client, config2) {
22342
22516
 
22343
22517
  // src/sync/engine.ts
22344
22518
  var DEFAULT_PULL_INTERVAL = 60000;
22519
+ function recordNowEpoch(db, key) {
22520
+ db.setSyncState(key, String(Math.floor(Date.now() / 1000)));
22521
+ }
22345
22522
 
22346
22523
  class SyncEngine {
22347
22524
  db;
@@ -22400,7 +22577,11 @@ class SyncEngine {
22400
22577
  return;
22401
22578
  this._pushing = true;
22402
22579
  try {
22403
- await pushOutbox(this.db, this.config, this.config.sync.batch_size);
22580
+ recoverOutboxAfterAuthChange(this.db, this.config);
22581
+ const result = await pushOutbox(this.db, this.config, this.config.sync.batch_size);
22582
+ if (result.pushed > 0) {
22583
+ recordNowEpoch(this.db, "last_push_epoch");
22584
+ }
22404
22585
  } finally {
22405
22586
  this._pushing = false;
22406
22587
  }
@@ -22410,11 +22591,16 @@ class SyncEngine {
22410
22591
  return;
22411
22592
  this._pulling = true;
22412
22593
  try {
22413
- await pullFromVector(this.db, this.client, this.config);
22594
+ const primary = await pullFromVector(this.db, this.client, this.config);
22595
+ let totalReceived = primary.received;
22414
22596
  if (this.fleetClient) {
22415
- await pullFromVector(this.db, this.fleetClient, this.config);
22597
+ const fleet = await pullFromVector(this.db, this.fleetClient, this.config);
22598
+ totalReceived += fleet.received;
22416
22599
  }
22417
22600
  await pullSettings(this.client, this.config);
22601
+ if (totalReceived > 0) {
22602
+ recordNowEpoch(this.db, "last_pull_epoch");
22603
+ }
22418
22604
  } finally {
22419
22605
  this._pulling = false;
22420
22606
  }
@@ -23070,21 +23256,50 @@ function resolveAgentName(clientName) {
23070
23256
  return clientName;
23071
23257
  }
23072
23258
  var syncEngine = null;
23073
- process.on("SIGINT", () => {
23074
- syncEngine?.stop();
23075
- db.close();
23076
- process.exit(0);
23077
- });
23078
- process.on("SIGTERM", () => {
23259
+ var shuttingDown = false;
23260
+ function shutdown(code = 0) {
23261
+ if (shuttingDown)
23262
+ return;
23263
+ shuttingDown = true;
23079
23264
  syncEngine?.stop();
23080
23265
  db.close();
23081
- process.exit(0);
23082
- });
23266
+ process.exit(code);
23267
+ }
23268
+ process.on("SIGINT", () => shutdown(0));
23269
+ process.on("SIGTERM", () => shutdown(0));
23270
+ function installStdioLivenessGuards() {
23271
+ process.stdin.on("end", () => shutdown(0));
23272
+ process.stdin.on("close", () => shutdown(0));
23273
+ process.stdin.on("error", () => shutdown(0));
23274
+ process.stdin.resume();
23275
+ const parentPid = process.ppid;
23276
+ if (!Number.isInteger(parentPid) || parentPid <= 1)
23277
+ return;
23278
+ const heartbeat = setInterval(() => {
23279
+ try {
23280
+ process.kill(parentPid, 0);
23281
+ } catch {
23282
+ shutdown(0);
23283
+ }
23284
+ if (process.ppid === 1) {
23285
+ shutdown(0);
23286
+ }
23287
+ }, 30000);
23288
+ heartbeat.unref();
23289
+ }
23083
23290
  function buildServer() {
23084
23291
  const server = new McpServer({
23085
23292
  name: "engrm",
23086
- version: "0.4.42"
23293
+ version: "0.4.44"
23087
23294
  });
23295
+ const enabledToolNames = getEnabledToolNames(config2.tool_profile);
23296
+ const originalTool = server.tool.bind(server);
23297
+ server.tool = (name, ...args) => {
23298
+ if (enabledToolNames && !enabledToolNames.has(name)) {
23299
+ return server;
23300
+ }
23301
+ return originalTool(name, ...args);
23302
+ };
23088
23303
  server.tool("save_observation", "Directly save a durable memory item now. Use this when something should be remembered on purpose instead of waiting for an end-of-session digest.", {
23089
23304
  type: exports_external.enum([
23090
23305
  "bugfix",
@@ -24146,6 +24361,7 @@ ${observationLines}`
24146
24361
  text: `Schema: v${result.schema_version} (${result.schema_current ? "current" : "outdated"})
24147
24362
  ` + `HTTP MCP: ${result.http_enabled ? `enabled${result.http_port ? ` (:${result.http_port})` : ""}` : "disabled"}
24148
24363
  ` + `HTTP bearer tokens: ${result.http_bearer_token_count}
24364
+ ` + `Tool profile: ${result.tool_profile}${typeof result.enabled_tool_count === "number" ? ` (${result.enabled_tool_count} tools)` : ""}
24149
24365
  ` + `Fleet project: ${result.fleet_project_name ?? "none"}
24150
24366
  ` + `Fleet sync: ${result.fleet_configured ? "configured" : "not configured"}
24151
24367
 
@@ -25085,6 +25301,7 @@ async function main() {
25085
25301
  await startHttpServer();
25086
25302
  return;
25087
25303
  }
25304
+ installStdioLivenessGuards();
25088
25305
  const transport = new StdioServerTransport;
25089
25306
  await server.connect(transport);
25090
25307
  }
@@ -31,12 +31,12 @@ The MCP entry written by the helper script matches:
31
31
  {
32
32
  "$schema": "https://opencode.ai/config.json",
33
33
  "mcp": {
34
- "engrm": {
35
- "type": "local",
36
- "command": ["engrm", "serve"],
37
- "enabled": true,
38
- "timeout": 5000
39
- }
34
+ "engrm": {
35
+ "type": "local",
36
+ "command": ["node", "/absolute/path/to/engrm/dist/server.js"],
37
+ "enabled": true,
38
+ "timeout": 5000
39
+ }
40
40
  }
41
41
  }
42
42
  ```