@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.js CHANGED
@@ -1402,6 +1402,29 @@ ${lines.join("\n")}`;
1402
1402
  function compactWhitespace(value) {
1403
1403
  return value.replace(/\s+/g, " ").trim();
1404
1404
  }
1405
+ function normalizeSummary(value) {
1406
+ return compactWhitespace(String(value || "").toLowerCase());
1407
+ }
1408
+ function tokenize(value) {
1409
+ return normalizeSummary(value).split(/[^a-z0-9_./-]+/i).map((token) => token.trim()).filter(Boolean);
1410
+ }
1411
+ function jaccardOverlap(left, right) {
1412
+ const leftTokens = new Set(tokenize(left));
1413
+ const rightTokens = new Set(tokenize(right));
1414
+ if (leftTokens.size === 0 || rightTokens.size === 0) return 0;
1415
+ let intersection = 0;
1416
+ for (const token of leftTokens) {
1417
+ if (rightTokens.has(token)) intersection += 1;
1418
+ }
1419
+ const union = (/* @__PURE__ */ new Set([...leftTokens, ...rightTokens])).size;
1420
+ return union > 0 ? intersection / union : 0;
1421
+ }
1422
+ function clamp01(value) {
1423
+ if (!Number.isFinite(value)) return 0;
1424
+ if (value < 0) return 0;
1425
+ if (value > 1) return 1;
1426
+ return value;
1427
+ }
1405
1428
  function withTimeout(promise, timeoutMs) {
1406
1429
  return new Promise((resolve, reject) => {
1407
1430
  const timeout = setTimeout(() => {
@@ -1435,39 +1458,76 @@ function extractTimestamp(metadata) {
1435
1458
  }
1436
1459
  return 0;
1437
1460
  }
1438
- function salienceBoost(metadata) {
1439
- const value = metadata?.salience;
1440
- if (value === "high") return 0.12;
1441
- if (value === "medium") return 0.06;
1442
- return 0;
1443
- }
1461
+ var DEFAULT_RANK_WEIGHTS = {
1462
+ focusedPassBonus: 0.2,
1463
+ sourceMatchBonus: 0.18,
1464
+ touchedFileBonus: 0.12,
1465
+ clientMatchBonus: 0.1,
1466
+ highSalienceBonus: 0.12,
1467
+ mediumSalienceBonus: 0.06,
1468
+ staleBroadPenalty: -0.1,
1469
+ unrelatedClientPenalty: -0.18,
1470
+ lowSaliencePenalty: -0.12
1471
+ };
1472
+ var DEFAULT_SOURCE_ACTIVITY = {
1473
+ maxTurns: 10,
1474
+ maxIdleMs: 30 * 60 * 1e3,
1475
+ decayAfterTurns: 5,
1476
+ decayAfterIdleMs: 15 * 60 * 1e3,
1477
+ evictOnTaskSwitch: true
1478
+ };
1444
1479
  var WhisperAgentRuntime = class {
1445
1480
  constructor(args) {
1446
1481
  this.args = args;
1447
1482
  this.bindingStore = createBindingStore(args.options.bindingStorePath);
1448
- this.topK = args.options.topK ?? 6;
1483
+ const retrieval = args.options.retrieval || {};
1484
+ this.focusedTopK = retrieval.focusedTopK ?? args.options.topK ?? 6;
1485
+ this.broadTopK = retrieval.broadTopK ?? Math.max(args.options.topK ?? 6, 10);
1449
1486
  this.maxTokens = args.options.maxTokens ?? 4e3;
1450
1487
  this.targetRetrievalMs = args.options.targetRetrievalMs ?? 2500;
1451
1488
  this.hardRetrievalTimeoutMs = args.options.hardRetrievalTimeoutMs ?? 4e3;
1452
1489
  this.recentWorkLimit = args.options.recentWorkLimit ?? 40;
1453
1490
  this.baseContext = args.baseContext;
1454
1491
  this.clientName = args.baseContext.clientName || "whisper-agent-runtime";
1492
+ this.minFocusedResults = retrieval.minFocusedResults ?? 3;
1493
+ this.minFocusedTopScore = retrieval.minFocusedTopScore ?? 0.55;
1494
+ this.minProjectScore = retrieval.minProjectScore ?? 0.5;
1495
+ this.minMemoryScore = retrieval.minMemoryScore ?? 0.6;
1496
+ this.rankWeights = { ...DEFAULT_RANK_WEIGHTS, ...retrieval.rankWeights || {} };
1497
+ this.sourceActivityOptions = { ...DEFAULT_SOURCE_ACTIVITY, ...retrieval.sourceActivity || {} };
1455
1498
  }
1456
1499
  bindingStore;
1457
- topK;
1500
+ focusedTopK;
1501
+ broadTopK;
1458
1502
  maxTokens;
1459
1503
  targetRetrievalMs;
1460
1504
  hardRetrievalTimeoutMs;
1461
1505
  recentWorkLimit;
1462
1506
  baseContext;
1463
1507
  clientName;
1508
+ minFocusedResults;
1509
+ minFocusedTopScore;
1510
+ minProjectScore;
1511
+ minMemoryScore;
1512
+ rankWeights;
1513
+ sourceActivityOptions;
1464
1514
  bindings = null;
1465
1515
  touchedFiles = [];
1466
1516
  recentWork = [];
1517
+ recentSourceActivity = [];
1467
1518
  bufferedLowSalience = [];
1468
1519
  lastPreparedTurn = null;
1469
1520
  mergedCount = 0;
1470
1521
  droppedCount = 0;
1522
+ focusedPassHits = 0;
1523
+ fallbackTriggers = 0;
1524
+ floorDroppedCount = 0;
1525
+ injectedItemCount = 0;
1526
+ sourceScopedTurns = 0;
1527
+ broadScopedTurns = 0;
1528
+ totalTurns = 0;
1529
+ currentTurn = 0;
1530
+ lastTaskSummary = "";
1471
1531
  lastScope = {};
1472
1532
  async getBindings() {
1473
1533
  if (!this.bindings) {
@@ -1485,6 +1545,64 @@ var WhisperAgentRuntime = class {
1485
1545
  pushWorkEvent(event) {
1486
1546
  this.recentWork = [...this.recentWork, event].slice(-this.recentWorkLimit);
1487
1547
  }
1548
+ noteSourceActivity(sourceIds) {
1549
+ const now = Date.now();
1550
+ for (const sourceId of [...new Set((sourceIds || []).map((value) => String(value || "").trim()).filter(Boolean))]) {
1551
+ this.recentSourceActivity = [
1552
+ ...this.recentSourceActivity.filter((entry) => entry.sourceId !== sourceId),
1553
+ { sourceId, turn: this.currentTurn, at: now }
1554
+ ].slice(-24);
1555
+ }
1556
+ }
1557
+ refreshTaskSummary(taskSummary) {
1558
+ const next = normalizeSummary(taskSummary);
1559
+ if (!next) return;
1560
+ if (this.sourceActivityOptions.evictOnTaskSwitch && this.lastTaskSummary && this.lastTaskSummary !== next && jaccardOverlap(this.lastTaskSummary, next) < 0.6) {
1561
+ this.recentSourceActivity = [];
1562
+ }
1563
+ this.lastTaskSummary = next;
1564
+ }
1565
+ activeSourceIds() {
1566
+ const now = Date.now();
1567
+ const active = /* @__PURE__ */ new Map();
1568
+ const maxTurns = this.sourceActivityOptions.maxTurns;
1569
+ const maxIdleMs = this.sourceActivityOptions.maxIdleMs;
1570
+ const decayAfterTurns = this.sourceActivityOptions.decayAfterTurns;
1571
+ const decayAfterIdleMs = this.sourceActivityOptions.decayAfterIdleMs;
1572
+ const fresh = [];
1573
+ for (const entry of this.recentSourceActivity) {
1574
+ const turnDelta = this.currentTurn - entry.turn;
1575
+ const idleDelta = now - entry.at;
1576
+ if (turnDelta > maxTurns || idleDelta > maxIdleMs) continue;
1577
+ fresh.push(entry);
1578
+ let weight = 1;
1579
+ if (turnDelta > decayAfterTurns || idleDelta > decayAfterIdleMs) {
1580
+ weight = 0.5;
1581
+ }
1582
+ const current = active.get(entry.sourceId) || 0;
1583
+ active.set(entry.sourceId, Math.max(current, weight));
1584
+ }
1585
+ this.recentSourceActivity = fresh.slice(-24);
1586
+ return [...active.entries()].sort((left, right) => right[1] - left[1]).map(([sourceId]) => sourceId).slice(0, 4);
1587
+ }
1588
+ focusedScope(input) {
1589
+ const sourceIds = this.activeSourceIds();
1590
+ const fileHints = [...new Set([
1591
+ ...input.touchedFiles || [],
1592
+ ...this.touchedFiles,
1593
+ ...this.recentWork.flatMap((event) => event.filePaths || [])
1594
+ ].map((value) => String(value || "").trim()).filter(Boolean))].slice(-4);
1595
+ return {
1596
+ sourceIds,
1597
+ fileHints,
1598
+ clientName: this.clientName || void 0
1599
+ };
1600
+ }
1601
+ exactFileMetadataFilter(fileHints) {
1602
+ const exact = fileHints.find((value) => /[\\/]/.test(value));
1603
+ if (!exact) return void 0;
1604
+ return { filePath: exact };
1605
+ }
1488
1606
  makeTaskFrameQuery(input) {
1489
1607
  const task = compactWhitespace(input.taskSummary || "");
1490
1608
  const salient = this.recentWork.filter((event) => event.salience === "high").slice(-3).map((event) => `${event.kind}: ${event.summary}`);
@@ -1561,23 +1679,29 @@ var WhisperAgentRuntime = class {
1561
1679
  };
1562
1680
  }
1563
1681
  }
1564
- contextItems(result, sourceQuery) {
1682
+ contextItems(result, sourceQuery, pass) {
1683
+ const sourceScope = result.meta?.source_scope;
1684
+ if (sourceScope?.mode === "auto" || sourceScope?.mode === "explicit") {
1685
+ this.noteSourceActivity(sourceScope.source_ids || []);
1686
+ }
1565
1687
  return (result.results || []).map((item) => ({
1566
1688
  id: item.id,
1567
1689
  content: item.content,
1568
1690
  type: "project",
1569
1691
  score: item.score ?? 0,
1570
1692
  sourceQuery,
1693
+ pass,
1571
1694
  metadata: item.metadata || {}
1572
1695
  }));
1573
1696
  }
1574
- memoryItems(result, sourceQuery) {
1697
+ memoryItems(result, sourceQuery, pass) {
1575
1698
  return (result.results || []).map((item, index) => ({
1576
1699
  id: item.memory?.id || item.chunk?.id || `${sourceQuery}_memory_${index}`,
1577
1700
  content: item.chunk?.content || item.memory?.content || "",
1578
1701
  type: "memory",
1579
1702
  score: item.similarity ?? 0,
1580
1703
  sourceQuery,
1704
+ pass,
1581
1705
  metadata: {
1582
1706
  ...item.chunk?.metadata || {},
1583
1707
  ...item.memory?.temporal || {},
@@ -1585,22 +1709,99 @@ var WhisperAgentRuntime = class {
1585
1709
  }
1586
1710
  })).filter((item) => item.content);
1587
1711
  }
1588
- rerank(items) {
1712
+ stableItemKey(item) {
1713
+ const metadata = item.metadata || {};
1714
+ const sourceId = String(metadata.source_id || "");
1715
+ const documentId = String(metadata.document_id || metadata.documentId || "");
1716
+ const chunkId = String(metadata.chunk_id || metadata.chunkId || item.id || "");
1717
+ return stableHash(`${sourceId}|${documentId}|${chunkId}|${item.content.slice(0, 256)}`);
1718
+ }
1719
+ metadataStrings(item) {
1720
+ const metadata = item.metadata || {};
1721
+ return [
1722
+ metadata.filePath,
1723
+ metadata.file_path,
1724
+ metadata.path,
1725
+ metadata.section_path,
1726
+ metadata.parent_section_path,
1727
+ metadata.web_url,
1728
+ metadata.url
1729
+ ].map((value) => String(value || "").toLowerCase()).filter(Boolean);
1730
+ }
1731
+ hasSourceMatch(item, scope) {
1732
+ const sourceId = String(item.metadata?.source_id || "");
1733
+ return Boolean(sourceId && scope.sourceIds.includes(sourceId));
1734
+ }
1735
+ hasFileMatch(item, scope) {
1736
+ if (scope.fileHints.length === 0) return false;
1737
+ const metadata = this.metadataStrings(item);
1738
+ const lowerHints = scope.fileHints.map((hint) => hint.toLowerCase());
1739
+ return lowerHints.some((hint) => {
1740
+ const base = pathBase(hint).toLowerCase();
1741
+ return metadata.some((value) => value.includes(hint) || value.endsWith(base));
1742
+ });
1743
+ }
1744
+ hasClientMatch(item, scope) {
1745
+ const itemClient = String(item.metadata?.client_name || "");
1746
+ return Boolean(scope.clientName && itemClient && itemClient === scope.clientName);
1747
+ }
1748
+ salienceAdjustment(item) {
1749
+ const salience = item.metadata?.salience;
1750
+ if (salience === "high") return this.rankWeights.highSalienceBonus;
1751
+ if (salience === "medium") return this.rankWeights.mediumSalienceBonus;
1752
+ if (salience === "low") return this.rankWeights.lowSaliencePenalty;
1753
+ return 0;
1754
+ }
1755
+ narrowFocusedMemories(items, scope) {
1756
+ const hasSignals = scope.sourceIds.length > 0 || scope.fileHints.length > 0 || Boolean(scope.clientName);
1757
+ if (!hasSignals) return items;
1758
+ const narrowed = items.filter((item) => {
1759
+ const matchesClient = this.hasClientMatch(item, scope);
1760
+ const matchesFile = this.hasFileMatch(item, scope);
1761
+ const matchesSource = this.hasSourceMatch(item, scope);
1762
+ const salience = item.metadata?.salience;
1763
+ if (scope.clientName && item.metadata?.client_name && !matchesClient) {
1764
+ return false;
1765
+ }
1766
+ if (salience === "low" && !matchesFile && !matchesSource) {
1767
+ return false;
1768
+ }
1769
+ return matchesClient || matchesFile || matchesSource || !scope.clientName;
1770
+ });
1771
+ return narrowed.length > 0 ? narrowed : items;
1772
+ }
1773
+ applyRelevanceFloor(items) {
1774
+ const filtered = items.filter(
1775
+ (item) => item.type === "project" ? item.score >= this.minProjectScore : item.score >= this.minMemoryScore
1776
+ );
1777
+ return { items: filtered, dropped: Math.max(0, items.length - filtered.length) };
1778
+ }
1779
+ rerank(items, scope) {
1589
1780
  const deduped = /* @__PURE__ */ new Map();
1590
1781
  for (const item of items) {
1591
- const key = `${item.id}:${item.content.toLowerCase()}`;
1782
+ const key = this.stableItemKey(item);
1592
1783
  const recency = extractTimestamp(item.metadata) > 0 ? 0.04 : 0;
1593
1784
  const queryBonus = item.sourceQuery === "primary" ? 0.08 : item.sourceQuery === "task_frame" ? 0.04 : 0.03;
1785
+ const sourceMatch = this.hasSourceMatch(item, scope);
1786
+ const fileMatch = this.hasFileMatch(item, scope);
1787
+ const clientMatch = this.hasClientMatch(item, scope);
1788
+ const broadPenalty = item.pass === "broad" && !sourceMatch && !fileMatch && !clientMatch ? this.rankWeights.staleBroadPenalty : 0;
1789
+ const clientPenalty = scope.clientName && item.metadata?.client_name && !clientMatch ? this.rankWeights.unrelatedClientPenalty : 0;
1594
1790
  const next = {
1595
1791
  ...item,
1596
- score: item.score + queryBonus + salienceBoost(item.metadata) + recency
1792
+ score: clamp01(
1793
+ 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
1794
+ )
1597
1795
  };
1598
1796
  const existing = deduped.get(key);
1599
1797
  if (!existing || next.score > existing.score) {
1600
1798
  deduped.set(key, next);
1601
1799
  }
1602
1800
  }
1603
- return [...deduped.values()].sort((left, right) => right.score - left.score);
1801
+ return {
1802
+ items: [...deduped.values()].sort((left, right) => right.score - left.score),
1803
+ dedupedCount: Math.max(0, items.length - deduped.size)
1804
+ };
1604
1805
  }
1605
1806
  buildContext(items) {
1606
1807
  const maxChars = this.maxTokens * 4;
@@ -1639,7 +1840,7 @@ ${lines.join("\n")}`;
1639
1840
  this.runBranch("project_rules", () => this.args.adapter.query({
1640
1841
  project: scope.project,
1641
1842
  query: "project rules instructions constraints conventions open threads",
1642
- top_k: this.topK,
1843
+ top_k: this.focusedTopK,
1643
1844
  include_memories: false,
1644
1845
  user_id: scope.userId,
1645
1846
  session_id: scope.sessionId,
@@ -1657,7 +1858,7 @@ ${lines.join("\n")}`;
1657
1858
  continue;
1658
1859
  }
1659
1860
  if (branch.name === "project_rules") {
1660
- items.push(...this.contextItems(branch.value, "bootstrap"));
1861
+ items.push(...this.contextItems(branch.value, "bootstrap", "bootstrap"));
1661
1862
  continue;
1662
1863
  }
1663
1864
  const records = branch.value.memories || [];
@@ -1667,10 +1868,12 @@ ${lines.join("\n")}`;
1667
1868
  type: "memory",
1668
1869
  score: 0.4,
1669
1870
  sourceQuery: "bootstrap",
1871
+ pass: "bootstrap",
1670
1872
  metadata: memory
1671
1873
  })).filter((item) => item.content));
