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.
@@ -419,10 +419,11 @@ function readProjectConfigFile(directory) {
419
419
  }
420
420
  }
421
421
  function detectProject(directory) {
422
- const remoteUrl = getGitRemoteUrl(directory);
422
+ const resolvedDirectory = resolve(directory);
423
+ const remoteUrl = getGitRemoteUrl(resolvedDirectory);
423
424
  if (remoteUrl) {
424
425
  const canonicalId = normaliseGitRemoteUrl(remoteUrl);
425
- const repoRoot = getGitTopLevel(directory) ?? directory;
426
+ const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
426
427
  return {
427
428
  canonical_id: canonicalId,
428
429
  name: projectNameFromCanonicalId(canonicalId),
@@ -430,21 +431,22 @@ function detectProject(directory) {
430
431
  local_path: repoRoot
431
432
  };
432
433
  }
433
- const configFile = readProjectConfigFile(directory);
434
+ const configFile = readProjectConfigFile(resolvedDirectory);
434
435
  if (configFile) {
435
436
  return {
436
437
  canonical_id: configFile.project_id,
437
438
  name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
438
439
  remote_url: null,
439
- local_path: directory
440
+ local_path: resolvedDirectory
440
441
  };
441
442
  }
442
- const dirName = basename(directory);
443
+ const dirName = basename(resolvedDirectory);
444
+ const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
443
445
  return {
444
- canonical_id: `local/${dirName}`,
445
- name: dirName,
446
+ canonical_id: `local/${safeDirName}`,
447
+ name: safeDirName,
446
448
  remote_url: null,
447
- local_path: directory
449
+ local_path: resolvedDirectory
448
450
  };
449
451
  }
450
452
  function detectProjectForPath(filePath, fallbackCwd) {
@@ -986,7 +988,8 @@ function createDefaultConfig() {
986
988
  project_name: "shared-experience",
987
989
  namespace: "",
988
990
  api_key: ""
989
- }
991
+ },
992
+ tool_profile: "full"
990
993
  };
991
994
  }
992
995
  function loadConfig() {
@@ -1057,7 +1060,8 @@ function loadConfig() {
1057
1060
  project_name: asString(config["fleet"]?.["project_name"], defaults.fleet.project_name),
1058
1061
  namespace: asString(config["fleet"]?.["namespace"], defaults.fleet.namespace),
1059
1062
  api_key: asString(config["fleet"]?.["api_key"], defaults.fleet.api_key)
1060
- }
1063
+ },
1064
+ tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
1061
1065
  };
1062
1066
  }
