From 90cfef55c6f96f3a4c181e42723d86bdf31c8c89 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:39:03 +0000 Subject: [PATCH 1/2] perf(discover): avoid array allocations in ticker discovery hotpath Co-authored-by: toreleon <42534763+toreleon@users.noreply.github.com> --- .jules/bolt.md | 3 +++ src/tools/discover.ts | 48 ++++++++++++++++++++++++++++--------------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 9312d3f..c8c12ce 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -9,3 +9,6 @@ ## 2024-05-18 - Avoid unnecessary array allocations in frequent I/O paths **Learning:** `upsertSession` in `src/runtime/sessionStore.ts` is called very frequently (every time a session record is appended, which happens constantly during agent streaming). The original implementation used `.filter()` to remove the existing session and then pushed the updated one, resulting in significant garbage collection overhead and an O(N) array allocation on every single token/event stream chunk. Since this function is the bottleneck for chat interactivity, replacing `.filter()` with `.findIndex()` and in-place assignment yielded a > 2x speedup on session updates. **Action:** When updating arrays that back frequent disk I/O operations (like the session store), always prefer in-place mutation and sorting over immutable array recreation (`.filter()`, `.map()`) to minimize garbage collection pauses. +## 2025-02-12 - Prevent Array Allocations in Ticker Discovery Hot Path +**Learning:** In highly repetitive data aggregations over market ticks/bars, chaining `.map()` to extract primitive arrays (like `closes` or `vols`) and then chaining `.slice(-N).reduce(...)` generates significant garbage collection overhead and intermediate object allocation. +**Action:** When extracting sub-intervals from chronologically sorted time-series object arrays, access properties directly via indexing into the source array (`bars[i]!.volume`). Replace `.slice(-N).reduce(...)` aggregations with indexed `for` loops iterating from the end (`Math.max(0, len - N)`). Use optional chaining (`?.`) when directly plucking end elements to handle arrays safely when length may vary. diff --git a/src/tools/discover.ts b/src/tools/discover.ts index 413e476..0d02759 100644 --- a/src/tools/discover.ts +++ b/src/tools/discover.ts @@ -54,13 +54,14 @@ function pct(now: number, prev: number | undefined): number | null { return ((now - prev) / prev) * 100; } -function rsi14(closes: number[]): number | null { - if (closes.length < 15) return null; - const window = closes.slice(-15); +function rsi14(bars: readonly { close: number }[]): number | null { + const len = bars.length; + if (len < 15) return null; let gains = 0; let losses = 0; - for (let i = 1; i < window.length; i++) { - const d = window[i]! - window[i - 1]!; + const startIdx = Math.max(0, len - 15); + for (let i = startIdx + 1; i < len; i++) { + const d = bars[i]!.close - bars[i - 1]!.close; if (d > 0) gains += d; else losses -= d; } @@ -80,7 +81,8 @@ async function buildCandidate(ticker: string): Promise { 600, () => getStockOhlcv(ticker, "1D", from, to), ).catch(() => [] as Awaited>); - if (bars.length < 25) { + const len = bars.length; + if (len < 25) { return { ticker, metric: null, @@ -91,24 +93,36 @@ async function buildCandidate(ticker: string): Promise { vol_ratio: null, }; } - const closes = bars.map((b) => b.close); - const vols = bars.map((b) => b.volume); - const last = closes[closes.length - 1]!; - const prev1w = closes[closes.length - 6]; - const prev1m = closes[closes.length - 22]; - const recentVol = vols.slice(-5).reduce((a, b) => a + b, 0) / 5; - const priorVol = - vols.length >= 25 - ? vols.slice(-25, -5).reduce((a, b) => a + b, 0) / 20 - : null; + const last = bars[len - 1]!.close; + const prev1w = bars[len - 6]?.close; + const prev1m = bars[len - 22]?.close; + + let recentVolSum = 0; + for (let i = Math.max(0, len - 5); i < len; i++) { + recentVolSum += bars[i]!.volume; + } + const recentVol = recentVolSum / 5; + + let priorVol: number | null = null; + if (len >= 25) { + let priorVolSum = 0; + const startIdx = Math.max(0, len - 25); + const endIdx = len - 5; + for (let i = startIdx; i < endIdx; i++) { + priorVolSum += bars[i]!.volume; + } + priorVol = priorVolSum / 20; + } + const volRatio = priorVol != null && priorVol > 0 ? recentVol / priorVol : null; + return { ticker, metric: null, latest_close: last, ret_1w: pct(last, prev1w), ret_1m: pct(last, prev1m), - rsi14: rsi14(closes), + rsi14: rsi14(bars), vol_ratio: volRatio, }; } From 2401d3dd99010f4a36cb7d7b7e6e28e70af8a2f3 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:43:39 +0000 Subject: [PATCH 2/2] fix: resolve undici audit vulnerability by bumping version Co-authored-by: toreleon <42534763+toreleon@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 92516ea..b88a672 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "better-sqlite3" ], "overrides": { - "undici": ">=7.28.0 <8.0.0", + "undici": ">=7.29.0 <8.0.0", "ws": ">=8.21.0" } }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index abb401c..3cff6ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,7 @@ settings: excludeLinksFromLockfile: false overrides: - undici: '>=7.28.0 <8.0.0' + undici: '>=7.29.0 <8.0.0' ws: '>=8.21.0' importers: @@ -37,8 +37,8 @@ importers: specifier: ^3.1.0 version: 3.1.0 undici: - specifier: '>=7.28.0 <8.0.0' - version: 7.28.0 + specifier: '>=7.29.0 <8.0.0' + version: 7.29.1 yaml: specifier: ^2.6.0 version: 2.8.3 @@ -1520,8 +1520,8 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@7.28.0: - resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + undici@7.29.1: + resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==} engines: {node: '>=20.18.1'} universalify@0.1.2: @@ -2281,7 +2281,7 @@ snapshots: parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 - undici: 7.28.0 + undici: 7.29.1 whatwg-mimetype: 4.0.0 chownr@1.1.4: {} @@ -2986,7 +2986,7 @@ snapshots: undici-types@6.21.0: {} - undici@7.28.0: {} + undici@7.29.1: {} universalify@0.1.2: {}