@relayburn/sdk 1.5.0 → 1.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/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to `@relayburn/sdk`.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.7.0] - 2026-05-02
8
+
9
+ ### Added
10
+
11
+ - `hotspots({ patterns })` now also surfaces `tool-output-bloat`, `ghost-surface`, and `tool-call-pattern` findings (previously only the core `detectPatterns` set). Each side-channel detector loads its own inputs (Claude settings, tool-result events, on-disk surface) lazily based on the requested patterns.
12
+
7
13
  ## [1.5.0] - 2026-05-01
8
14
 
9
15
  ### Added
package/index.d.ts CHANGED
@@ -7,5 +7,19 @@ export declare function ingest(opts?: IngestOptions): Promise<unknown>
7
7
  export interface SummaryOptions { session?: string; project?: string; since?: string; ledgerHome?: string }
8
8
  export declare function summary(opts?: SummaryOptions): Promise<{ totalTokens: number; totalCost: number; byTool: Array<{tool:string;tokens:number;cost:number;count:number}>; byModel: Array<{model:string;tokens:number;cost:number}> }>
9
9
 
10
- export interface HotspotsOptions { session?: string; patterns?: string[]; ledgerHome?: string }
10
+ export interface HotspotsOptions {
11
+ session?: string;
12
+ /**
13
+ * Pattern kinds to detect. Supported kinds:
14
+ * - core (via `detectPatterns`): `retry-loop`, `failure-run`,
15
+ * `cancellation-run`, `compaction-loss`, `edit-revert`, `edit-heavy`,
16
+ * `skill-recall-dup`, `skill-pruning-protection`, `system-prompt-tax`
17
+ * - side-channel: `tool-output-bloat`, `ghost-surface`, `tool-call-pattern`
18
+ *
19
+ * When omitted or empty, returns the attribution result instead of a
20
+ * findings array.
21
+ */
22
+ patterns?: string[];
23
+ ledgerHome?: string;
24
+ }
11
25
  export declare function hotspots(opts?: HotspotsOptions): Promise<unknown>
package/index.js CHANGED
@@ -1,6 +1,21 @@
1
- import { queryAll, queryUserTurns } from '@relayburn/ledger';
2
- import { loadPricing, costForTurn, attributeHotspots, detectPatterns, findingsFromPatterns } from '@relayburn/analyze';
3
- import { ingestAll } from '@relayburn/cli';
1
+ import { queryAll, queryUserTurns, queryToolResultEvents } from '@relayburn/ledger';
2
+ import {
3
+ loadPricing,
4
+ costForTurn,
5
+ attributeHotspots,
6
+ detectPatterns,
7
+ findingsFromPatterns,
8
+ detectToolOutputBloat,
9
+ toolOutputBloatToFinding,
10
+ detectGhostSurface,
11
+ ghostSurfaceToFinding,
12
+ detectToolCallPatterns,
13
+ toolCallPatternToFinding,
14
+ loadClaudeSettings,
15
+ userClaudeSettingsPath,
16
+ projectClaudeSettingsPath,
17
+ } from '@relayburn/analyze';
18
+ import { ingestAll, buildGhostSurfaceInputs } from '@relayburn/cli';
4
19
 
5
20
  function withHome(home, fn) {
6
21
  const prev = process.env.RELAYBURN_HOME;
@@ -71,11 +86,63 @@ export async function hotspots(opts = {}) {
71
86
  return withHome(opts.ledgerHome, async () => {
72
87
  const turns = await queryAll({ sessionId: opts.session });
73
88
  const userTurns = await queryUserTurns({ sessionId: opts.session });
74
- const attribution = attributeHotspots({ turns, userTurns });
89
+ const pricing = await loadPricing();
90
+ const userTurnsBySession = bucketBySession(userTurns);
91
+ const attribution = attributeHotspots(turns, { pricing, userTurnsBySession });
75
92
 
76
93
  if (!opts.patterns || opts.patterns.length === 0) return attribution;
77
94
 
78
- const detected = detectPatterns({ turns, userTurns, hotspots: attribution });
79
- return findingsFromPatterns(detected).filter((f) => opts.patterns.includes(f.kind));
95
+ const wanted = new Set(opts.patterns);
96
+ const findings = [];
97
+
98
+ // Core patterns (retries, failures, edit-heavy, etc.) flow through
99
+ // detectPatterns + findingsFromPatterns; non-matching kinds are filtered.
100
+ const detected = detectPatterns(turns, { pricing, userTurnsBySession });
101
+ for (const f of findingsFromPatterns(detected)) {
102
+ if (wanted.has(f.kind)) findings.push(f);
103
+ }
104
+
105
+ // Side-channel detectors live outside detectPatterns. Each one reads its
106
+ // own slice of state, so we run them lazily based on `wanted`.
107
+
108
+ if (wanted.has('tool-output-bloat')) {
109
+ const settings = [];
110
+ const userLoaded = await loadClaudeSettings(userClaudeSettingsPath());
111
+ if (userLoaded) settings.push(userLoaded);
112
+ const projectLoaded = await loadClaudeSettings(projectClaudeSettingsPath());
113
+ if (projectLoaded) settings.push(projectLoaded);
114
+ const toolResultEvents = await queryToolResultEvents({ sessionId: opts.session });
115
+ const bloats = detectToolOutputBloat({
116
+ settings,
117
+ toolResultEvents,
118
+ userTurns,
119
+ turns,
120
+ pricing,
121
+ });
122
+ for (const b of bloats) findings.push(toolOutputBloatToFinding(b));
123
+ }
124
+
125
+ if (wanted.has('ghost-surface')) {
126
+ const ghostInputs = await buildGhostSurfaceInputs(turns, pricing);
127
+ const ghosts = await detectGhostSurface(ghostInputs);
128
+ for (const g of ghosts) findings.push(ghostSurfaceToFinding(g));
129
+ }
130
+
131
+ if (wanted.has('tool-call-pattern')) {
132
+ const patterns = detectToolCallPatterns(turns, { pricing });
133
+ for (const p of patterns) findings.push(toolCallPatternToFinding(p));
134
+ }
135
+
136
+ return findings;
80
137
  });
81
138
  }
139
+
140
+ function bucketBySession(userTurns) {
141
+ const out = new Map();
142
+ for (const ut of userTurns) {
143
+ const list = out.get(ut.sessionId);
144
+ if (list) list.push(ut);
145
+ else out.set(ut.sessionId, [ut]);
146
+ }
147
+ return out;
148
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relayburn/sdk",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Embeddable Relayburn SDK for in-process ingest, summary, and hotspots queries",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -16,9 +16,9 @@
16
16
  "node": ">=22"
17
17
  },
18
18
  "dependencies": {
19
- "@relayburn/analyze": "1.5.0",
20
- "@relayburn/ledger": "1.5.0",
21
- "@relayburn/cli": "1.5.0"
19
+ "@relayburn/analyze": "1.7.0",
20
+ "@relayburn/cli": "1.7.0",
21
+ "@relayburn/ledger": "1.7.0"
22
22
  },
23
23
  "repository": {
24
24
  "type": "git",