@bububuger/spanory 0.1.15 → 0.1.18

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.
@@ -1,10 +1,12 @@
1
1
  // @ts-nocheck
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { summarizeAgents, summarizeCache, summarizeCommands, summarizeMcp, summarizeSessions, summarizeTurnDiff, } from '../report/aggregate.js';
4
+ const DEFAULT_CONTEXT_WINDOW_TOKENS = 200000;
4
5
  function getMetricFromSessionRow(row, metric, refs = {}) {
5
6
  const cacheRow = refs.cacheBySessionId?.get(row.sessionId);
6
7
  const agentRow = refs.agentBySessionId?.get(row.sessionId);
7
8
  const turnDiffRows = refs.turnDiffBySessionId?.get(row.sessionId) ?? [];
9
+ const contextRow = refs.contextBySessionId?.get(row.sessionId);
8
10
  switch (metric) {
9
11
  case 'events':
10
12
  return row.events ?? 0;
@@ -26,10 +28,154 @@ function getMetricFromSessionRow(row, metric, refs = {}) {
26
28
  return agentRow?.agentTasks ?? 0;
27
29
  case 'diff.char_delta.max':
28
30
  return turnDiffRows.reduce((max, rowItem) => Math.max(max, Math.abs(Number(rowItem.charDelta ?? 0))), 0);
31
+ case 'context.unknown_delta_share.window5':
32
+ return contextRow?.unknownDeltaShareWindow5 ?? 0;
33
+ case 'context.unknown_top_streak':
34
+ return contextRow?.unknownTopStreak ?? 0;
35
+ case 'context.high_pollution_source_streak':
36
+ return contextRow?.highPollutionSourceStreak ?? 0;
37
+ case 'context.fill_ratio.max':
38
+ return contextRow?.maxFillRatio ?? 0;
39
+ case 'context.delta_ratio.max':
40
+ return contextRow?.maxDeltaRatio ?? 0;
41
+ case 'context.compact.count':
42
+ return contextRow?.compactCount ?? 0;
29
43
  default:
30
44
  return 0;
31
45
  }
32
46
  }
47
+ function parseJsonObject(value) {
48
+ if (typeof value !== 'string' || !value.trim())
49
+ return null;
50
+ try {
51
+ const parsed = JSON.parse(value);
52
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
53
+ return parsed;
54
+ }
55
+ catch {
56
+ // ignore parse errors
57
+ }
58
+ return null;
59
+ }
60
+ function parseJsonArray(value) {
61
+ if (typeof value !== 'string' || !value.trim())
62
+ return [];
63
+ try {
64
+ const parsed = JSON.parse(value);
65
+ if (Array.isArray(parsed))
66
+ return parsed;
67
+ }
68
+ catch {
69
+ // ignore parse errors
70
+ }
71
+ return [];
72
+ }
73
+ function summarizeContextForSession(events) {
74
+ const snapshots = events.filter((event) => event?.attributes?.['agentic.context.event_type'] === 'context_snapshot');
75
+ const attributions = events.filter((event) => event?.attributes?.['agentic.context.event_type'] === 'context_source_attribution');
76
+ const last5 = snapshots.slice(-5);
77
+ let unknownTokens = 0;
78
+ let totalTokens = 0;
79
+ for (const snapshot of last5) {
80
+ const composition = parseJsonObject(snapshot?.attributes?.['agentic.context.composition']);
81
+ if (!composition)
82
+ continue;
83
+ for (const [kind, raw] of Object.entries(composition)) {
84
+ const value = Number(raw);
85
+ if (!Number.isFinite(value) || value <= 0)
86
+ continue;
87
+ totalTokens += value;
88
+ if (kind === 'unknown')
89
+ unknownTokens += value;
90
+ }
91
+ }
92
+ const unknownDeltaShareWindow5 = totalTokens > 0 ? unknownTokens / totalTokens : 0;
93
+ let unknownTopStreak = 0;
94
+ let runningUnknown = 0;
95
+ for (const snapshot of snapshots) {
96
+ const topSources = parseJsonArray(snapshot?.attributes?.['agentic.context.top_sources']);
97
+ const top = String(topSources[0] ?? '').trim();
98
+ if (top === 'unknown') {
99
+ runningUnknown += 1;
100
+ if (runningUnknown > unknownTopStreak)
101
+ unknownTopStreak = runningUnknown;
102
+ }
103
+ else {
104
+ runningUnknown = 0;
105
+ }
106
+ }
107
+ const turnOrder = [];
108
+ const highByTurn = new Map();
109
+ for (const event of attributions) {
110
+ const attrs = event?.attributes ?? {};
111
+ const turnId = String(event?.turnId ?? '');
112
+ if (!turnId)
113
+ continue;
114
+ if (!highByTurn.has(turnId)) {
115
+ highByTurn.set(turnId, []);
116
+ turnOrder.push(turnId);
117
+ }
118
+ const sourceKind = String(attrs['agentic.context.source_kind'] ?? '').trim();
119
+ const score = Number(attrs['agentic.context.pollution_score']);
120
+ if (!sourceKind || !Number.isFinite(score))
121
+ continue;
122
+ if (score < 80)
123
+ continue;
124
+ highByTurn.get(turnId).push({ sourceKind, score });
125
+ }
126
+ let highPollutionSourceStreak = 0;
127
+ let runningSource = '';
128
+ let runningCount = 0;
129
+ for (const turnId of turnOrder) {
130
+ const items = highByTurn.get(turnId) ?? [];
131
+ if (!items.length) {
132
+ runningSource = '';
133
+ runningCount = 0;
134
+ continue;
135
+ }
136
+ items.sort((a, b) => b.score - a.score);
137
+ const topSource = items[0].sourceKind;
138
+ if (topSource === runningSource) {
139
+ runningCount += 1;
140
+ }
141
+ else {
142
+ runningSource = topSource;
143
+ runningCount = 1;
144
+ }
145
+ if (runningCount > highPollutionSourceStreak) {
146
+ highPollutionSourceStreak = runningCount;
147
+ }
148
+ }
149
+ let maxFillRatio = 0;
150
+ let maxDeltaRatio = 0;
151
+ let compactCount = 0;
152
+ for (const event of events) {
153
+ const attrs = event?.attributes ?? {};
154
+ if (String(attrs['agentic.context.event_type'] ?? '') === 'context_snapshot') {
155
+ const fillRatio = Number(attrs['agentic.context.fill_ratio']);
156
+ if (Number.isFinite(fillRatio) && fillRatio > maxFillRatio)
157
+ maxFillRatio = fillRatio;
158
+ const deltaTokens = Number(attrs['agentic.context.delta_tokens']);
159
+ if (Number.isFinite(deltaTokens) && deltaTokens > 0) {
160
+ const ratio = deltaTokens / DEFAULT_CONTEXT_WINDOW_TOKENS;
161
+ if (ratio > maxDeltaRatio)
162
+ maxDeltaRatio = ratio;
163
+ }
164
+ }
165
+ if (String(attrs['agentic.context.event_type'] ?? '') === 'context_boundary'
166
+ && String(attrs['agentic.context.boundary_kind'] ?? '') === 'compact_after') {
167
+ compactCount += 1;
168
+ }
169
+ }
170
+ return {
171
+ unknownDeltaShareWindow5,
172
+ unknownTopStreak,
173
+ highPollutionSourceStreak,
174
+ maxFillRatio,
175
+ maxDeltaRatio,
176
+ compactCount,
177
+ };
178
+ }
33
179
  function getMetricFromAgentRow(row, metric) {
34
180
  switch (metric) {
35
181
  case 'agentTasks':
@@ -68,12 +214,17 @@ function evaluateSessionRule(rule, sessions) {
68
214
  list.push(row);
69
215
  turnDiffBySessionId.set(key, list);
70
216
  }
217
+ const contextBySessionId = new Map(sessions.map((session) => [
218
+ session.context?.sessionId ?? session.events?.[0]?.sessionId,
219
+ summarizeContextForSession(session.events ?? []),
220
+ ]));
71
221
  const matched = rows
72
222
  .map((row) => {
73
223
  const value = getMetricFromSessionRow(row, rule.metric, {
74
224
  cacheBySessionId,
75
225
  agentBySessionId,
76
226
  turnDiffBySessionId,
227
+ contextBySessionId,
77
228
  });
78
229
  return { row, value };
79
230
  })
package/dist/env.d.ts CHANGED
@@ -1,2 +1,6 @@
1
+ export declare function resolveUserHome(): string;
2
+ export declare function resolveSpanoryHome(): string;
3
+ export declare function resolveSpanoryEnvPath(): string;
4
+ export declare function resolveLegacyUserEnvPath(): string;
1
5
  export declare function parseSimpleDotEnv(raw: any): {};
2
6
  export declare function loadUserEnv(): Promise<void>;
package/dist/env.js CHANGED
@@ -1,9 +1,23 @@
1
1
  // @ts-nocheck
2
2
  import path from 'node:path';
3
- import { readFile } from 'node:fs/promises';
4
- function resolveUserHome() {
3
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ export function resolveUserHome() {
5
5
  return process.env.HOME || process.env.USERPROFILE || '';
6
6
  }
7
+ export function resolveSpanoryHome() {
8
+ if (process.env.SPANORY_HOME)
9
+ return process.env.SPANORY_HOME;
10
+ const home = resolveUserHome();
11
+ return home ? path.join(home, '.spanory') : '';
12
+ }
13
+ export function resolveSpanoryEnvPath() {
14
+ const root = resolveSpanoryHome();
15
+ return root ? path.join(root, '.env') : '';
16
+ }
17
+ export function resolveLegacyUserEnvPath() {
18
+ const home = resolveUserHome();
19
+ return home ? path.join(home, '.env') : '';
20
+ }
7
21
  export function parseSimpleDotEnv(raw) {
8
22
  const out = {};
9
23
  for (const line of String(raw).split('\n')) {
@@ -36,19 +50,33 @@ export function parseSimpleDotEnv(raw) {
36
50
  return out;
37
51
  }
38
52
  export async function loadUserEnv() {
39
- const home = resolveUserHome();
40
- if (!home)
53
+ const spanoryHome = resolveSpanoryHome();
54
+ const envPath = resolveSpanoryEnvPath();
55
+ const legacyEnvPath = resolveLegacyUserEnvPath();
56
+ if (!spanoryHome || !envPath)
41
57
  return;
42
- const envPath = path.join(home, '.env');
43
58
  try {
44
- const raw = await readFile(envPath, 'utf-8');
45
- const parsed = parseSimpleDotEnv(raw);
46
- for (const [k, v] of Object.entries(parsed)) {
47
- if (process.env[k] === undefined)
48
- process.env[k] = v;
49
- }
59
+ await mkdir(spanoryHome, { recursive: true });
60
+ await writeFile(envPath, '', { flag: 'a' });
50
61
  }
51
62
  catch {
52
- // ignore missing ~/.env
63
+ // best effort only
64
+ }
65
+ const candidates = [envPath, legacyEnvPath].filter(Boolean);
66
+ for (const candidate of candidates) {
67
+ try {
68
+ const raw = await readFile(candidate, 'utf-8');
69
+ const parsed = parseSimpleDotEnv(raw);
70
+ if (Object.keys(parsed).length === 0)
71
+ continue;
72
+ for (const [k, v] of Object.entries(parsed)) {
73
+ if (process.env[k] === undefined)
74
+ process.env[k] = v;
75
+ }
76
+ return;
77
+ }
78
+ catch {
79
+ // try next candidate
80
+ }
53
81
  }
54
82
  }