wayfind 2.0.76 → 2.0.77

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.
@@ -1401,8 +1401,8 @@ async function indexConversations(options = {}) {
1401
1401
  candidates.push({ filePath, fp, transcript, transcriptText });
1402
1402
  }
1403
1403
 
1404
- // Phase 2: Fire LLM extraction calls in parallel with concurrency cap
1405
- // Cap at 5 to avoid API rate limits while still getting parallelism benefit
1404
+ // Phase 2+3: Extract decisions and merge results in batches.
1405
+ // Saving convIndex after each batch ensures a hook timeout doesn't discard all progress.
1406
1406
  const MAX_CONCURRENT = 5;
1407
1407
 
1408
1408
  if (options.onProgress) {
@@ -1411,7 +1411,6 @@ async function indexConversations(options = {}) {
1411
1411
  }
1412
1412
  }
1413
1413
 
1414
- const extractionResults = [];
1415
1414
  for (let i = 0; i < candidates.length; i += MAX_CONCURRENT) {
1416
1415
  const batch = candidates.slice(i, i + MAX_CONCURRENT);
1417
1416
  const batchResults = await Promise.all(
@@ -1424,65 +1423,67 @@ async function indexConversations(options = {}) {
1424
1423
  }
1425
1424
  })
1426
1425
  );
1427
- extractionResults.push(...batchResults);
1428
- }
1429
1426
 
