engrm 0.4.43 → 0.4.45

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.
@@ -97,10 +97,11 @@ function readProjectConfigFile(directory) {
97
97
  }
98
98
  }
99
99
  function detectProject(directory) {
100
- const remoteUrl = getGitRemoteUrl(directory);
100
+ const resolvedDirectory = resolve(directory);
101
+ const remoteUrl = getGitRemoteUrl(resolvedDirectory);
101
102
  if (remoteUrl) {
102
103
  const canonicalId = normaliseGitRemoteUrl(remoteUrl);
103
- const repoRoot = getGitTopLevel(directory) ?? directory;
104
+ const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
104
105
  return {
105
106
  canonical_id: canonicalId,
106
107
  name: projectNameFromCanonicalId(canonicalId),
@@ -108,21 +109,22 @@ function detectProject(directory) {
108
109
  local_path: repoRoot
109
110
  };
110
111
  }
111
- const configFile = readProjectConfigFile(directory);
112
+ const configFile = readProjectConfigFile(resolvedDirectory);
112
113
  if (configFile) {
113
114
  return {
114
115
  canonical_id: configFile.project_id,
115
116
  name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
116
117
  remote_url: null,
117
- local_path: directory
118
+ local_path: resolvedDirectory
118
119
  };
119
120
  }
120
- const dirName = basename(directory);
121
+ const dirName = basename(resolvedDirectory);
122
+ const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
121
123
  return {
122
- canonical_id: `local/${dirName}`,
123
- name: dirName,
124
+ canonical_id: `local/${safeDirName}`,
125
+ name: safeDirName,
124
126
  remote_url: null,
125
- local_path: directory
127
+ local_path: resolvedDirectory
126
128
  };
127
129
  }
128
130
  function detectProjectForPath(filePath, fallbackCwd) {
@@ -1625,7 +1627,19 @@ function parseJsonArray2(value) {
1625
1627
  }
1626
1628
  }
1627
1629
  function compactLine(value) {
1628
- const trimmed = value?.replace(/\s+/g, " ").trim();
1630
+ if (value === null || value === undefined)
1631
+ return null;
1632
+ let text;
1633
+ if (typeof value === "string") {
1634
+ text = value;
1635
+ } else {
1636
+ try {
1637
+ text = JSON.stringify(value);
1638
+ } catch {
1639
+ text = String(value);
1640
+ }
1641
+ }
1642
+ const trimmed = text.replace(/\s+/g, " ").trim();
1629
1643
  if (!trimmed)
1630
1644
  return null;
1631
1645
  return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
@@ -3266,7 +3280,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
3266
3280
  import { join as join3 } from "node:path";
3267
3281
  import { homedir } from "node:os";
3268
3282
  var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
3269
- var CLIENT_VERSION = "0.4.42";
3283
+ var CLIENT_VERSION = "0.4.45";
3270
3284
  function hashFile(filePath) {
3271
3285
  try {
3272
3286
  if (!existsSync3(filePath))
@@ -3619,6 +3633,96 @@ function asTeams(value, fallback) {
3619
3633
  return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
3620
3634
  }
3621
3635
 
3636
+ // src/sync/auth.ts
3637
+ import { createHash as createHash3 } from "node:crypto";
3638
+
3639
+ // src/storage/outbox.ts
3640
+ function getPendingEntries(db, limit = 50) {
3641
+ const now = Math.floor(Date.now() / 1000);
3642
+ return db.db.query(`SELECT * FROM sync_outbox
3643
+ WHERE (status = 'pending')
3644
+ OR (status = 'failed' AND retry_count < max_retries AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?))
3645
+ ORDER BY created_at_epoch ASC
3646
+ LIMIT ?`).all(now, limit);
3647
+ }
3648
+ function markSyncing(db, entryId) {
3649
+ const now = Math.floor(Date.now() / 1000);
3650
+ db.db.query("UPDATE sync_outbox SET status = 'syncing', next_retry_epoch = ? WHERE id = ?").run(now, entryId);
3651
+ }
3652
+ function markSynced(db, entryId) {
3653
+ const now = Math.floor(Date.now() / 1000);
3654
+ db.db.query("UPDATE sync_outbox SET status = 'synced', synced_at_epoch = ?, next_retry_epoch = NULL, last_error = NULL WHERE id = ?").run(now, entryId);
3655
+ }
3656
+ function markFailed(db, entryId, error) {
3657
+ const now = Math.floor(Date.now() / 1000);
3658
+ db.db.query(`UPDATE sync_outbox SET
3659
+ status = 'failed',
3660
+ retry_count = retry_count + 1,
3661
+ last_error = ?,
3662
+ next_retry_epoch = ? + MIN(30 * (1 << retry_count), 3600)
3663
+ WHERE id = ?`).run(error, now, entryId);
3664
+ }
3665
+ function classifyOutboxFailure(error) {
3666
+ const normalized = error.toLowerCase();
3667
+ if (normalized.includes("401") || normalized.includes("invalid or missing credentials")) {
3668
+ return "auth";
3669
+ }
3670
+ if (normalized.includes("429") || normalized.includes("rate limit")) {
3671
+ return "rate_limit";
3672
+ }
3673
+ if (normalized.includes("timeout") || normalized.includes("abort")) {
3674
+ return "timeout";
3675
+ }
3676
+ if (normalized.includes("network") || normalized.includes("fetch") || normalized.includes("econn")) {
3677
+ return "network";
3678
+ }
3679
+ if (normalized.includes("400") || normalized.includes("422") || normalized.includes("validation")) {
3680
+ return "validation";
3681
+ }
3682
+ return "other";
3683
+ }
3684
+ function resetFailedEntries(db) {
3685
+ const result = db.db.query(`UPDATE sync_outbox
3686
+ SET status = 'pending',
3687
+ retry_count = 0,
3688
+ last_error = NULL,
3689
+ next_retry_epoch = NULL
3690
+ WHERE status = 'failed'`).run();
3691
+ return result.changes;
3692
+ }
3693
+ function resetFailedEntriesMatching(db, predicate) {
3694
+ const rows = db.db.query(`SELECT id, last_error
3695
+ FROM sync_outbox
3696
+ WHERE status = 'failed'`).all();
3697
+ const matchingIds = rows.filter((row) => row.last_error && predicate(row.last_error)).map((row) => row.id);
3698
+ if (matchingIds.length === 0)
3699
+ return 0;
3700
+ const placeholders = matchingIds.map(() => "?").join(", ");
3701
+ const result = db.db.query(`UPDATE sync_outbox
3702
+ SET status = 'pending',
3703
+ retry_count = 0,
3704
+ last_error = NULL,
3705
+ next_retry_epoch = NULL
3706
+ WHERE id IN (${placeholders})`).run(...matchingIds);
3707
+ return result.changes;
3708
+ }
3709
+ function resetSyncingEntries(db) {
3710
+ const result = db.db.query(`UPDATE sync_outbox
3711
+ SET status = 'pending',
3712
+ next_retry_epoch = NULL
3713
+ WHERE status = 'syncing'`).run();
3714
+ return result.changes;
3715
+ }
3716
+ function resetStaleSyncingEntries(db, maxAgeSeconds = 300) {
3717
+ const cutoff = Math.floor(Date.now() / 1000) - maxAgeSeconds;
3718
+ const result = db.db.query(`UPDATE sync_outbox
3719
+ SET status = 'pending',
3720
+ next_retry_epoch = NULL
3721
+ WHERE status = 'syncing'
3722
+ AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?)`).run(cutoff);
3723
+ return result.changes;
3724
+ }
3725
+
3622
3726
  // src/sync/auth.ts
3623
3727
  var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
3624
3728
  function normalizeBaseUrl(url) {
@@ -3650,6 +3754,36 @@ function getBaseUrl(config) {
3650
3754
  }
3651
3755
  return null;
3652
3756
  }
3757
+ function getAuthFingerprint(config) {
3758
+ const apiKey = getApiKey(config);
3759
+ const baseUrl = getBaseUrl(config);
3760
+ if (!apiKey || !baseUrl)
3761
+ return null;
3762
+ return createHash3("sha256").update(`${baseUrl}
3763
+ ${apiKey}
3764
+ ${config.namespace}
3765
+ ${config.site_id}`).digest("hex");
3766
+ }
3767
+ function recoverOutboxAfterAuthChange(db, config) {
3768
+ const fingerprint = getAuthFingerprint(config);
3769
+ if (!fingerprint) {
3770
+ const staleSyncingReset2 = resetStaleSyncingEntries(db);
3771
+ return { fingerprintChanged: false, failedReset: 0, authFailedReset: 0, syncingReset: 0, staleSyncingReset: staleSyncingReset2 };
3772
+ }
3773
+ const key = "sync_auth_fingerprint";
3774
+ const previous = db.getSyncState(key);
3775
+ const fingerprintChanged = previous !== fingerprint;
3776
+ if (!fingerprintChanged) {
3777
+ const authFailedReset = resetFailedEntriesMatching(db, (error) => classifyOutboxFailure(error) === "auth");
3778
+ const staleSyncingReset2 = resetStaleSyncingEntries(db);
3779
+ return { fingerprintChanged: false, failedReset: 0, authFailedReset, syncingReset: 0, staleSyncingReset: staleSyncingReset2 };
3780
+ }
3781
+ const failedReset = resetFailedEntries(db);
3782
+ const syncingReset = resetSyncingEntries(db);
3783
+ const staleSyncingReset = 0;
3784
+ db.setSyncState(key, fingerprint);
3785
+ return { fingerprintChanged: true, failedReset, authFailedReset: 0, syncingReset, staleSyncingReset };
3786
+ }
3653
3787
  function buildSourceId(config, localId, type = "obs") {
3654
3788
  return `${config.user_id}-${config.device_id}-${type}-${localId}`;
3655
3789
  }
@@ -4639,6 +4773,7 @@ function ensureObservationTypes(db) {
4639
4773
  DROP TABLE observations;
4640
4774
  ALTER TABLE observations_repair RENAME TO observations;
4641
4775
  CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
4776
+ CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
4642
4777
  CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
4643
4778
  CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
4644
4779
  CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
@@ -4771,7 +4906,7 @@ function getSchemaVersion(db) {
4771
4906
  var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
4772
4907
 
4773
4908
  // src/storage/sqlite.ts
4774
- import { createHash as createHash3 } from "node:crypto";
4909
+ import { createHash as createHash4 } from "node:crypto";
4775
4910
  var IS_BUN = typeof globalThis.Bun !== "undefined";
4776
4911
  function openDatabase(dbPath) {
4777
4912
  if (IS_BUN) {
@@ -4839,6 +4974,7 @@ class MemDatabase {
4839
4974
  this.db = openDatabase(dbPath);
4840
4975
  this.db.exec("PRAGMA journal_mode = WAL");
4841
4976
  this.db.exec("PRAGMA foreign_keys = ON");
4977
+ this.db.exec("PRAGMA busy_timeout = 5000");
4842
4978
  this.vecAvailable = this.loadVecExtension();
4843
4979
  runMigrations(this.db);
4844
4980
  ensureObservationTypes(this.db);
@@ -4860,8 +4996,16 @@ class MemDatabase {
4860
4996
  this.db.close();
4861
4997
  }
4862
4998
  upsertProject(project) {
4999
+ const canonicalId = project.canonical_id?.trim();
5000
+ const name = project.name?.trim();
5001
+ if (!canonicalId) {
5002
+ throw new Error("Project canonical_id is required");
5003
+ }
5004
+ if (!name) {
5005
+ throw new Error("Project name is required");
5006
+ }
4863
5007
  const now = Math.floor(Date.now() / 1000);
4864
- const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(project.canonical_id);
5008
+ const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
4865
5009
  if (existing) {
4866
5010
  this.db.query(`UPDATE projects SET
4867
5011
  local_path = COALESCE(?, local_path),
@@ -4876,7 +5020,7 @@ class MemDatabase {
4876
5020
  };
4877
5021
  }
4878
5022
  const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
4879
- VALUES (?, ?, ?, ?, ?, ?)`).run(project.canonical_id, project.name, project.local_path ?? null, project.remote_url ?? null, now, now);
5023
+ VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
4880
5024
  return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
4881
5025
  }
4882
5026
  getProjectByCanonicalId(canonicalId) {
@@ -5552,7 +5696,7 @@ class MemDatabase {
5552
5696
  }
5553
5697
  }
5554
5698
  function hashPrompt(prompt) {
5555
- return createHash3("sha256").update(prompt).digest("hex");
5699
+ return createHash4("sha256").update(prompt).digest("hex");
5556
5700
  }
5557
5701
 
5558
5702
  // src/hooks/common.ts
@@ -1165,6 +1165,7 @@ function ensureObservationTypes(db) {
1165
1165
  DROP TABLE observations;
1166
1166
  ALTER TABLE observations_repair RENAME TO observations;
1167
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);
1168
1169
  CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
1169
1170
  CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
1170
1171
  CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
@@ -1365,6 +1366,7 @@ class MemDatabase {
1365
1366
  this.db = openDatabase(dbPath);
1366
1367
  this.db.exec("PRAGMA journal_mode = WAL");
1367
1368
  this.db.exec("PRAGMA foreign_keys = ON");
1369
+ this.db.exec("PRAGMA busy_timeout = 5000");
1368
1370
  this.vecAvailable = this.loadVecExtension();
1369
1371
  runMigrations(this.db);
1370
1372
  ensureObservationTypes(this.db);
@@ -1386,8 +1388,16 @@ class MemDatabase {
1386
1388
  this.db.close();
1387
1389
  }
1388
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
+ }
1389
1399
  const now = Math.floor(Date.now() / 1000);
1390
- 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);
1391
1401
  if (existing) {
1392
1402
  this.db.query(`UPDATE projects SET
1393
1403
  local_path = COALESCE(?, local_path),
@@ -1402,7 +1412,7 @@ class MemDatabase {
1402
1412
  };
1403
1413
  }
1404
1414
  const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
1405
- 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);
1406
1416
  return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
1407
1417
  }
1408
1418
  getProjectByCanonicalId(canonicalId) {
@@ -2214,6 +2224,96 @@ function formatRiskTrafficLight(result) {
2214
2224
  return `${icon} Risk: ${result.score}/100 (${result.level})`;
2215
2225
  }
2216
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
+
2217
2317
  // src/sync/auth.ts
2218
2318
  var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
2219
2319
  function normalizeBaseUrl(url) {
@@ -2245,6 +2345,36 @@ function getBaseUrl(config) {
2245
2345
  }
2246
2346
  return null;
2247
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
+ }
2248
2378
  function buildSourceId(config, localId, type = "obs") {
2249
2379
  return `${config.user_id}-${config.device_id}-${type}-${localId}`;
2250
2380
  }
@@ -2378,32 +2508,6 @@ class VectorApiError extends Error {
2378
2508
  }
2379
2509
  }
2380
2510
 
2381
- // src/storage/outbox.ts
2382
- function getPendingEntries(db, limit = 50) {
2383
- const now = Math.floor(Date.now() / 1000);
2384
- return db.db.query(`SELECT * FROM sync_outbox
2385
- WHERE (status = 'pending')
2386
- OR (status = 'failed' AND retry_count < max_retries AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?))
2387
- ORDER BY created_at_epoch ASC
2388
- LIMIT ?`).all(now, limit);
2389
- }
2390
- function markSyncing(db, entryId) {
2391
- db.db.query("UPDATE sync_outbox SET status = 'syncing' WHERE id = ?").run(entryId);
2392
- }
2393
- function markSynced(db, entryId) {
2394
- const now = Math.floor(Date.now() / 1000);
2395
- db.db.query("UPDATE sync_outbox SET status = 'synced', synced_at_epoch = ? WHERE id = ?").run(now, entryId);
2396
- }
2397
- function markFailed(db, entryId, error) {
2398
- const now = Math.floor(Date.now() / 1000);
2399
- db.db.query(`UPDATE sync_outbox SET
2400
- status = 'failed',
2401
- retry_count = retry_count + 1,
2402
- last_error = ?,
2403
- next_retry_epoch = ? + MIN(30 * (1 << retry_count), 3600)
2404
- WHERE id = ?`).run(error, now, entryId);
2405
- }
2406
-
2407
2511
  // src/intelligence/value-signals.ts
2408
2512
  var LESSON_TYPES = new Set(["bugfix", "decision", "pattern"]);
2409
2513
  function computeSessionValueSignals(observations, securityFindings = []) {
@@ -2484,7 +2588,19 @@ function buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolN
2484
2588
  return null;
2485
2589
  }
2486
2590
  function compactLine(value) {
2487
- 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();
2488
2604
  if (!trimmed)
2489
2605
  return null;
2490
2606
  return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
@@ -3015,13 +3131,20 @@ function summarizeObservationSourceTools(observations) {
3015
3131
  }
3016
3132
 
3017
3133
  // src/sync/push-once.ts
3134
+ function recordNowEpoch(db, key) {
3135
+ db.setSyncState(key, String(Math.floor(Date.now() / 1000)));
3136
+ }
3018
3137
  async function pushOnce(db, config, options = {}) {
3019
3138
  if (!config.sync.enabled)
3020
3139
  return 0;
3021
3140
  if (!VectorClient.isConfigured(config))
3022
3141
  return 0;
3023
3142
  try {
3143
+ recoverOutboxAfterAuthChange(db, config);
3024
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
+ }
3025
3148
  return result.pushed;
3026
3149
  } catch {
3027
3150
  return 0;
@@ -3245,7 +3368,7 @@ function buildBeacon(db, config, sessionId, metrics) {
3245
3368
  sentinel_used: valueSignals.security_findings_count > 0,
3246
3369
  risk_score: riskScore,
3247
3370
  stacks_detected: stacks,
3248
- client_version: "0.4.42",
3371
+ client_version: "0.4.45",
3249
3372
  context_observations_injected: metrics?.contextObsInjected ?? 0,
3250
3373
  context_total_available: metrics?.contextTotalAvailable ?? 0,
3251
3374
  recall_attempts: metrics?.recallAttempts ?? 0,
@@ -3377,10 +3500,11 @@ function readProjectConfigFile(directory) {
3377
3500
  }
3378
3501
  }
3379
3502
  function detectProject(directory) {
3380
- const remoteUrl = getGitRemoteUrl(directory);
3503
+ const resolvedDirectory = resolve(directory);
3504
+ const remoteUrl = getGitRemoteUrl(resolvedDirectory);
3381
3505
  if (remoteUrl) {
3382
3506
  const canonicalId = normaliseGitRemoteUrl(remoteUrl);
3383
- const repoRoot = getGitTopLevel(directory) ?? directory;
3507
+ const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
3384
3508
  return {
3385
3509
  canonical_id: canonicalId,
3386
3510
  name: projectNameFromCanonicalId(canonicalId),
@@ -3388,21 +3512,22 @@ function detectProject(directory) {
3388
3512
  local_path: repoRoot
3389
3513
  };
3390
3514
  }
3391
- const configFile = readProjectConfigFile(directory);
3515
+ const configFile = readProjectConfigFile(resolvedDirectory);
3392
3516
  if (configFile) {
3393
3517
  return {
3394
3518
  canonical_id: configFile.project_id,
3395
3519
  name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
3396
3520
  remote_url: null,
3397
- local_path: directory
3521
+ local_path: resolvedDirectory
3398
3522
  };
3399
3523
  }
3400
- const dirName = basename2(directory);
3524
+ const dirName = basename2(resolvedDirectory);
3525
+ const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
3401
3526
  return {
3402
- canonical_id: `local/${dirName}`,
3403
- name: dirName,
3527
+ canonical_id: `local/${safeDirName}`,
3528
+ name: safeDirName,
3404
3529
  remote_url: null,
3405
- local_path: directory
3530
+ local_path: resolvedDirectory
3406
3531
  };
3407
3532
  }
3408
3533
  function detectProjectForPath(filePath, fallbackCwd) {
@@ -3433,7 +3558,7 @@ function detectProjectFromTouchedPaths(paths, fallbackCwd) {
3433
3558
  }
3434
3559
 
3435
3560
  // src/capture/transcript.ts
3436
- import { createHash as createHash3 } from "node:crypto";
3561
+ import { createHash as createHash4 } from "node:crypto";
3437
3562
  import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
3438
3563
  import { join as join5 } from "node:path";
3439
3564
  import { homedir as homedir3 } from "node:os";
@@ -4230,7 +4355,7 @@ function dedupeHistoryMessages(messages) {
4230
4355
  return deduped;
4231
4356
  }
4232
4357
  function buildHistorySourceId(sessionId, createdAtEpoch, text) {
4233
- const digest = createHash3("sha1").update(text).digest("hex").slice(0, 12);
4358
+ const digest = createHash4("sha1").update(text).digest("hex").slice(0, 12);
4234
4359
  return `history:${sessionId}:${createdAtEpoch}:${digest}`;
4235
4360
  }
4236
4361
  function truncateTranscript(messages, maxBytes = 50000) {
@@ -4741,7 +4866,19 @@ function parseJsonArray4(value) {
4741
4866
  }
4742
4867
  }
4743
4868
  function compactLine2(value) {
4744
- 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();
4745
4882
  if (!trimmed)
4746
4883
  return null;
4747
4884
  return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;