wayfind 2.0.60 → 2.0.62

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.
@@ -421,6 +421,28 @@ function cosineSimilarity(a, b) {
421
421
  * @param {boolean} [options.embeddings] - Generate embeddings (default: true if OPENAI_API_KEY or TEAM_CONTEXT_SIMULATE)
422
422
  * @returns {Promise<Object>} - Stats: { entryCount, newEntries, updatedEntries, skippedEntries, removedEntries }
423
423
  */
424
+
425
+ /**
426
+ * Embed a batch of items concurrently.
427
+ * @param {Array<{id: string, content: string}>} items
428
+ * @param {number} [concurrency=20]
429
+ * @returns {Promise<Map<string, Array|null>>} Map from id to vector (or null on failure)
430
+ */
431
+ async function batchEmbed(items, concurrency = 20) {
432
+ const results = new Map();
433
+ for (let i = 0; i < items.length; i += concurrency) {
434
+ const chunk = items.slice(i, i + concurrency);
435
+ await Promise.all(chunk.map(async ({ id, content }) => {
436
+ try {
437
+ results.set(id, await llm.generateEmbedding(content));
438
+ } catch {
439
+ results.set(id, null);
440
+ }
441
+ }));
442
+ }
443
+ return results;
444
+ }
445
+
424
446
  async function indexJournals(options = {}) {
425
447
  const journalDir = options.journalDir || DEFAULT_JOURNAL_DIR;
426
448
  const storePath = options.storePath || resolveStorePath();
@@ -497,10 +519,11 @@ async function indexJournals(options = {}) {
497
519
  }
498
520
  }
499
521
 
500
- // Compute diffs
522
+ // Compute diffs — collect embedding work for batching
501
523
  const stats = { entryCount: 0, newEntries: 0, updatedEntries: 0, skippedEntries: 0, removedEntries: 0 };
502
524
  const finalEntries = {};
503
525
  const finalEmbeddings = { ...existingEmbeddings };
526
+ const pendingEmbeds = []; // { id, content, reason: 'update'|'changed'|'new' }
504
527
 
505
528
  for (const [id, entry] of Object.entries(newEntries)) {
506
529
  const existing = existingEntries[id];
@@ -511,46 +534,38 @@ async function indexJournals(options = {}) {
511
534
  // Unchanged — but generate embedding if missing and embeddings are enabled
512
535
  entry.hasEmbedding = existing.hasEmbedding;
513
536
  if (doEmbeddings && !existing.hasEmbedding && content) {
514
- try {
515
- const vec = await llm.generateEmbedding(content);
516
- finalEmbeddings[id] = vec;
517
- entry.hasEmbedding = true;
518
- stats.updatedEntries++;
519
- } catch (err) {
520
- stats.skippedEntries++;
521
- }
537
+ pendingEmbeds.push({ id, content, reason: 'update' });
522
538
  } else {
523
539
  stats.skippedEntries++;
524
540
  }
525
541
  finalEntries[id] = entry;
526
542
  } else if (existing) {
527
543
  // Changed — re-embed
528
- if (doEmbeddings) {
529
- try {
530
- const vec = await llm.generateEmbedding(content);
531
- finalEmbeddings[id] = vec;
532
- entry.hasEmbedding = true;
533
- } catch (err) {
534
- // Keep going without embedding for this entry
535
- entry.hasEmbedding = false;
536
- delete finalEmbeddings[id];
537
- }
538
- }
539
544
  finalEntries[id] = entry;
540
545
  stats.updatedEntries++;
546
+ if (doEmbeddings) pendingEmbeds.push({ id, content, reason: 'changed' });
541
547
  } else {
542
548
  // New entry
543
- if (doEmbeddings) {
544
- try {
545
- const vec = await llm.generateEmbedding(content);
546
- finalEmbeddings[id] = vec;
547
- entry.hasEmbedding = true;
548
- } catch (err) {
549
- entry.hasEmbedding = false;
550
- }
551
- }
552
549
  finalEntries[id] = entry;
553
550
  stats.newEntries++;
551
+ if (doEmbeddings) pendingEmbeds.push({ id, content, reason: 'new' });
552
+ }
553
+ }
554
+
555
+ // Batch embed all pending entries concurrently
556
+ if (pendingEmbeds.length > 0) {
557
+ const embedResults = await batchEmbed(pendingEmbeds);
558
+ for (const { id, reason } of pendingEmbeds) {
559
+ const vec = embedResults.get(id);
560
+ if (vec) {
561
+ finalEmbeddings[id] = vec;
562
+ finalEntries[id].hasEmbedding = true;
563
+ if (reason === 'update') stats.updatedEntries++;
564
+ } else {
565
+ if (reason === 'update') stats.skippedEntries++;
566
+ else if (reason === 'changed') { finalEntries[id].hasEmbedding = false; delete finalEmbeddings[id]; }
567
+ // 'new': hasEmbedding stays false
568
+ }
554
569
  }
555
570
  }
556
571
 
@@ -1582,18 +1597,6 @@ async function indexConversations(options = {}) {
1582
1597
  };
1583
1598
  convEntry.qualityScore = computeQualityScore(convEntry);
1584
1599
  existingIndex.entries[id] = convEntry;
1585
-
1586
- if (doEmbeddings) {
1587
- try {
1588
- const vec = await llm.generateEmbedding(content);
1589
- existingEmbeddings[id] = vec;
1590
- existingIndex.entries[id].hasEmbedding = true;
1591
- } catch {
1592
- // Continue without embedding
1593
- }
1594
- }
1595
-
1596
- delete existingIndex.entries[id]._content;
1597
1600
  entryIds.push(id);
1598
1601
  stats.decisionsExtracted++;
1599
1602
  }