1672
1874
  }
1673
- const ranked = this.rerank(items).slice(0, this.topK * 2);
1875
+ const reranked = this.rerank(items, { sourceIds: [], fileHints: [], clientName: this.clientName });
1876
+ const ranked = reranked.items.slice(0, this.broadTopK * 2);
1674
1877
  const prepared = {
1675
1878
  scope,
1676
1879
  retrieval: {
@@ -1682,7 +1885,14 @@ ${lines.join("\n")}`;
1682
1885
  durationMs: Date.now() - startedAt,
1683
1886
  targetBudgetMs: this.targetRetrievalMs,
1684
1887
  hardTimeoutMs: this.hardRetrievalTimeoutMs,
1685
- branchStatus
1888
+ branchStatus,
1889
+ focusedScopeApplied: false,
1890
+ focusedSourceIds: [],
1891
+ focusedFileHints: [],
1892
+ clientScoped: false,
1893
+ fallbackUsed: false,
1894
+ droppedBelowFloor: 0,
1895
+ dedupedCount: reranked.dedupedCount
1686
1896
  },
1687
1897
  context: this.buildContext(ranked),
1688
1898
  items: ranked
@@ -1691,100 +1901,195 @@ ${lines.join("\n")}`;
1691
1901
  return prepared;
1692
1902
  }
1693
1903
  async beforeTurn(input, context = {}) {
1904
+ this.currentTurn += 1;
1694
1905
  this.pushTouchedFiles(input.touchedFiles);
1906
+ this.refreshTaskSummary(input.taskSummary);
1695
1907
  const { scope, warning } = await this.resolveScope(context);
1696
1908
  const primaryQuery = compactWhitespace(input.userMessage);
1697
1909
  const taskFrameQuery = this.makeTaskFrameQuery(input);
1910
+ const focusedScope = this.focusedScope(input);
1911
+ const focusedMetadataFilter = this.exactFileMetadataFilter(focusedScope.fileHints);
1912
+ const focusedScopeApplied = focusedScope.sourceIds.length > 0 || focusedScope.fileHints.length > 0 || Boolean(focusedScope.clientName);
1698
1913
  const warnings = warning ? [warning] : [];
1699
1914
  const startedAt = Date.now();
1700
- const branches = await Promise.all([
1701
- this.runBranch("context_primary", () => this.args.adapter.query({
1915
+ const branchStatus = {};
1916
+ const collectFromBranches = (branches, pass) => {
1917
+ const collected = [];
1918
+ let okCount = 0;
1919
+ for (const branch of branches) {
1920
+ branchStatus[branch.name] = branch.status;
1921
+ if (branch.status !== "ok") {
1922
+ if (branch.status !== "skipped" && branch.reason) warnings.push(`${branch.name}:${branch.reason}`);
1923
+ continue;
1924
+ }
1925
+ okCount += 1;
1926
+ if (branch.name.startsWith("context")) {
1927
+ collected.push(...this.contextItems(
1928
+ branch.value,
1929
+ branch.name.includes("task_frame") ? "task_frame" : "primary",
1930
+ pass
1931
+ ));
1932
+ } else {
1933
+ const memoryItems = this.memoryItems(
1934
+ branch.value,
1935
+ branch.name.includes("task_frame") ? "task_frame" : "primary",
1936
+ pass
1937
+ );
1938
+ collected.push(...pass === "focused" ? this.narrowFocusedMemories(memoryItems, focusedScope) : memoryItems);
1939
+ }
1940
+ }
1941
+ return { collected, okCount };
1942
+ };
1943
+ const focusedBranches = await Promise.all([
1944
+ this.runBranch("context_primary_focused", () => this.args.adapter.query({
1702
1945
  project: scope.project,
1703
1946
  query: primaryQuery,
1704
- top_k: this.topK,
1947
+ top_k: this.focusedTopK,
1705
1948
  include_memories: false,
1706
1949
  user_id: scope.userId,
1707
1950
  session_id: scope.sessionId,
1951
+ source_ids: focusedScope.sourceIds.length > 0 ? focusedScope.sourceIds : void 0,
1952
+ metadata_filter: focusedMetadataFilter,
1708
1953
  max_tokens: this.maxTokens,
1709
1954
  compress: true,
1710
1955
  compression_strategy: "adaptive"
1711
1956
  })),
1712
- this.runBranch("memory_primary", () => this.args.adapter.searchMemories({
1957
+ this.runBranch("memory_primary_focused", () => this.args.adapter.searchMemories({
1713
1958
  project: scope.project,
1714
1959
  query: primaryQuery,
1715
1960
  user_id: scope.userId,
1716
1961
  session_id: scope.sessionId,
1717
- top_k: this.topK,
1962
+ top_k: this.focusedTopK,
1718
1963
  include_pending: true,
1719
1964
  profile: "balanced"
1720
1965
  })),
1721
- taskFrameQuery ? this.runBranch("context_task_frame", () => this.args.adapter.query({
1966
+ taskFrameQuery ? this.runBranch("context_task_frame_focused", () => this.args.adapter.query({
1722
1967
  project: scope.project,
1723
1968
  query: taskFrameQuery,
1724
- top_k: this.topK,
1969
+ top_k: this.focusedTopK,
1725
1970
  include_memories: false,
1726
1971
  user_id: scope.userId,
1727
1972
  session_id: scope.sessionId,
1973
+ source_ids: focusedScope.sourceIds.length > 0 ? focusedScope.sourceIds : void 0,
1974
+ metadata_filter: focusedMetadataFilter,
1728
1975
  max_tokens: this.maxTokens,
1729
1976
  compress: true,
1730
1977
  compression_strategy: "adaptive"
1731
- })) : Promise.resolve({
1732
- name: "context_task_frame",
1733
- status: "skipped",
1734
- durationMs: 0
1735
- }),
1736
- taskFrameQuery ? this.runBranch("memory_task_frame", () => this.args.adapter.searchMemories({
1978
+ })) : Promise.resolve({ name: "context_task_frame_focused", status: "skipped", durationMs: 0 }),
1979
+ taskFrameQuery ? this.runBranch("memory_task_frame_focused", () => this.args.adapter.searchMemories({
1737
1980
  project: scope.project,
1738
1981
  query: taskFrameQuery,
1739
1982
  user_id: scope.userId,
1740
1983
  session_id: scope.sessionId,
1741
- top_k: this.topK,
1984
+ top_k: this.focusedTopK,
1742
1985
  include_pending: true,
1743
1986
  profile: "balanced"
1744
- })) : Promise.resolve({
1745
- name: "memory_task_frame",
1746
- status: "skipped",
1747
- durationMs: 0
1748
- })
1987
+ })) : Promise.resolve({ name: "memory_task_frame_focused", status: "skipped", durationMs: 0 })
1749
1988
  ]);
1750
- const branchStatus = {};
1751
- const collected = [];
1752
- let okCount = 0;
1753
- for (const branch of branches) {
1989
+ const focusedCollected = collectFromBranches(focusedBranches, "focused");
1990
+ const focusedRanked = this.rerank(focusedCollected.collected, focusedScope);
1991
+ const focusedFloored = this.applyRelevanceFloor(focusedRanked.items);
1992
+ let allCollected = [...focusedFloored.items];
1993
+ let totalOkCount = focusedCollected.okCount;
1994
+ let dedupedCount = focusedRanked.dedupedCount;
1995
+ let droppedBelowFloor = focusedFloored.dropped;
1996
+ const focusedTopScore = focusedFloored.items[0]?.score ?? 0;
1997
+ const fallbackUsed = focusedFloored.items.length < this.minFocusedResults || focusedTopScore < this.minFocusedTopScore;
1998
+ if (focusedScopeApplied) {
1999
+ this.sourceScopedTurns += 1;
2000
+ }
2001
+ if (!fallbackUsed) {
2002
+ this.focusedPassHits += 1;
2003
+ }
2004
+ const broadBranches = fallbackUsed ? await Promise.all([
2005
+ this.runBranch("context_primary_broad", () => this.args.adapter.query({
2006
+ project: scope.project,
2007
+ query: primaryQuery,
2008
+ top_k: this.broadTopK,
2009
+ include_memories: false,
2010
+ user_id: scope.userId,
2011
+ session_id: scope.sessionId,
2012
+ max_tokens: this.maxTokens,
2013
+ compress: true,
2014
+ compression_strategy: "adaptive"
2015
+ })),
2016
+ this.runBranch("memory_primary_broad", () => this.args.adapter.searchMemories({
2017
+ project: scope.project,
2018
+ query: primaryQuery,
2019
+ user_id: scope.userId,
2020
+ session_id: scope.sessionId,
2021
+ top_k: this.broadTopK,
2022
+ include_pending: true,
2023
+ profile: "balanced"
2024
+ })),
2025
+ taskFrameQuery ? this.runBranch("context_task_frame_broad", () => this.args.adapter.query({
2026
+ project: scope.project,
2027
+ query: taskFrameQuery,
2028
+ top_k: this.broadTopK,
2029
+ include_memories: false,
2030
+ user_id: scope.userId,
2031
+ session_id: scope.sessionId,
2032
+ max_tokens: this.maxTokens,
2033
+ compress: true,
2034
+ compression_strategy: "adaptive"
2035
+ })) : Promise.resolve({ name: "context_task_frame_broad", status: "skipped", durationMs: 0 }),
2036
+ taskFrameQuery ? this.runBranch("memory_task_frame_broad", () => this.args.adapter.searchMemories({
2037
+ project: scope.project,
2038
+ query: taskFrameQuery,
2039
+ user_id: scope.userId,
2040
+ session_id: scope.sessionId,
2041
+ top_k: this.broadTopK,
2042
+ include_pending: true,
2043
+ profile: "balanced"
2044
+ })) : Promise.resolve({ name: "memory_task_frame_broad", status: "skipped", durationMs: 0 })
2045
+ ]) : [
2046
+ { name: "context_primary_broad", status: "skipped", durationMs: 0 },
2047
+ { name: "memory_primary_broad", status: "skipped", durationMs: 0 },
2048
+ { name: "context_task_frame_broad", status: "skipped", durationMs: 0 },
2049
+ { name: "memory_task_frame_broad", status: "skipped", durationMs: 0 }
2050
+ ];
2051
+ const broadCollected = collectFromBranches(broadBranches, "broad");
2052
+ totalOkCount += broadCollected.okCount;
2053
+ if (fallbackUsed) {
2054
+ this.fallbackTriggers += 1;
2055
+ this.broadScopedTurns += 1;
2056
+ allCollected = [...allCollected, ...broadCollected.collected];
2057
+ }
2058
+ const ranked = this.rerank(allCollected, focusedScope);
2059
+ dedupedCount += ranked.dedupedCount;
2060
+ const floored = this.applyRelevanceFloor(ranked.items);
2061
+ droppedBelowFloor += floored.dropped;
2062
+ this.floorDroppedCount += droppedBelowFloor;
2063
+ this.droppedCount += droppedBelowFloor;
2064
+ const finalItems = floored.items.slice(0, this.broadTopK);
2065
+ this.injectedItemCount += finalItems.length;
2066
+ this.totalTurns += 1;
2067
+ const executedBranches = [...focusedBranches, ...broadBranches].filter((branch) => branch.status !== "skipped");
2068
+ for (const branch of [...focusedBranches, ...broadBranches]) {
1754
2069
  branchStatus[branch.name] = branch.status;
1755
- if (branch.status !== "ok") {
1756
- if (branch.status !== "skipped" && branch.reason) warnings.push(`${branch.name}:${branch.reason}`);
1757
- continue;
1758
- }
1759
- okCount += 1;
1760
- if (branch.name.startsWith("context")) {
1761
- collected.push(...this.contextItems(
1762
- branch.value,
1763
- branch.name.includes("task_frame") ? "task_frame" : "primary"
1764
- ));
1765
- } else {
1766
- collected.push(...this.memoryItems(
1767
- branch.value,
1768
- branch.name.includes("task_frame") ? "task_frame" : "primary"
1769
- ));
1770
- }
1771
2070
  }
1772
- const ranked = this.rerank(collected).slice(0, this.topK * 2);
1773
2071
  const prepared = {
1774
2072
  scope,
1775
2073
  retrieval: {
1776
2074
  primaryQuery,
1777
2075
  taskFrameQuery,
1778
2076
  warnings,
1779
- degraded: okCount < branches.filter((branch) => branch.status !== "skipped").length,
1780
- degradedReason: okCount === 0 ? "all_retrieval_failed" : warnings.length > 0 ? "partial_retrieval_failed" : void 0,
2077
+ degraded: totalOkCount < executedBranches.length,
2078
+ degradedReason: totalOkCount === 0 ? "all_retrieval_failed" : warnings.length > 0 ? "partial_retrieval_failed" : void 0,
1781
2079
  durationMs: Date.now() - startedAt,
1782
2080
  targetBudgetMs: this.targetRetrievalMs,
1783
2081
  hardTimeoutMs: this.hardRetrievalTimeoutMs,
1784
- branchStatus
2082
+ branchStatus,
2083
+ focusedScopeApplied,
2084
+ focusedSourceIds: focusedScope.sourceIds,
2085
+ focusedFileHints: focusedScope.fileHints.map((value) => pathBase(value)),
2086
+ clientScoped: Boolean(focusedScope.clientName),
2087
+ fallbackUsed,
2088
+ droppedBelowFloor,
2089
+ dedupedCount
1785
2090
  },
1786
- context: this.buildContext(ranked),
1787
- items: ranked
2091
+ context: this.buildContext(finalItems),
2092
+ items: finalItems
1788
2093
  };
1789
2094
  this.lastPreparedTurn = prepared.retrieval;
1790
2095
  return prepared;
@@ -1879,7 +2184,14 @@ ${lines.join("\n")}`;
1879
2184
  counters: {
1880
2185
  mergedCount: this.mergedCount,
1881
2186
  droppedCount: this.droppedCount,
1882
- bufferedLowSalience: this.bufferedLowSalience.length
2187
+ bufferedLowSalience: this.bufferedLowSalience.length,
2188
+ focusedPassHits: this.focusedPassHits,
2189
+ fallbackTriggers: this.fallbackTriggers,
2190
+ floorDroppedCount: this.floorDroppedCount,
2191
+ injectedItemCount: this.injectedItemCount,
2192
+ sourceScopedTurns: this.sourceScopedTurns,
2193
+ broadScopedTurns: this.broadScopedTurns,
2194
+ totalTurns: this.totalTurns
1883
2195
  }
1884
2196
  };
1885
2197
  }