1063
1067
  function saveConfig(config) {
@@ -1112,6 +1116,11 @@ function asObserverMode(value, fallback) {
1112
1116
  return value;
1113
1117
  return fallback;
1114
1118
  }
1119
+ function asToolProfile(value, fallback) {
1120
+ if (value === "full" || value === "memory")
1121
+ return value;
1122
+ return fallback;
1123
+ }
1115
1124
  function asTeams(value, fallback) {
1116
1125
  if (!Array.isArray(value))
1117
1126
  return fallback;
@@ -1745,6 +1754,7 @@ function ensureObservationTypes(db) {
1745
1754
  DROP TABLE observations;
1746
1755
  ALTER TABLE observations_repair RENAME TO observations;
1747
1756
  CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
1757
+ CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
1748
1758
  CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
1749
1759
  CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
1750
1760
  CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
@@ -2025,6 +2035,7 @@ class MemDatabase {
2025
2035
  this.db = openDatabase(dbPath);
2026
2036
  this.db.exec("PRAGMA journal_mode = WAL");
2027
2037
  this.db.exec("PRAGMA foreign_keys = ON");
2038
+ this.db.exec("PRAGMA busy_timeout = 5000");
2028
2039
  this.vecAvailable = this.loadVecExtension();
2029
2040
  runMigrations(this.db);
2030
2041
  ensureObservationTypes(this.db);
@@ -2046,8 +2057,16 @@ class MemDatabase {
2046
2057
  this.db.close();
2047
2058
  }
2048
2059
  upsertProject(project) {
2060
+ const canonicalId = project.canonical_id?.trim();
2061
+ const name = project.name?.trim();
2062
+ if (!canonicalId) {
2063
+ throw new Error("Project canonical_id is required");
2064
+ }
2065
+ if (!name) {
2066
+ throw new Error("Project name is required");
2067
+ }
2049
2068
  const now = Math.floor(Date.now() / 1000);
2050
- const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(project.canonical_id);
2069
+ const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
2051
2070
  if (existing) {
2052
2071
  this.db.query(`UPDATE projects SET
2053
2072
  local_path = COALESCE(?, local_path),
@@ -2062,7 +2081,7 @@ class MemDatabase {
2062
2081
  };
2063
2082
  }
2064
2083
  const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
2065
- VALUES (?, ?, ?, ?, ?, ?)`).run(project.canonical_id, project.name, project.local_path ?? null, project.remote_url ?? null, now, now);
2084
+ VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
2066
2085
  return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
2067
2086
  }
2068
2087
  getProjectByCanonicalId(canonicalId) {
@@ -292,7 +292,8 @@ function createDefaultConfig() {
292
292
  project_name: "shared-experience",
293
293
  namespace: "",
294
294
  api_key: ""
295
- }
295
+ },
296
+ tool_profile: "full"
296
297
  };
297
298
  }
298
299
  function loadConfig() {
@@ -363,7 +364,8 @@ function loadConfig() {
363
364
  project_name: asString(config["fleet"]?.["project_name"], defaults.fleet.project_name),
364
365
  namespace: asString(config["fleet"]?.["namespace"], defaults.fleet.namespace),
365
366
  api_key: asString(config["fleet"]?.["api_key"], defaults.fleet.api_key)
366
- }
367
+ },
368
+ tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
367
369
  };
368
370
  }
369
371
  function saveConfig(config) {
@@ -418,6 +420,11 @@ function asObserverMode(value, fallback) {
418
420
  return value;
419
421
  return fallback;
420
422
  }
423
+ function asToolProfile(value, fallback) {
424
+ if (value === "full" || value === "memory")
425
+ return value;
426
+ return fallback;
427
+ }
421
428
  function asTeams(value, fallback) {
422
429
  if (!Array.isArray(value))
423
430
  return fallback;
@@ -1051,6 +1058,7 @@ function ensureObservationTypes(db) {
1051
1058
  DROP TABLE observations;
1052
1059
  ALTER TABLE observations_repair RENAME TO observations;
1053
1060
  CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
1061
+ CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
1054
1062
  CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
1055
1063
  CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
1056
1064
  CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
@@ -1331,6 +1339,7 @@ class MemDatabase {
1331
1339
  this.db = openDatabase(dbPath);
1332
1340
  this.db.exec("PRAGMA journal_mode = WAL");
1333
1341
  this.db.exec("PRAGMA foreign_keys = ON");
1342
+ this.db.exec("PRAGMA busy_timeout = 5000");
1334
1343
  this.vecAvailable = this.loadVecExtension();
1335
1344
  runMigrations(this.db);
1336
1345
  ensureObservationTypes(this.db);
@@ -1352,8 +1361,16 @@ class MemDatabase {
1352
1361
  this.db.close();
1353
1362
  }
1354
1363
  upsertProject(project) {
1364
+ const canonicalId = project.canonical_id?.trim();
1365
+ const name = project.name?.trim();
1366
+ if (!canonicalId) {
1367
+ throw new Error("Project canonical_id is required");
1368
+ }
1369
+ if (!name) {
1370
+ throw new Error("Project name is required");
1371
+ }
1355
1372
  const now = Math.floor(Date.now() / 1000);
1356
- const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(project.canonical_id);
1373
+ const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
1357
1374
  if (existing) {
1358
1375
  this.db.query(`UPDATE projects SET
1359
1376
  local_path = COALESCE(?, local_path),
@@ -1368,7 +1385,7 @@ class MemDatabase {
1368
1385
  };
1369
1386
  }
1370
1387
  const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
1371
- VALUES (?, ?, ?, ?, ?, ?)`).run(project.canonical_id, project.name, project.local_path ?? null, project.remote_url ?? null, now, now);
1388
+ VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
1372
1389
  return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
1373
1390
  }
1374
1391
  getProjectByCanonicalId(canonicalId) {
@@ -2518,10 +2535,11 @@ function readProjectConfigFile(directory) {
2518
2535
  }
2519
2536
  }
2520
2537
  function detectProject(directory) {
2521
- const remoteUrl = getGitRemoteUrl(directory);
2538
+ const resolvedDirectory = resolve(directory);
2539
+ const remoteUrl = getGitRemoteUrl(resolvedDirectory);
2522
2540
  if (remoteUrl) {
2523
2541
  const canonicalId = normaliseGitRemoteUrl(remoteUrl);
2524
- const repoRoot = getGitTopLevel(directory) ?? directory;
2542
+ const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
2525
2543
  return {
2526
2544
  canonical_id: canonicalId,
2527
2545
  name: projectNameFromCanonicalId(canonicalId),
@@ -2529,21 +2547,22 @@ function detectProject(directory) {
2529
2547
  local_path: repoRoot
2530
2548
  };
2531
2549
  }
2532
- const configFile = readProjectConfigFile(directory);
2550
+ const configFile = readProjectConfigFile(resolvedDirectory);
2533
2551
  if (configFile) {
2534
2552
  return {
2535
2553
  canonical_id: configFile.project_id,
2536
2554
  name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
2537
2555
  remote_url: null,
2538
- local_path: directory
2556
+ local_path: resolvedDirectory
2539
2557
  };
2540
2558
  }
2541
- const dirName = basename(directory);
2559
+ const dirName = basename(resolvedDirectory);
2560
+ const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
2542
2561
  return {
2543
- canonical_id: `local/${dirName}`,
2544
- name: dirName,
2562
+ canonical_id: `local/${safeDirName}`,
2563
+ name: safeDirName,
2545
2564
  remote_url: null,
2546
- local_path: directory
2565
+ local_path: resolvedDirectory
2547
2566
  };
2548
2567
  }
2549
2568
  function detectProjectForPath(filePath, fallbackCwd) {
@@ -3427,9 +3446,21 @@ function incrementObserverSaveCount(sessionId) {
3427
3446
  // src/capture/recall.ts
3428
3447
  var VEC_DISTANCE_THRESHOLD = 0.25;
3429
3448
  function extractErrorSignature(output) {
3430
- if (!output || output.length < 10)
3449
+ if (output === null || output === undefined)
3450
+ return null;
3451
+ let text;
3452
+ if (typeof output === "string") {
3453
+ text = output;
3454
+ } else {
3455
+ try {
3456
+ text = JSON.stringify(output);
3457
+ } catch {
3458
+ text = String(output);
3459
+ }
3460
+ }
3461
+ if (!text || text.length < 10)
3431
3462
  return null;
3432
- const lines = output.split(`
3463
+ const lines = text.split(`
3433
3464
  `);
3434
3465
  for (let i = lines.length - 1;i >= 0; i--) {
3435
3466
  const line = lines[i].trim();
@@ -3759,7 +3790,19 @@ function buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolN
3759
3790
  return null;
3760
3791
  }
3761
3792
  function compactLine(value) {
3762
- const trimmed = value?.replace(/\s+/g, " ").trim();
3793
+ if (value === null || value === undefined)
3794
+ return null;
3795
+ let text;
3796
+ if (typeof value === "string") {
3797
+ text = value;
3798
+ } else {
3799
+ try {
3800
+ text = JSON.stringify(value);
3801
+ } catch {
3802
+ text = String(value);
3803
+ }
3804
+ }
3805
+ const trimmed = text.replace(/\s+/g, " ").trim();
3763
3806
  if (!trimmed)
3764
3807
  return null;
3765
3808
  return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
@@ -4500,7 +4543,19 @@ function parseJsonArray3(value) {
4500
4543
  }
4501
4544
  }
4502
4545
  function compactLine2(value) {
4503
- const trimmed = value?.replace(/\s+/g, " ").trim();
4546
+ if (value === null || value === undefined)
4547
+ return null;
4548
+ let text;
4549
+ if (typeof value === "string") {
4550
+ text = value;
4551
+ } else {
4552
+ try {
4553
+ text = JSON.stringify(value);
4554
+ } catch {
4555
+ text = String(value);
4556
+ }
4557
+ }
4558
+ const trimmed = text.replace(/\s+/g, " ").trim();
4504
4559
  if (!trimmed)
4505
4560
  return null;
4506
4561
  return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
@@ -4532,7 +4587,7 @@ async function main() {
4532
4587
  db.setSyncState("hook_post_tool_last_payload_preview", truncatePreview(raw, 400) ?? "parsed");
4533
4588
  try {
4534
4589
  if (event.session_id) {
4535
- persistRawToolChronology(event, config.user_id, config.device_id);
4590
+ persistRawToolChronology(db, event, config.user_id, config.device_id);
4536
4591
  await syncTranscriptChat(db, config, event.session_id, event.cwd);
4537
4592
  }
4538
4593
  const textToScan = extractScanText(event);
@@ -4638,24 +4693,22 @@ async function main() {
4638
4693
  db.close();
4639
4694
  }
4640
4695
  }
4641
- function persistRawToolChronology(event, userId, deviceId) {
4642
- const rawDb = new MemDatabase(getDbPath());
4696
+ function persistRawToolChronology(rawDb, event, userId, deviceId) {
4697
+ const detected = detectProjectForEvent(event);
4698
+ const project = rawDb.upsertProject({
4699
+ canonical_id: detected.canonical_id,
4700
+ name: detected.name,
4701
+ local_path: detected.local_path,
4702
+ remote_url: detected.remote_url ?? null
4703
+ });
4704
+ rawDb.upsertSession(event.session_id, project.id, userId, deviceId, "claude-code");
4705
+ const metricsIncrement = {
4706
+ toolCalls: 1
4707
+ };
4708
+ if ((event.tool_name === "Edit" || event.tool_name === "Write") && event.tool_input["file_path"]) {
4709
+ metricsIncrement.files = 1;
4710
+ }
4643
4711
  try {
4644
- const detected = detectProjectForEvent(event);
4645
- const project = rawDb.upsertProject({
4646
- canonical_id: detected.canonical_id,
4647
- name: detected.name,
4648
- local_path: detected.local_path,
4649
- remote_url: detected.remote_url ?? null
4650
- });
4651
- rawDb.upsertSession(event.session_id, project.id, userId, deviceId, "claude-code");
4652
- const metricsIncrement = {
4653
- toolCalls: 1
4654
- };
4655
- if ((event.tool_name === "Edit" || event.tool_name === "Write") && event.tool_input["file_path"]) {
4656
- metricsIncrement.files = 1;
4657
- }
4658
- rawDb.incrementSessionMetrics(event.session_id, metricsIncrement);
4659
4712
  rawDb.insertToolEvent({
4660
4713
  session_id: event.session_id,
4661
4714
  project_id: project.id,
@@ -4668,8 +4721,13 @@ function persistRawToolChronology(event, userId, deviceId) {
4668
4721
  device_id: deviceId,
4669
4722
  agent: "claude-code"
4670
4723
  });
4671
- } finally {
4672
- rawDb.close();
4724
+ rawDb.incrementSessionMetrics(event.session_id, metricsIncrement);
4725
+ rawDb.setSyncState("hook_post_tool_last_store_status", "stored");
4726
+ rawDb.setSyncState("hook_post_tool_last_store_error", "");
4727
+ } catch (error) {
4728
+ rawDb.setSyncState("hook_post_tool_last_store_status", "store-error");
4729
+ rawDb.setSyncState("hook_post_tool_last_store_error", truncatePreview(error instanceof Error ? error.message : String(error), 400) ?? "unknown");
4730
+ throw error;
4673
4731
  }
4674
4732
  }
4675
4733
  async function withTimeout(promise, timeoutMs) {
@@ -4754,9 +4812,21 @@ function safeSerializeToolInput(toolInput) {
4754
4812
  }
4755
4813
  }
4756
4814
  function truncatePreview(value, maxLen) {
4757
- if (!value)
4815
+ if (value === null || value === undefined)
4816
+ return null;
4817
+ let text;
4818
+ if (typeof value === "string") {
4819
+ text = value;
4820
+ } else {
4821
+ try {
4822
+ text = JSON.stringify(value);
4823
+ } catch {
4824
+ text = String(value);
4825
+ }
4826
+ }
4827
+ if (!text)
4758
4828
  return null;
4759
- const normalized = value.replace(/\s+/g, " ").trim();
4829
+ const normalized = text.replace(/\s+/g, " ").trim();
4760
4830
  if (!normalized)
4761
4831
  return null;
4762
4832
  if (normalized.length <= maxLen)
@@ -86,7 +86,8 @@ function createDefaultConfig() {
86
86
  project_name: "shared-experience",
87
87
  namespace: "",
88
88
  api_key: ""
89
- }
89
+ },
90
+ tool_profile: "full"
90
91
  };
91
92
  }
92
93
  function loadConfig() {
@@ -157,7 +158,8 @@ function loadConfig() {
157
158
  project_name: asString(config["fleet"]?.["project_name"], defaults.fleet.project_name),
158
159
  namespace: asString(config["fleet"]?.["namespace"], defaults.fleet.namespace),
159
160
  api_key: asString(config["fleet"]?.["api_key"], defaults.fleet.api_key)
160
- }
161
+ },
162
+ tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
161
163
  };
162
164
  }
163
165
  function saveConfig(config) {
@@ -212,6 +214,11 @@ function asObserverMode(value, fallback) {
212
214
  return value;
213
215
  return fallback;
214
216
  }
217
+ function asToolProfile(value, fallback) {
218
+ if (value === "full" || value === "memory")
219
+ return value;
220
+ return fallback;
221
+ }
215
222
  function asTeams(value, fallback) {
216
223
  if (!Array.isArray(value))
217
224
  return fallback;
@@ -845,6 +852,7 @@ function ensureObservationTypes(db) {
845
852
  DROP TABLE observations;
846
853
  ALTER TABLE observations_repair RENAME TO observations;
847
854
  CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
855
+ CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
848
856
  CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
849
857
  CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
850
858
  CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
@@ -1125,6 +1133,7 @@ class MemDatabase {
1125
1133
  this.db = openDatabase(dbPath);
1126
1134
  this.db.exec("PRAGMA journal_mode = WAL");
1127
1135
  this.db.exec("PRAGMA foreign_keys = ON");
1136
+ this.db.exec("PRAGMA busy_timeout = 5000");
1128
1137
  this.vecAvailable = this.loadVecExtension();
1129
1138
  runMigrations(this.db);
1130
1139
  ensureObservationTypes(this.db);
@@ -1146,8 +1155,16 @@ class MemDatabase {
1146
1155
  this.db.close();
1147
1156
  }
1148
1157
  upsertProject(project) {
1158
+ const canonicalId = project.canonical_id?.trim();
1159
+ const name = project.name?.trim();
1160
+ if (!canonicalId) {
1161
+ throw new Error("Project canonical_id is required");
1162
+ }
1163
+ if (!name) {
1164
+ throw new Error("Project name is required");
1165
+ }
1149
1166
  const now = Math.floor(Date.now() / 1000);
1150
- const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(project.canonical_id);
1167
+ const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
1151
1168
  if (existing) {
1152
1169
  this.db.query(`UPDATE projects SET
1153
1170
  local_path = COALESCE(?, local_path),
@@ -1162,7 +1179,7 @@ class MemDatabase {
1162
1179
  };
1163
1180
  }
1164
1181
  const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
1165
- VALUES (?, ?, ?, ?, ?, ?)`).run(project.canonical_id, project.name, project.local_path ?? null, project.remote_url ?? null, now, now);
1182
+ VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
1166
1183
  return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
1167
1184
  }
1168
1185
  getProjectByCanonicalId(canonicalId) {
@@ -1930,10 +1947,11 @@ function readProjectConfigFile(directory) {
1930
1947
  }
1931
1948
  }
1932
1949
  function detectProject(directory) {
1933
- const remoteUrl = getGitRemoteUrl(directory);
1950
+ const resolvedDirectory = resolve(directory);
1951
+ const remoteUrl = getGitRemoteUrl(resolvedDirectory);
1934
1952
  if (remoteUrl) {
1935
1953
  const canonicalId = normaliseGitRemoteUrl(remoteUrl);
1936
- const repoRoot = getGitTopLevel(directory) ?? directory;
1954
+ const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
1937
1955
  return {
1938
1956
  canonical_id: canonicalId,
1939
1957
  name: projectNameFromCanonicalId(canonicalId),
@@ -1941,21 +1959,22 @@ function detectProject(directory) {
1941
1959
  local_path: repoRoot
1942
1960
  };
1943
1961
  }
1944
- const configFile = readProjectConfigFile(directory);
1962
+ const configFile = readProjectConfigFile(resolvedDirectory);
1945
1963
  if (configFile) {
1946
1964
  return {
1947
1965
  canonical_id: configFile.project_id,
1948
1966
  name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
1949
1967
  remote_url: null,
1950
- local_path: directory
1968
+ local_path: resolvedDirectory
1951
1969
  };
1952
1970
  }
1953
- const dirName = basename(directory);
1971
+ const dirName = basename(resolvedDirectory);
1972
+ const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
1954
1973
  return {
1955
- canonical_id: `local/${dirName}`,
1956
- name: dirName,
1974
+ canonical_id: `local/${safeDirName}`,
1975
+ name: safeDirName,
1957
1976
  remote_url: null,
1958
- local_path: directory
1977
+ local_path: resolvedDirectory
1959
1978
  };
1960
1979
  }
1961
1980
  function detectProjectForPath(filePath, fallbackCwd) {
@@ -3380,7 +3399,19 @@ function parseJsonArray2(value) {
3380
3399
  }
3381
3400
  }
3382
3401
  function compactLine(value) {
3383
- const trimmed = value?.replace(/\s+/g, " ").trim();
3402
+ if (value === null || value === undefined)
3403
+ return null;
3404
+ let text;
3405
+ if (typeof value === "string") {
3406
+ text = value;
3407
+ } else {
3408
+ try {
3409
+ text = JSON.stringify(value);
3410
+ } catch {
3411
+ text = String(value);
3412
+ }
3413
+ }
3414
+ const trimmed = text.replace(/\s+/g, " ").trim();
3384
3415
  if (!trimmed)
3385
3416
  return null;
3386
3417
  return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
@@ -162,7 +162,8 @@ function createDefaultConfig() {
162
162
  project_name: "shared-experience",
163
163
  namespace: "",
164
164
  api_key: ""
165
- }
165
+ },
166
+ tool_profile: "full"
166
167
  };
167
168
  }
168
169
  function loadConfig() {
@@ -233,7 +234,8 @@ function loadConfig() {
233
234
  project_name: asString(config["fleet"]?.["project_name"], defaults.fleet.project_name),
234
235
  namespace: asString(config["fleet"]?.["namespace"], defaults.fleet.namespace),
235
236
  api_key: asString(config["fleet"]?.["api_key"], defaults.fleet.api_key)
236
- }
237
+ },
238
+ tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
237
239
  };
238
240
  }
239
241
  function saveConfig(config) {
@@ -288,6 +290,11 @@ function asObserverMode(value, fallback) {
288
290
  return value;
289
291
  return fallback;
290
292
  }
293
+ function asToolProfile(value, fallback) {
294
+ if (value === "full" || value === "memory")
295
+ return value;
296
+ return fallback;
297
+ }
291
298
  function asTeams(value, fallback) {
292
299
  if (!Array.isArray(value))
293
300
  return fallback;
@@ -921,6 +928,7 @@ function ensureObservationTypes(db) {
921
928
  DROP TABLE observations;
922
929
  ALTER TABLE observations_repair RENAME TO observations;
923
930
  CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
931
+ CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
924
932
  CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
925
933
  CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
926
934
  CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
@@ -1201,6 +1209,7 @@ class MemDatabase {
1201
1209
  this.db = openDatabase(dbPath);
1202
1210
  this.db.exec("PRAGMA journal_mode = WAL");
1203
1211
  this.db.exec("PRAGMA foreign_keys = ON");
1212
+ this.db.exec("PRAGMA busy_timeout = 5000");
1204
1213
  this.vecAvailable = this.loadVecExtension();
1205
1214
  runMigrations(this.db);
1206
1215
  ensureObservationTypes(this.db);
@@ -1222,8 +1231,16 @@ class MemDatabase {
1222
1231
  this.db.close();
1223
1232
  }
1224
1233
  upsertProject(project) {
1234
+ const canonicalId = project.canonical_id?.trim();
1235
+ const name = project.name?.trim();
1236
+ if (!canonicalId) {
1237
+ throw new Error("Project canonical_id is required");
1238
+ }
1239
+ if (!name) {
1240
+ throw new Error("Project name is required");
1241
+ }
1225
1242
  const now = Math.floor(Date.now() / 1000);
1226
- const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(project.canonical_id);
1243
+ const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
1227
1244
  if (existing) {
1228
1245
  this.db.query(`UPDATE projects SET
1229
1246
  local_path = COALESCE(?, local_path),
@@ -1238,7 +1255,7 @@ class MemDatabase {
1238
1255
  };
1239
1256
  }
1240
1257
  const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
1241
- VALUES (?, ?, ?, ?, ?, ?)`).run(project.canonical_id, project.name, project.local_path ?? null, project.remote_url ?? null, now, now);
1258
+ VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
1242
1259
  return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
1243
1260
  }
1244
1261
  getProjectByCanonicalId(canonicalId) {
@@ -2060,10 +2077,11 @@ function readProjectConfigFile(directory) {
2060
2077
  }
2061
2078
  }
2062
2079
  function detectProject(directory) {
2063
- const remoteUrl = getGitRemoteUrl(directory);
2080
+ const resolvedDirectory = resolve(directory);
2081
+ const remoteUrl = getGitRemoteUrl(resolvedDirectory);
2064
2082
  if (remoteUrl) {
2065
2083
  const canonicalId = normaliseGitRemoteUrl(remoteUrl);
2066
- const repoRoot = getGitTopLevel(directory) ?? directory;
2084
+ const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
2067
2085
  return {
2068
2086
  canonical_id: canonicalId,
2069
2087
  name: projectNameFromCanonicalId(canonicalId),
@@ -2071,21 +2089,22 @@ function detectProject(directory) {
2071
2089
  local_path: repoRoot
2072
2090
  };
2073
2091
  }
2074
- const configFile = readProjectConfigFile(directory);
2092
+ const configFile = readProjectConfigFile(resolvedDirectory);
2075
2093
  if (configFile) {
2076
2094
  return {
2077
2095
  canonical_id: configFile.project_id,
2078
2096
  name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
2079
2097
  remote_url: null,
2080
- local_path: directory
2098
+ local_path: resolvedDirectory
2081
2099
  };
2082
2100
  }
2083
- const dirName = basename(directory);
2101
+ const dirName = basename(resolvedDirectory);
2102
+ const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
2084
2103
  return {
2085
- canonical_id: `local/${dirName}`,
2086
- name: dirName,
2104
+ canonical_id: `local/${safeDirName}`,
2105
+ name: safeDirName,
2087
2106
  remote_url: null,
2088
- local_path: directory
2107
+ local_path: resolvedDirectory
2089
2108
  };
2090
2109
  }
2091
2110
  function detectProjectForPath(filePath, fallbackCwd) {