@@ -1608,6 +1611,29 @@ async function indexConversations(options = {}) {
1608
1611
  stats.transcriptsProcessed++;
1609
1612
  }
1610
1613
 
1614
+ // Batch embed all new conversation entries
1615
+ if (doEmbeddings) {
1616
+ const pendingEmbeds = Object.entries(existingIndex.entries)
1617
+ .filter(([, e]) => e._content && !e.hasEmbedding)
1618
+ .map(([id, e]) => ({ id, content: e._content }));
1619
+
1620
+ if (pendingEmbeds.length > 0) {
1621
+ const embedResults = await batchEmbed(pendingEmbeds);
1622
+ for (const { id } of pendingEmbeds) {
1623
+ const vec = embedResults.get(id);
1624
+ if (vec) {
1625
+ existingEmbeddings[id] = vec;
1626
+ existingIndex.entries[id].hasEmbedding = true;
1627
+ }
1628
+ }
1629
+ }
1630
+ }
1631
+
1632
+ // Strip temp _content fields
1633
+ for (const entry of Object.values(existingIndex.entries)) {
1634
+ delete entry._content;
1635
+ }
1636
+
1611
1637
  // Save everything
1612
1638
  existingIndex.entryCount = Object.keys(existingIndex.entries).length;
1613
1639
  if (doEmbeddings) {
@@ -1876,6 +1902,7 @@ async function indexSignals(options = {}) {
1876
1902
  const existingEmbeddings = doEmbeddings ? (_sigModelChanged ? {} : backend.loadEmbeddings()) : {};
1877
1903
 
1878
1904
  const stats = { fileCount: 0, newEntries: 0, updatedEntries: 0, skippedEntries: 0 };
1905
+ const signalPendingEmbeds = []; // { id, content, reason: 'update'|'changed'|'new'|'chunk-update'|'chunk-new' }
1879
1906
 
1880
1907
  // Scan channel directories
1881
1908
  let channels;
@@ -1960,14 +1987,7 @@ async function indexSignals(options = {}) {
1960
1987
  if (existing && existing.contentHash === hash) {
1961
1988
  // Unchanged — but generate embedding if missing
1962
1989
  if (doEmbeddings && !existing.hasEmbedding && content) {
1963
- try {
1964
- const vec = await llm.generateEmbedding(content);
1965
- existingEmbeddings[id] = vec;
1966
- existing.hasEmbedding = true;
1967
- stats.updatedEntries++;
1968
- } catch {
1969
- stats.skippedEntries++;
1970
- }
1990
+ signalPendingEmbeds.push({ id, content, reason: 'update' });
1971
1991
  } else {
1972
1992
  stats.skippedEntries++;
1973
1993
  }
@@ -1985,16 +2005,7 @@ async function indexSignals(options = {}) {
1985
2005
  tags,
1986
2006
  hasEmbedding: false,
1987
2007
  };
1988
-
1989
- if (doEmbeddings) {
1990
- try {
1991
- const vec = await llm.generateEmbedding(content);
1992
- existingEmbeddings[id] = vec;
1993
- existingIndex.entries[id].hasEmbedding = true;
1994
- } catch {
1995
- delete existingEmbeddings[id];
1996
- }
1997
- }
2008
+ if (doEmbeddings) signalPendingEmbeds.push({ id, content, reason: 'changed' });
1998
2009
  stats.updatedEntries++;
1999
2010
  } else {
2000
2011
  // New entry
@@ -2010,16 +2021,7 @@ async function indexSignals(options = {}) {
2010
2021
  tags,
2011
2022
  hasEmbedding: false,
2012
2023
  };
2013
-
2014
- if (doEmbeddings) {
2015
- try {
2016
- const vec = await llm.generateEmbedding(content);
2017
- existingEmbeddings[id] = vec;
2018
- existingIndex.entries[id].hasEmbedding = true;
2019
- } catch {
2020
- // Continue without embedding
2021
- }
2022
- }
2024
+ if (doEmbeddings) signalPendingEmbeds.push({ id, content, reason: 'new' });
2023
2025
  stats.newEntries++;
2024
2026
  }
2025
2027
  }
@@ -2095,13 +2097,7 @@ async function indexSignals(options = {}) {
2095
2097
 
2096
2098
  if (existingChunk && existingChunk.contentHash === chunkHash) {
2097
2099
  if (doEmbeddings && !existingChunk.hasEmbedding) {
2098
- try {
2099
- const vec = await llm.generateEmbedding(section);
2100
- existingEmbeddings[chunkId] = vec;
2101
- existingChunk.hasEmbedding = true;
2102
- } catch {
2103
- // Skip
2104
- }
2100
+ signalPendingEmbeds.push({ id: chunkId, content: section, reason: 'chunk-update' });
2105
2101
  }
2106
2102
  continue;
2107
2103
  }
@@ -2122,13 +2118,24 @@ async function indexSignals(options = {}) {
2122
2118
  };
2123
2119
 
2124
2120
  if (doEmbeddings) {
2125
- try {
2126
- const vec = await llm.generateEmbedding(section);
2127
- existingEmbeddings[chunkId] = vec;
2128
- existingIndex.entries[chunkId].hasEmbedding = true;
2129
- } catch {
2130
- // Continue without embedding
2131
- }
2121
+ signalPendingEmbeds.push({ id: chunkId, content: section, reason: 'chunk-new' });
2122
+ }
2123
+ }
2124
+ }
2125
+
2126
+ // Batch embed all pending signal entries
2127
+ if (signalPendingEmbeds.length > 0) {
2128
+ const embedResults = await batchEmbed(signalPendingEmbeds);
2129
+ for (const { id, reason } of signalPendingEmbeds) {
2130
+ const vec = embedResults.get(id);
2131
+ if (vec) {
2132
+ existingEmbeddings[id] = vec;
2133
+ existingIndex.entries[id].hasEmbedding = true;
2134
+ if (reason === 'update') stats.updatedEntries++;
2135
+ } else {
2136
+ if (reason === 'update') stats.skippedEntries++;
2137
+ else if (reason === 'changed') { existingIndex.entries[id].hasEmbedding = false; delete existingEmbeddings[id]; }
2138
+ // 'new', 'chunk-update', 'chunk-new': hasEmbedding stays false
2132
2139
  }
2133
2140
  }
2134
2141
  }
@@ -203,11 +203,13 @@ class SqliteBackend {
203
203
  for (const row of rows) {
204
204
  entries[row.id] = rowToEntry(row);
205
205
  }
206
+ const embeddingModelRow = this.db.prepare("SELECT value FROM metadata WHERE key = 'embedding_model'").get();
206
207
  return {
207
208
  version: INDEX_VERSION,
208
209
  lastUpdated: Date.now(),
209
210
  entryCount: rows.length,
210
211
  entries,
212
+ ...(embeddingModelRow ? { embedding_model: embeddingModelRow.value } : {}),
211
213
  };
212
214
  }
213
215
 
@@ -228,6 +230,9 @@ class SqliteBackend {
228
230
  for (const [id, entry] of Object.entries(entries)) {
229
231
  stmt.run(entryToRow(id, entry));
230
232
  }
233
+ if (index.embedding_model) {
234
+ this.db.prepare('INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)').run('embedding_model', index.embedding_model);
235
+ }
231
236
  });