1430
- // Phase 3: Merge results into shared indexes (sequential single-threaded, no races)
1431
- for (const result of extractionResults) {
1432
- if (result.error) {
1433
- console.error(`Extraction failed for ${result.filePath}: ${result.error}`);
1434
- stats.errors++;
1435
- continue;
1436
- }
1427
+ // Merge batch results immediately so a timeout preserves partial progress
1428
+ for (const result of batchResults) {
1429
+ if (result.error) {
1430
+ console.error(`Extraction failed for ${result.filePath}: ${result.error}`);
1431
+ stats.errors++;
1432
+ continue;
1433
+ }
1437
1434
 
1438
- const { filePath, fp, transcript, decisions } = result;
1435
+ const { filePath, fp, transcript, decisions } = result;
1439
1436
 
1440
- // Store extracted decisions in the content store
1441
- const entryIds = [];
1442
- const date = transcript.timestamp
1443
- ? transcript.timestamp.slice(0, 10)
1444
- : new Date().toISOString().slice(0, 10);
1437
+ // Store extracted decisions in the content store
1438
+ const entryIds = [];
1439
+ const date = transcript.timestamp
1440
+ ? transcript.timestamp.slice(0, 10)
1441
+ : new Date().toISOString().slice(0, 10);
1445
1442
 
1446
- for (const decision of decisions) {
1447
- const id = generateEntryId(date, transcript.repo, decision.title);
1448
- const content = [
1449
- `${transcript.repo} — ${decision.title}`,
1450
- `Date: ${date}`,
1451
- `Decision: ${decision.decision}`,
1452
- decision.alternatives ? `Alternatives considered: ${decision.alternatives}` : '',
1453
- ].filter(Boolean).join('\n');
1443
+ for (const decision of decisions) {
1444
+ const id = generateEntryId(date, transcript.repo, decision.title);
1445
+ const content = [
1446
+ `${transcript.repo} — ${decision.title}`,
1447
+ `Date: ${date}`,
1448
+ `Decision: ${decision.decision}`,
1449
+ decision.alternatives ? `Alternatives considered: ${decision.alternatives}` : '',
1450
+ ].filter(Boolean).join('\n');
1454
1451
 
1455
- const hash = contentHash(content);
1452
+ const hash = contentHash(content);
1456
1453
 
1457
- const convEntry = {
1458
- date,
1459
- repo: transcript.repo,
1460
- title: decision.title,
1461
- source: 'conversation',
1462
- user: '',
1463
- drifted: false,
1464
- contentHash: hash,
1465
- contentLength: content.length,
1466
- tags: decision.tags || [],
1467
- hasEmbedding: false,
1468
- hasReasoning: !!decision.has_reasoning,
1469
- hasAlternatives: !!decision.has_alternatives,
1470
- _content: content,
1471
- };
1472
- convEntry.qualityScore = computeQualityScore(convEntry);
1473
- existingIndex.entries[id] = convEntry;
1474
- entryIds.push(id);
1475
- stats.decisionsExtracted++;
1476
- }
1454
+ const convEntry = {
1455
+ date,
1456
+ repo: transcript.repo,
1457
+ title: decision.title,
1458
+ source: 'conversation',
1459
+ user: '',
1460
+ drifted: false,
1461
+ contentHash: hash,
1462
+ contentLength: content.length,
1463
+ tags: decision.tags || [],
1464
+ hasEmbedding: false,
1465
+ hasReasoning: !!decision.has_reasoning,
1466
+ hasAlternatives: !!decision.has_alternatives,
1467
+ _content: content,
1468
+ };
1469
+ convEntry.qualityScore = computeQualityScore(convEntry);
1470
+ existingIndex.entries[id] = convEntry;
1471
+ entryIds.push(id);
1472
+ stats.decisionsExtracted++;
1473
+ }
1474
+
1475
+ // Notify caller of extracted decisions (used for journal export)
1476
+ if (options.onDecisions && decisions.length > 0) {
1477
+ options.onDecisions(date, transcript.repo, decisions, fp);
1478
+ }
1477
1479
 
1478
- // Notify caller of extracted decisions (used for journal export)
1479
- if (options.onDecisions && decisions.length > 0) {
1480
- options.onDecisions(date, transcript.repo, decisions);
1480
+ // Update conversation index
1481
+ convIndex[filePath] = { fingerprint: fp, entryIds, extractedAt: Date.now() };
1482
+ stats.transcriptsProcessed++;
1481
1483
  }
1482
1484
 
1483
- // Update conversation index
1484
- convIndex[filePath] = { fingerprint: fp, entryIds, extractedAt: Date.now() };
1485
- stats.transcriptsProcessed++;
1485
+ // Save convIndex after each batch — partial progress survives a timeout kill
1486
+ backend.saveConversationIndex(convIndex);
1486
1487
  }
1487
1488
 
1488
1489
  // Batch embed all new conversation entries
@@ -1640,21 +1641,25 @@ async function generateOnboardingPack(repoQuery, options = {}) {
1640
1641
  * @param {Array<{ title: string, decision: string, alternatives: string, tags: string[] }>} decisions
1641
1642
  * @param {string} journalDir - Journal directory path
1642
1643
  */
1643
- function exportDecisionsAsJournal(date, repo, decisions, journalDir, teamId, author) {
1644
+ function exportDecisionsAsJournal(date, repo, decisions, journalDir, teamId, author, srcFp) {
1644
1645
  if (!decisions || decisions.length === 0) return;
1645
1646
 
1646
1647
  const authorPart = author ? `-${author}` : '';
1647
1648
  const teamPart = teamId ? `-${teamId}` : '';
1648
1649
  const filePath = path.join(journalDir, `${date}${authorPart}${teamPart}.md`);
1649
1650
 
1650
- // Dedup each decision individually against existing file content
1651
1651
  const existing = fs.existsSync(filePath)
1652
1652
  ? fs.readFileSync(filePath, 'utf8')
1653
1653
  : null;
1654
1654
 
1655
+ // If this exact transcript (by fingerprint) was already exported, skip the whole block.
1656
+ // This prevents re-export when a growing transcript's fingerprint changes and it gets
1657
+ // re-extracted, producing LLM-rephrased titles that bypass title-substring dedup.
1658
+ if (srcFp && existing && existing.includes(`<!-- src-fp:${srcFp} -->`)) return;
1659
+
1655
1660
  const newLines = [];
1656
1661
  for (const d of decisions) {
1657
- // Skip if this decision's title already appears in the file
1662
+ // Fallback per-title dedup for entries written before src-fp markers were added
1658
1663
  if (existing && existing.includes(d.title)) continue;
1659
1664
 
1660
1665
  const qualityTags = [];
@@ -1678,6 +1683,9 @@ function exportDecisionsAsJournal(date, repo, decisions, journalDir, teamId, aut
1678
1683
 
1679
1684
  if (newLines.length === 0) return;
1680
1685
 
1686
+ // Append source fingerprint marker so future runs can skip this whole block
1687
+ if (srcFp) newLines.push(`<!-- src-fp:${srcFp} -->`);
1688
+
1681
1689
  const content = '\n' + newLines.join('\n');
1682
1690
 
1683
1691
  if (existing !== null) {
@@ -1709,16 +1717,16 @@ async function indexConversationsWithExport(options = {}) {
1709
1717
 
1710
1718
  const stats = await indexConversations({
1711
1719
  ...options,
1712
- onDecisions: exportDir ? (date, repo, decisions) => {
1713
- pendingExports.push({ date, repo, decisions });
1720
+ onDecisions: exportDir ? (date, repo, decisions, fp) => {
1721
+ pendingExports.push({ date, repo, decisions, fp });
1714
1722
  } : undefined,
1715
1723
  });
1716
1724
 
1717
1725
  // Write pending exports — route to per-team journal files
1718
- for (const { date, repo, decisions } of pendingExports) {
1726
+ for (const { date, repo, decisions, fp } of pendingExports) {
1719
1727
  const teamId = repoToTeam(repo);
1720
1728
  if (!teamId) continue; // Unbound repo — skip export (opt-in via .claude/wayfind.json)
1721
- exportDecisionsAsJournal(date, repo, decisions, exportDir, teamId, author);
1729
+ exportDecisionsAsJournal(date, repo, decisions, exportDir, teamId, author, fp);
1722
1730
  exported += decisions.length;
1723
1731
  for (const d of decisions) {
1724
1732
  if (d.has_reasoning || d.has_alternatives) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.76",
3
+ "version": "2.0.77",
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",