@usewhisper/sdk 3.6.0 → 3.7.0

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.
package/index.mjs CHANGED
@@ -1353,6 +1353,29 @@ ${lines.join("\n")}`;
1353
1353
  function compactWhitespace(value) {
1354
1354
  return value.replace(/\s+/g, " ").trim();
1355
1355
  }
1356
+ function normalizeSummary(value) {
1357
+ return compactWhitespace(String(value || "").toLowerCase());
1358
+ }
1359
+ function tokenize(value) {
1360
+ return normalizeSummary(value).split(/[^a-z0-9_./-]+/i).map((token) => token.trim()).filter(Boolean);
1361
+ }
1362
+ function jaccardOverlap(left, right) {
1363
+ const leftTokens = new Set(tokenize(left));
1364
+ const rightTokens = new Set(tokenize(right));
1365
+ if (leftTokens.size === 0 || rightTokens.size === 0) return 0;
1366
+ let intersection = 0;
1367
+ for (const token of leftTokens) {
1368
+ if (rightTokens.has(token)) intersection += 1;
1369
+ }
1370
+ const union = (/* @__PURE__ */ new Set([...leftTokens, ...rightTokens])).size;
1371
+ return union > 0 ? intersection / union : 0;
1372
+ }
1373
+ function clamp01(value) {
1374
+ if (!Number.isFinite(value)) return 0;
1375
+ if (value < 0) return 0;
1376
+ if (value > 1) return 1;
1377
+ return value;
1378
+ }
1356
1379
  function withTimeout(promise, timeoutMs) {
1357
1380
  return new Promise((resolve, reject) => {
1358
1381
  const timeout = setTimeout(() => {
@@ -1386,39 +1409,76 @@ function extractTimestamp(metadata) {
1386
1409
  }
1387
1410
  return 0;
1388
1411
  }
1389
- function salienceBoost(metadata) {
1390
- const value = metadata?.salience;
1391
- if (value === "high") return 0.12;
1392
- if (value === "medium") return 0.06;
1393
- return 0;
1394
- }
1412
+ var DEFAULT_RANK_WEIGHTS = {
1413
+ focusedPassBonus: 0.2,
1414
+ sourceMatchBonus: 0.18,
1415
+ touchedFileBonus: 0.12,
1416
+ clientMatchBonus: 0.1,
1417
+ highSalienceBonus: 0.12,
1418
+ mediumSalienceBonus: 0.06,
1419
+ staleBroadPenalty: -0.1,
1420
+ unrelatedClientPenalty: -0.18,
1421
+ lowSaliencePenalty: -0.12
1422
+ };
1423
+ var DEFAULT_SOURCE_ACTIVITY = {
1424
+ maxTurns: 10,
1425
+ maxIdleMs: 30 * 60 * 1e3,
1426
+ decayAfterTurns: 5,
1427
+ decayAfterIdleMs: 15 * 60 * 1e3,
1428
+ evictOnTaskSwitch: true
1429
+ };
1395
1430
  var WhisperAgentRuntime = class {
1396
1431
  constructor(args) {
1397
1432
  this.args = args;
1398
1433
  this.bindingStore = createBindingStore(args.options.bindingStorePath);
1399
- this.topK = args.options.topK ?? 6;
1434
+ const retrieval = args.options.retrieval || {};
1435
+ this.focusedTopK = retrieval.focusedTopK ?? args.options.topK ?? 6;
1436
+ this.broadTopK = retrieval.broadTopK ?? Math.max(args.options.topK ?? 6, 10);
1400
1437
  this.maxTokens = args.options.maxTokens ?? 4e3;
1401
1438
  this.targetRetrievalMs = args.options.targetRetrievalMs ?? 2500;
1402
1439
  this.hardRetrievalTimeoutMs = args.options.hardRetrievalTimeoutMs ?? 4e3;
1403
1440
  this.recentWorkLimit = args.options.recentWorkLimit ?? 40;
1404
1441
  this.baseContext = args.baseContext;
1405
1442
  this.clientName = args.baseContext.clientName || "whisper-agent-runtime";
1443
+ this.minFocusedResults = retrieval.minFocusedResults ?? 3;
1444
+ this.minFocusedTopScore = retrieval.minFocusedTopScore ?? 0.55;
1445
+ this.minProjectScore = retrieval.minProjectScore ?? 0.5;
1446
+ this.minMemoryScore = retrieval.minMemoryScore ?? 0.6;
1447
+ this.rankWeights = { ...DEFAULT_RANK_WEIGHTS, ...retrieval.rankWeights || {} };
1448
+ this.sourceActivityOptions = { ...DEFAULT_SOURCE_ACTIVITY, ...retrieval.sourceActivity || {} };
1406
1449
  }
1407
1450
  bindingStore;
1408
- topK;
1451
+ focusedTopK;
1452
+ broadTopK;
1409
1453
  maxTokens;
1410
1454
  targetRetrievalMs;
1411
1455
  hardRetrievalTimeoutMs;
1412
1456
  recentWorkLimit;
1413
1457
  baseContext;
1414
1458
  clientName;
1459
+ minFocusedResults;
1460
+ minFocusedTopScore;
1461
+ minProjectScore;
1462
+ minMemoryScore;
1463
+ rankWeights;
1464
+ sourceActivityOptions;
1415
1465
  bindings = null;
1416
1466
  touchedFiles = [];
1417
1467
  recentWork = [];
1468
+ recentSourceActivity = [];
1418
1469
  bufferedLowSalience = [];
1419
1470
  lastPreparedTurn = null;
1420
1471
  mergedCount = 0;
1421
1472
  droppedCount = 0;
1473
+ focusedPassHits = 0;
1474
+ fallbackTriggers = 0;
1475
+ floorDroppedCount = 0;
1476
+ injectedItemCount = 0;
1477
+ sourceScopedTurns = 0;
1478
+ broadScopedTurns = 0;
1479
+ totalTurns = 0;
1480
+ currentTurn = 0;
1481
+ lastTaskSummary = "";
1422
1482
  lastScope = {};
1423
1483
  async getBindings() {
1424
1484
  if (!this.bindings) {
@@ -1436,6 +1496,64 @@ var WhisperAgentRuntime = class {
1436
1496
  pushWorkEvent(event) {
1437
1497
  this.recentWork = [...this.recentWork, event].slice(-this.recentWorkLimit);
1438
1498
  }
1499
+ noteSourceActivity(sourceIds) {
1500
+ const now = Date.now();
1501
+ for (const sourceId of [...new Set((sourceIds || []).map((value) => String(value || "").trim()).filter(Boolean))]) {
1502
+ this.recentSourceActivity = [
1503
+ ...this.recentSourceActivity.filter((entry) => entry.sourceId !== sourceId),
1504
+ { sourceId, turn: this.currentTurn, at: now }
1505
+ ].slice(-24);
1506
+ }
1507
+ }
1508
+ refreshTaskSummary(taskSummary) {
1509
+ const next = normalizeSummary(taskSummary);
1510
+ if (!next) return;
1511
+ if (this.sourceActivityOptions.evictOnTaskSwitch && this.lastTaskSummary && this.lastTaskSummary !== next && jaccardOverlap(this.lastTaskSummary, next) < 0.6) {
1512
+ this.recentSourceActivity = [];
1513
+ }
1514
+ this.lastTaskSummary = next;
1515
+ }
1516
+ activeSourceIds() {
1517
+ const now = Date.now();
1518
+ const active = /* @__PURE__ */ new Map();
1519
+ const maxTurns = this.sourceActivityOptions.maxTurns;
1520
+ const maxIdleMs = this.sourceActivityOptions.maxIdleMs;
1521
+ const decayAfterTurns = this.sourceActivityOptions.decayAfterTurns;
1522
+ const decayAfterIdleMs = this.sourceActivityOptions.decayAfterIdleMs;
1523
+ const fresh = [];
1524
+ for (const entry of this.recentSourceActivity) {
1525
+ const turnDelta = this.currentTurn - entry.turn;
1526
+ const idleDelta = now - entry.at;
1527
+ if (turnDelta > maxTurns || idleDelta > maxIdleMs) continue;
1528
+ fresh.push(entry);
1529
+ let weight = 1;
1530
+ if (turnDelta > decayAfterTurns || idleDelta > decayAfterIdleMs) {
1531
+ weight = 0.5;
1532
+ }
1533
+ const current = active.get(entry.sourceId) || 0;
1534
+ active.set(entry.sourceId, Math.max(current, weight));
1535
+ }
1536
+ this.recentSourceActivity = fresh.slice(-24);
1537
+ return [...active.entries()].sort((left, right) => right[1] - left[1]).map(([sourceId]) => sourceId).slice(0, 4);
1538
+ }
1539
+ focusedScope(input) {
1540
+ const sourceIds = this.activeSourceIds();
1541
+ const fileHints = [...new Set([
1542
+ ...input.touchedFiles || [],
1543
+ ...this.touchedFiles,
1544
+ ...this.recentWork.flatMap((event) => event.filePaths || [])
1545
+ ].map((value) => String(value || "").trim()).filter(Boolean))].slice(-4);
1546
+ return {
1547
+ sourceIds,
1548
+ fileHints,
1549
+ clientName: this.clientName || void 0
1550
+ };
1551
+ }
1552
+ exactFileMetadataFilter(fileHints) {
1553
+ const exact = fileHints.find((value) => /[\\/]/.test(value));
1554
+ if (!exact) return void 0;
1555
+ return { filePath: exact };
1556
+ }
1439
1557
  makeTaskFrameQuery(input) {
1440
1558
  const task = compactWhitespace(input.taskSummary || "");
1441
1559
  const salient = this.recentWork.filter((event) => event.salience === "high").slice(-3).map((event) => `${event.kind}: ${event.summary}`);
@@ -1512,23 +1630,29 @@ var WhisperAgentRuntime = class {
1512
1630
  };
1513
1631
  }
1514
1632
  }
1515
- contextItems(result, sourceQuery) {
1633
+ contextItems(result, sourceQuery, pass) {
1634
+ const sourceScope = result.meta?.source_scope;
1635
+ if (sourceScope?.mode === "auto" || sourceScope?.mode === "explicit") {
1636
+ this.noteSourceActivity(sourceScope.source_ids || []);
1637
+ }
1516
1638
  return (result.results || []).map((item) => ({
1517
1639
  id: item.id,
1518
1640
  content: item.content,
1519
1641
  type: "project",
1520
1642
  score: item.score ?? 0,
1521
1643
  sourceQuery,
1644
+ pass,
1522
1645
  metadata: item.metadata || {}
1523
1646
  }));
1524
1647
  }
1525
- memoryItems(result, sourceQuery) {
1648
+ memoryItems(result, sourceQuery, pass) {
1526
1649
  return (result.results || []).map((item, index) => ({
1527
1650
  id: item.memory?.id || item.chunk?.id || `${sourceQuery}_memory_${index}`,
1528
1651
  content: item.chunk?.content || item.memory?.content || "",
1529
1652
  type: "memory",
1530
1653
  score: item.similarity ?? 0,
1531
1654
  sourceQuery,
1655
+ pass,
1532
1656
  metadata: {
1533
1657
  ...item.chunk?.metadata || {},
1534
1658
  ...item.memory?.temporal || {},
@@ -1536,22 +1660,99 @@ var WhisperAgentRuntime = class {
1536
1660
  }
1537
1661
  })).filter((item) => item.content);
1538
1662
  }
1539
- rerank(items) {
1663
+ stableItemKey(item) {
1664
+ const metadata = item.metadata || {};
1665
+ const sourceId = String(metadata.source_id || "");
1666
+ const documentId = String(metadata.document_id || metadata.documentId || "");
1667
+ const chunkId = String(metadata.chunk_id || metadata.chunkId || item.id || "");
1668
+ return stableHash(`${sourceId}|${documentId}|${chunkId}|${item.content.slice(0, 256)}`);
1669
+ }
1670
+ metadataStrings(item) {
1671
+ const metadata = item.metadata || {};
1672
+ return [
1673
+ metadata.filePath,
1674
+ metadata.file_path,
1675
+ metadata.path,
1676
+ metadata.section_path,
1677
+ metadata.parent_section_path,
1678
+ metadata.web_url,
1679
+ metadata.url
1680
+ ].map((value) => String(value || "").toLowerCase()).filter(Boolean);
1681
+ }
1682
+ hasSourceMatch(item, scope) {
1683
+ const sourceId = String(item.metadata?.source_id || "");
1684
+ return Boolean(sourceId && scope.sourceIds.includes(sourceId));
1685
+ }
1686
+ hasFileMatch(item, scope) {
1687
+ if (scope.fileHints.length === 0) return false;
1688
+ const metadata = this.metadataStrings(item);
1689
+ const lowerHints = scope.fileHints.map((hint) => hint.toLowerCase());
1690
+ return lowerHints.some((hint) => {
1691
+ const base = pathBase(hint).toLowerCase();
1692
+ return metadata.some((value) => value.includes(hint) || value.endsWith(base));
1693
+ });
1694
+ }
1695
+ hasClientMatch(item, scope) {
1696
+ const itemClient = String(item.metadata?.client_name || "");
1697
+ return Boolean(scope.clientName && itemClient && itemClient === scope.clientName);
1698
+ }
1699
+ salienceAdjustment(item) {
1700
+ const salience = item.metadata?.salience;
1701
+ if (salience === "high") return this.rankWeights.highSalienceBonus;
1702
+ if (salience === "medium") return this.rankWeights.mediumSalienceBonus;
1703
+ if (salience === "low") return this.rankWeights.lowSaliencePenalty;
1704
+ return 0;
1705
+ }
1706
+ narrowFocusedMemories(items, scope) {
1707
+ const hasSignals = scope.sourceIds.length > 0 || scope.fileHints.length > 0 || Boolean(scope.clientName);
1708
+ if (!hasSignals) return items;
1709
+ const narrowed = items.filter((item) => {
1710
+ const matchesClient = this.hasClientMatch(item, scope);
1711
+ const matchesFile = this.hasFileMatch(item, scope);
1712
+ const matchesSource = this.hasSourceMatch(item, scope);
1713
+ const salience = item.metadata?.salience;
1714
+ if (scope.clientName && item.metadata?.client_name && !matchesClient) {
1715
+ return false;
1716
+ }
1717
+ if (salience === "low" && !matchesFile && !matchesSource) {
1718
+ return false;
1719
+ }
1720
+ return matchesClient || matchesFile || matchesSource || !scope.clientName;
1721
+ });
1722
+ return narrowed.length > 0 ? narrowed : items;
1723
+ }
1724
+ applyRelevanceFloor(items) {
1725
+ const filtered = items.filter(
1726
+ (item) => item.type === "project" ? item.score >= this.minProjectScore : item.score >= this.minMemoryScore
1727
+ );
1728
+ return { items: filtered, dropped: Math.max(0, items.length - filtered.length) };
1729
+ }
1730
+ rerank(items, scope) {
1540
1731
  const deduped = /* @__PURE__ */ new Map();
1541
1732
  for (const item of items) {
1542
- const key = `${item.id}:${item.content.toLowerCase()}`;
1733
+ const key = this.stableItemKey(item);
1543
1734
  const recency = extractTimestamp(item.metadata) > 0 ? 0.04 : 0;
1544
1735
  const queryBonus = item.sourceQuery === "primary" ? 0.08 : item.sourceQuery === "task_frame" ? 0.04 : 0.03;
1736
+ const sourceMatch = this.hasSourceMatch(item, scope);
1737
+ const fileMatch = this.hasFileMatch(item, scope);
1738
+ const clientMatch = this.hasClientMatch(item, scope);
1739
+ const broadPenalty = item.pass === "broad" && !sourceMatch && !fileMatch && !clientMatch ? this.rankWeights.staleBroadPenalty : 0;
1740
+ const clientPenalty = scope.clientName && item.metadata?.client_name && !clientMatch ? this.rankWeights.unrelatedClientPenalty : 0;
1545
1741
  const next = {
1546
1742
  ...item,
1547
- score: item.score + queryBonus + salienceBoost(item.metadata) + recency
1743
+ score: clamp01(
1744
+ item.score + queryBonus + recency + (item.pass === "focused" ? this.rankWeights.focusedPassBonus : 0) + (sourceMatch ? this.rankWeights.sourceMatchBonus : 0) + (fileMatch ? this.rankWeights.touchedFileBonus : 0) + (clientMatch ? this.rankWeights.clientMatchBonus : 0) + this.salienceAdjustment(item) + broadPenalty + clientPenalty
1745
+ )
1548
1746
  };
1549
1747
  const existing = deduped.get(key);
1550
1748
  if (!existing || next.score > existing.score) {
1551
1749
  deduped.set(key, next);
1552
1750
  }
1553
1751
  }
1554
- return [...deduped.values()].sort((left, right) => right.score - left.score);
1752
+ return {
1753
+ items: [...deduped.values()].sort((left, right) => right.score - left.score),
1754
+ dedupedCount: Math.max(0, items.length - deduped.size)
1755
+ };
1555
1756
  }
1556
1757
  buildContext(items) {
1557
1758
  const maxChars = this.maxTokens * 4;
@@ -1590,7 +1791,7 @@ ${lines.join("\n")}`;
1590
1791
  this.runBranch("project_rules", () => this.args.adapter.query({
1591
1792
  project: scope.project,
1592
1793
  query: "project rules instructions constraints conventions open threads",
1593
- top_k: this.topK,
1794
+ top_k: this.focusedTopK,
1594
1795
  include_memories: false,
1595
1796
  user_id: scope.userId,
1596
1797
  session_id: scope.sessionId,
@@ -1608,7 +1809,7 @@ ${lines.join("\n")}`;
1608
1809
  continue;
1609
1810
  }
1610
1811
  if (branch.name === "project_rules") {
1611
- items.push(...this.contextItems(branch.value, "bootstrap"));
1812
+ items.push(...this.contextItems(branch.value, "bootstrap", "bootstrap"));
1612
1813
  continue;
1613
1814
  }
1614
1815
  const records = branch.value.memories || [];
@@ -1618,10 +1819,12 @@ ${lines.join("\n")}`;
1618
1819
  type: "memory",
1619
1820
  score: 0.4,
1620
1821
  sourceQuery: "bootstrap",
1822
+ pass: "bootstrap",
1621
1823
  metadata: memory
1622
1824
  })).filter((item) => item.content));