232
237
  txn();
233
238
  }
@@ -4926,12 +4926,12 @@ async function indexJournalsIfAvailable() {
4926
4926
  console.log('No journal files found — skipping index.');
4927
4927
  return;
4928
4928
  }
4929
- const hasEmbeddingKey = !!(process.env.OPENAI_API_KEY || process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT);
4930
- console.log(`Indexing ${entries.length} journal files from ${journalDir}${hasEmbeddingKey ? ' (with embeddings)' : ''}...`);
4929
+ const hasEmbeddings = !!(process.env.OPENAI_API_KEY || process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT) || llm.getEmbeddingProviderInfo().available;
4930
+ console.log(`Indexing ${entries.length} journal files from ${journalDir}${hasEmbeddings ? ' (with embeddings)' : ''}...`);
4931
4931
  try {
4932
4932
  const stats = await contentStore.indexJournals({
4933
4933
  journalDir,
4934
- embeddings: hasEmbeddingKey,
4934
+ embeddings: hasEmbeddings,
4935
4935
  });
4936
4936
  console.log(`Indexed ${stats.entryCount} entries (${stats.newEntries} new, ${stats.updatedEntries} updated).`);
4937
4937
  } catch (err) {
@@ -4953,7 +4953,7 @@ async function indexConversationsIfAvailable() {
4953
4953
  try {
4954
4954
  const stats = await contentStore.indexConversations({
4955
4955
  projectsDir,
4956
- embeddings: !!(process.env.OPENAI_API_KEY || process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT),
4956
+ embeddings: !!(process.env.OPENAI_API_KEY || process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT) || llm.getEmbeddingProviderInfo().available,
4957
4957
  });
4958
4958
  console.log(`Conversations: ${stats.transcriptsProcessed} processed, ${stats.decisionsExtracted} decisions extracted (${stats.skipped} skipped).`);
4959
4959
  } catch (err) {
@@ -4967,12 +4967,12 @@ async function indexSignalsIfAvailable() {
4967
4967
  console.log(`No signals at ${signalsDir} — skipping index.`);
4968
4968
  return;
4969
4969
  }
4970
- const hasEmbeddingKey = !!(process.env.OPENAI_API_KEY || process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT);
4971
- console.log(`Indexing signals from ${signalsDir}${hasEmbeddingKey ? ' (with embeddings)' : ''}...`);
4970
+ const hasEmbeddings = !!(process.env.OPENAI_API_KEY || process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT) || llm.getEmbeddingProviderInfo().available;
4971
+ console.log(`Indexing signals from ${signalsDir}${hasEmbeddings ? ' (with embeddings)' : ''}...`);
4972
4972
  try {
4973
4973
  const stats = await contentStore.indexSignals({
4974
4974
  signalsDir,
4975
- embeddings: hasEmbeddingKey,
4975
+ embeddings: hasEmbeddings,
4976
4976
  });
4977
4977
  console.log(`Signals: ${stats.fileCount} files (${stats.newEntries} new, ${stats.updatedEntries} updated, ${stats.skippedEntries} skipped).`);
4978
4978
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.60",
3
+ "version": "2.0.62",
4
4
  "description": "Team decision trail for AI-assisted development. The connective tissue between product, engineering, and strategy.",
5
5
  "bin": {
6
6
  "wayfind": "./bin/team-context.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.59",
3
+ "version": "2.0.62",
4
4
  "description": "Team decision trail for AI-assisted development. Session memory, decision journals, and team digests.",
5
5
  "author": {
6
6
  "name": "Wayfind",