1623
1825
  }
1624
- const ranked = this.rerank(items).slice(0, this.topK * 2);
1826
+ const reranked = this.rerank(items, { sourceIds: [], fileHints: [], clientName: this.clientName });
1827
+ const ranked = reranked.items.slice(0, this.broadTopK * 2);
1625
1828
  const prepared = {
1626
1829
  scope,
1627
1830
  retrieval: {
@@ -1633,7 +1836,14 @@ ${lines.join("\n")}`;
1633
1836
  durationMs: Date.now() - startedAt,
1634
1837
  targetBudgetMs: this.targetRetrievalMs,
1635
1838
  hardTimeoutMs: this.hardRetrievalTimeoutMs,
1636
- branchStatus
1839
+ branchStatus,
1840
+ focusedScopeApplied: false,
1841
+ focusedSourceIds: [],
1842
+ focusedFileHints: [],
1843
+ clientScoped: false,
1844
+ fallbackUsed: false,
1845
+ droppedBelowFloor: 0,
1846
+ dedupedCount: reranked.dedupedCount
1637
1847
  },
1638
1848
  context: this.buildContext(ranked),
1639
1849
  items: ranked
@@ -1642,100 +1852,195 @@ ${lines.join("\n")}`;
1642
1852
  return prepared;
1643
1853
  }
1644
1854
  async beforeTurn(input, context = {}) {
1855
+ this.currentTurn += 1;
1645
1856
  this.pushTouchedFiles(input.touchedFiles);
1857
+ this.refreshTaskSummary(input.taskSummary);
1646
1858
  const { scope, warning } = await this.resolveScope(context);
1647
1859
  const primaryQuery = compactWhitespace(input.userMessage);
1648
1860
  const taskFrameQuery = this.makeTaskFrameQuery(input);
1861
+ const focusedScope = this.focusedScope(input);
1862
+ const focusedMetadataFilter = this.exactFileMetadataFilter(focusedScope.fileHints);
1863
+ const focusedScopeApplied = focusedScope.sourceIds.length > 0 || focusedScope.fileHints.length > 0 || Boolean(focusedScope.clientName);
1649
1864
  const warnings = warning ? [warning] : [];
1650
1865
  const startedAt = Date.now();
1651
- const branches = await Promise.all([
1652
- this.runBranch("context_primary", () => this.args.adapter.query({
1866
+ const branchStatus = {};
1867
+ const collectFromBranches = (branches, pass) => {
1868
+ const collected = [];
1869
+ let okCount = 0;
1870
+ for (const branch of branches) {
1871
+ branchStatus[branch.name] = branch.status;
1872
+ if (branch.status !== "ok") {
1873
+ if (branch.status !== "skipped" && branch.reason) warnings.push(`${branch.name}:${branch.reason}`);
1874
+ continue;
1875
+ }
1876
+ okCount += 1;
1877
+ if (branch.name.startsWith("context")) {
1878
+ collected.push(...this.contextItems(
1879
+ branch.value,
1880
+ branch.name.includes("task_frame") ? "task_frame" : "primary",
1881
+ pass
1882
+ ));
1883
+ } else {
1884
+ const memoryItems = this.memoryItems(
1885
+ branch.value,
1886
+ branch.name.includes("task_frame") ? "task_frame" : "primary",
1887
+ pass
1888
+ );
1889
+ collected.push(...pass === "focused" ? this.narrowFocusedMemories(memoryItems, focusedScope) : memoryItems);
1890
+ }
1891
+ }
1892
+ return { collected, okCount };
1893
+ };
1894
+ const focusedBranches = await Promise.all([
1895
+ this.runBranch("context_primary_focused", () => this.args.adapter.query({
1653
1896
  project: scope.project,
1654
1897
  query: primaryQuery,
1655
- top_k: this.topK,
1898
+ top_k: this.focusedTopK,
1656
1899
  include_memories: false,
1657
1900
  user_id: scope.userId,
1658
1901
  session_id: scope.sessionId,
1902
+ source_ids: focusedScope.sourceIds.length > 0 ? focusedScope.sourceIds : void 0,
1903
+ metadata_filter: focusedMetadataFilter,
1659
1904
  max_tokens: this.maxTokens,
1660
1905
  compress: true,
1661
1906
  compression_strategy: "adaptive"
1662
1907
  })),
1663
- this.runBranch("memory_primary", () => this.args.adapter.searchMemories({
1908
+ this.runBranch("memory_primary_focused", () => this.args.adapter.searchMemories({
1664
1909
  project: scope.project,
1665
1910
  query: primaryQuery,
1666
1911
  user_id: scope.userId,
1667
1912
  session_id: scope.sessionId,
1668
- top_k: this.topK,
1913
+ top_k: this.focusedTopK,
1669
1914
  include_pending: true,
1670
1915
  profile: "balanced"
1671
1916
  })),
1672
- taskFrameQuery ? this.runBranch("context_task_frame", () => this.args.adapter.query({
1917
+ taskFrameQuery ? this.runBranch("context_task_frame_focused", () => this.args.adapter.query({
1673
1918
  project: scope.project,
1674
1919
  query: taskFrameQuery,
1675
- top_k: this.topK,
1920
+ top_k: this.focusedTopK,
1676
1921
  include_memories: false,
1677
1922
  user_id: scope.userId,
1678
1923
  session_id: scope.sessionId,
1924
+ source_ids: focusedScope.sourceIds.length > 0 ? focusedScope.sourceIds : void 0,
1925
+ metadata_filter: focusedMetadataFilter,
1679
1926
  max_tokens: this.maxTokens,
1680
1927
  compress: true,
1681
1928
  compression_strategy: "adaptive"
1682
- })) : Promise.resolve({
1683
- name: "context_task_frame",
1684
- status: "skipped",
1685
- durationMs: 0
1686
- }),
1687
- taskFrameQuery ? this.runBranch("memory_task_frame", () => this.args.adapter.searchMemories({
1929
+ })) : Promise.resolve({ name: "context_task_frame_focused", status: "skipped", durationMs: 0 }),
1930
+ taskFrameQuery ? this.runBranch("memory_task_frame_focused", () => this.args.adapter.searchMemories({
1688
1931
  project: scope.project,
1689
1932
  query: taskFrameQuery,
1690
1933
  user_id: scope.userId,
1691
1934
  session_id: scope.sessionId,
1692
- top_k: this.topK,
1935
+ top_k: this.focusedTopK,
1693
1936
  include_pending: true,
1694
1937
  profile: "balanced"
1695
- })) : Promise.resolve({
1696
- name: "memory_task_frame",
1697
- status: "skipped",
1698
- durationMs: 0
1699
- })
1938
+ })) : Promise.resolve({ name: "memory_task_frame_focused", status: "skipped", durationMs: 0 })
1700
1939
  ]);
1701
- const branchStatus = {};
1702
- const collected = [];
1703
- let okCount = 0;
1704
- for (const branch of branches) {
1940
+ const focusedCollected = collectFromBranches(focusedBranches, "focused");
1941
+ const focusedRanked = this.rerank(focusedCollected.collected, focusedScope);
1942
+ const focusedFloored = this.applyRelevanceFloor(focusedRanked.items);
1943
+ let allCollected = [...focusedFloored.items];
1944
+ let totalOkCount = focusedCollected.okCount;
1945
+ let dedupedCount = focusedRanked.dedupedCount;
1946
+ let droppedBelowFloor = focusedFloored.dropped;
1947
+ const focusedTopScore = focusedFloored.items[0]?.score ?? 0;
1948
+ const fallbackUsed = focusedFloored.items.length < this.minFocusedResults || focusedTopScore < this.minFocusedTopScore;
1949
+ if (focusedScopeApplied) {
1950
+ this.sourceScopedTurns += 1;
1951
+ }
1952
+ if (!fallbackUsed) {
1953
+ this.focusedPassHits += 1;
1954
+ }
1955
+ const broadBranches = fallbackUsed ? await Promise.all([
1956
+ this.runBranch("context_primary_broad", () => this.args.adapter.query({
1957
+ project: scope.project,
1958
+ query: primaryQuery,
1959
+ top_k: this.broadTopK,
1960
+ include_memories: false,
1961
+ user_id: scope.userId,
1962
+ session_id: scope.sessionId,
1963
+ max_tokens: this.maxTokens,
1964
+ compress: true,
1965
+ compression_strategy: "adaptive"
1966
+ })),
1967
+ this.runBranch("memory_primary_broad", () => this.args.adapter.searchMemories({
1968
+ project: scope.project,
1969
+ query: primaryQuery,
1970
+ user_id: scope.userId,
1971
+ session_id: scope.sessionId,
1972
+ top_k: this.broadTopK,
1973
+ include_pending: true,
1974
+ profile: "balanced"
1975
+ })),
1976
+ taskFrameQuery ? this.runBranch("context_task_frame_broad", () => this.args.adapter.query({
1977
+ project: scope.project,
1978
+ query: taskFrameQuery,
1979
+ top_k: this.broadTopK,
1980
+ include_memories: false,
1981
+ user_id: scope.userId,
1982
+ session_id: scope.sessionId,
1983
+ max_tokens: this.maxTokens,
1984
+ compress: true,
1985
+ compression_strategy: "adaptive"
1986
+ })) : Promise.resolve({ name: "context_task_frame_broad", status: "skipped", durationMs: 0 }),
1987
+ taskFrameQuery ? this.runBranch("memory_task_frame_broad", () => this.args.adapter.searchMemories({
1988
+ project: scope.project,
1989
+ query: taskFrameQuery,
1990
+ user_id: scope.userId,
1991
+ session_id: scope.sessionId,
1992
+ top_k: this.broadTopK,
1993
+ include_pending: true,
1994
+ profile: "balanced"
1995
+ })) : Promise.resolve({ name: "memory_task_frame_broad", status: "skipped", durationMs: 0 })
1996
+ ]) : [
1997
+ { name: "context_primary_broad", status: "skipped", durationMs: 0 },
1998
+ { name: "memory_primary_broad", status: "skipped", durationMs: 0 },
1999
+ { name: "context_task_frame_broad", status: "skipped", durationMs: 0 },
2000
+ { name: "memory_task_frame_broad", status: "skipped", durationMs: 0 }
2001
+ ];
2002
+ const broadCollected = collectFromBranches(broadBranches, "broad");
2003
+ totalOkCount += broadCollected.okCount;
2004
+ if (fallbackUsed) {
2005
+ this.fallbackTriggers += 1;
2006
+ this.broadScopedTurns += 1;
2007
+ allCollected = [...allCollected, ...broadCollected.collected];
2008
+ }
2009
+ const ranked = this.rerank(allCollected, focusedScope);
2010
+ dedupedCount += ranked.dedupedCount;
2011
+ const floored = this.applyRelevanceFloor(ranked.items);
2012
+ droppedBelowFloor += floored.dropped;
2013
+ this.floorDroppedCount += droppedBelowFloor;
2014
+ this.droppedCount += droppedBelowFloor;
2015
+ const finalItems = floored.items.slice(0, this.broadTopK);
2016
+ this.injectedItemCount += finalItems.length;
2017
+ this.totalTurns += 1;
2018
+ const executedBranches = [...focusedBranches, ...broadBranches].filter((branch) => branch.status !== "skipped");
2019
+ for (const branch of [...focusedBranches, ...broadBranches]) {
1705
2020
  branchStatus[branch.name] = branch.status;
1706
- if (branch.status !== "ok") {
1707
- if (branch.status !== "skipped" && branch.reason) warnings.push(`${branch.name}:${branch.reason}`);
1708
- continue;
1709
- }
1710
- okCount += 1;
1711
- if (branch.name.startsWith("context")) {
1712
- collected.push(...this.contextItems(
1713
- branch.value,
1714
- branch.name.includes("task_frame") ? "task_frame" : "primary"
1715
- ));
1716
- } else {
1717
- collected.push(...this.memoryItems(
1718
- branch.value,
1719
- branch.name.includes("task_frame") ? "task_frame" : "primary"
1720
- ));
1721
- }
1722
2021
  }
1723
- const ranked = this.rerank(collected).slice(0, this.topK * 2);
1724
2022
  const prepared = {
1725
2023
  scope,
1726
2024
  retrieval: {
1727
2025
  primaryQuery,
1728
2026
  taskFrameQuery,
1729
2027
  warnings,
1730
- degraded: okCount < branches.filter((branch) => branch.status !== "skipped").length,
1731
- degradedReason: okCount === 0 ? "all_retrieval_failed" : warnings.length > 0 ? "partial_retrieval_failed" : void 0,
2028
+ degraded: totalOkCount < executedBranches.length,
2029
+ degradedReason: totalOkCount === 0 ? "all_retrieval_failed" : warnings.length > 0 ? "partial_retrieval_failed" : void 0,
1732
2030
  durationMs: Date.now() - startedAt,
1733
2031
  targetBudgetMs: this.targetRetrievalMs,
1734
2032
  hardTimeoutMs: this.hardRetrievalTimeoutMs,
1735
- branchStatus
2033
+ branchStatus,
2034
+ focusedScopeApplied,
2035
+ focusedSourceIds: focusedScope.sourceIds,
2036
+ focusedFileHints: focusedScope.fileHints.map((value) => pathBase(value)),
2037
+ clientScoped: Boolean(focusedScope.clientName),
2038
+ fallbackUsed,
2039
+ droppedBelowFloor,
2040
+ dedupedCount
1736
2041
  },
1737
- context: this.buildContext(ranked),
1738
- items: ranked
2042
+ context: this.buildContext(finalItems),
2043
+ items: finalItems
1739
2044
  };
1740
2045
  this.lastPreparedTurn = prepared.retrieval;
1741
2046
  return prepared;
@@ -1830,7 +2135,14 @@ ${lines.join("\n")}`;
1830
2135
  counters: {
1831
2136
  mergedCount: this.mergedCount,
1832
2137
  droppedCount: this.droppedCount,
1833
- bufferedLowSalience: this.bufferedLowSalience.length
2138
+ bufferedLowSalience: this.bufferedLowSalience.length,
2139
+ focusedPassHits: this.focusedPassHits,
2140
+ fallbackTriggers: this.fallbackTriggers,
2141
+ floorDroppedCount: this.floorDroppedCount,
2142
+ injectedItemCount: this.injectedItemCount,
2143
+ sourceScopedTurns: this.sourceScopedTurns,
2144
+ broadScopedTurns: this.broadScopedTurns,
2145
+ totalTurns: this.totalTurns
1834
2146
  }
1835
2147
  };
1836
2148
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@usewhisper/sdk",
3
- "version": "3.6.0",
4
- "whisperContractVersion": "2026.03.09",
3
+ "version": "3.7.0",
4
+ "whisperContractVersion": "2026.03.10",
5
5
  "scripts": {
6
6
  "build": "tsup ../src/sdk/index.ts --format esm,cjs --dts --out-dir .",
7
7
  "prepublishOnly": "npm run build"