sidekick-shared 0.18.3 → 0.18.5

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/README.md CHANGED
@@ -26,6 +26,7 @@ npm install sidekick-shared
26
26
  | **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
27
27
  | **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date) |
28
28
  | **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction |
29
+ | **Session Context** | Provider-neutral context evidence snapshots (`buildSessionContextSnapshot()`, `calculateSessionContextPressure()`, `createSessionContextProjector()`, `readSessionContextSnapshot()`): layered evidence sources, low/medium/high context pressure, and observed capabilities (tools, MCP servers, permission mode, rate limits) |
29
30
  | **Report** | Self-contained HTML session report generation |
30
31
  | **Credentials** | Claude Max OAuth credential reading from `~/.claude/.credentials.json` |
31
32
  | **Quota** | Claude Max subscription quota fetching (5-hour and 7-day windows) and Codex rate-limit extraction from event streams |
@@ -180,6 +181,26 @@ const tools = extractToolCalls(event); // ToolCall[] — assistant
180
181
  const toolFromEvent = extractToolCall(event); // ToolCall | null — top-level `tool_use` events
181
182
  ```
182
183
 
184
+ ### Project session context evidence
185
+
186
+ Build a provider-neutral snapshot of what an assistant has "seen" in a session — layered evidence sources, context pressure, and observed capabilities. Read it through any session provider, or build it directly from a canonical `SessionEvent[]`.
187
+
188
+ ```typescript
189
+ import { detectProvider, readSessionContextSnapshot } from 'sidekick-shared';
190
+
191
+ const provider = await detectProvider('/path/to/project');
192
+ if (provider) {
193
+ const snapshot = readSessionContextSnapshot(provider, '/path/to/session.jsonl');
194
+
195
+ console.log(snapshot.pressure); // 'low' | 'medium' | 'high'
196
+ console.log(snapshot.contextTokens, '/', snapshot.contextWindow);
197
+ console.log(snapshot.capabilities.tools, snapshot.capabilities.mcpServers);
198
+ console.log(snapshot.sources.length, 'evidence sources');
199
+ }
200
+ ```
201
+
202
+ Use `createSessionContextProjector()` for incremental updates as new events stream in, or `calculateSessionContextPressure(contextTokens, contextWindow)` for the pressure band alone.
203
+
183
204
  ### Format shared dashboard values
184
205
 
185
206
  ```typescript
@@ -157,25 +157,29 @@ class EventAggregator {
157
157
  if (event.message.model) {
158
158
  this.currentModel = event.message.model;
159
159
  }
160
- // 3. Skip synthetic token-count events for messageCount
160
+ // 3. Skip system/audit events and synthetic token-count events for messageCount
161
161
  const msgId = event.message.id ?? '';
162
- if (!msgId.startsWith('token-count-')) {
162
+ if (event.type !== 'system' && !msgId.startsWith('token-count-')) {
163
163
  this.messageCount++;
164
164
  }
165
165
  // 4. Latency tracking
166
- this.processLatency(event);
166
+ if (event.type !== 'system') {
167
+ this.processLatency(event);
168
+ }
167
169
  // 5. Token accumulation
168
170
  if (event.message.usage) {
169
171
  this.accumulateUsage(event.message.usage, event.timestamp, event.message.model);
170
172
  }
171
- // 6. Tool extraction from content blocks
172
- this.extractToolsFromContent(event);
173
- // 7. Task state
174
- this.extractTaskStateFromEvent(event);
175
- // 8. Subagent tracking
176
- this.extractSubagentFromEvent(event);
177
- // 9. Plan extraction — convert SessionEvent to a FollowEvent shape for PlanExtractor
178
- this.extractPlanFromSessionEvent(event);
173
+ if (event.type !== 'system') {
174
+ // 6. Tool extraction from content blocks
175
+ this.extractToolsFromContent(event);
176
+ // 7. Task state
177
+ this.extractTaskStateFromEvent(event);
178
+ // 8. Subagent tracking
179
+ this.extractSubagentFromEvent(event);
180
+ // 9. Plan extraction — convert SessionEvent to a FollowEvent shape for PlanExtractor
181
+ this.extractPlanFromSessionEvent(event);
182
+ }
179
183
  // 10. Context attribution
180
184
  this.attributeContextFromEvent(event);
181
185
  // 11. Permission mode tracking
@@ -1447,6 +1451,12 @@ class EventAggregator {
1447
1451
  this.contextAttribution.other += this.estimateTokens(text);
1448
1452
  }
1449
1453
  }
1454
+ else if (event.type === 'system') {
1455
+ const text = this.extractTextContent(event) || '';
1456
+ if (text) {
1457
+ this.contextAttribution.systemPrompt += this.estimateTokens(text);
1458
+ }
1459
+ }
1450
1460
  }
1451
1461
  // ═══════════════════════════════════════════════════════════════════════
1452
1462
  // Private: Context Attribution from FollowEvent
@@ -1591,6 +1601,11 @@ class EventAggregator {
1591
1601
  description = 'Context compacted';
1592
1602
  noiseLevel = 'system';
1593
1603
  break;
1604
+ case 'system':
1605
+ tlType = 'session_start';
1606
+ description = this.extractTextContent(event) ?? event.message.sourceLabel ?? 'System event';
1607
+ noiseLevel = 'system';
1608
+ break;
1594
1609
  default:
1595
1610
  tlType = 'session_start';
1596
1611
  description = 'Event';
package/dist/browser.d.ts CHANGED
@@ -10,5 +10,7 @@
10
10
  export { getModelContextWindowSize, DEFAULT_CONTEXT_WINDOW } from './modelContext';
11
11
  export { formatDurationMs, formatTokenCount } from './formatting';
12
12
  export type { FormatDurationMsOptions, FormatTokenCountOptions } from './formatting';
13
+ export { buildSessionContextSnapshot, calculateSessionContextPressure, createSessionContextProjector, } from './context/sessionContext';
14
+ export type { BuildSessionContextSnapshotOptions, SessionContextCapabilities, SessionContextLayerBreakdown, SessionContextPressure, SessionContextProjector, SessionContextSnapshot, SessionContextSource, SessionContextSourceType, } from './context/sessionContext';
13
15
  export { parseModelId, getModelPricing, getModelInfo, calculateCost, calculateCostWithPricing, calculateCostWithProvenance, mergeCostSources, shortModelName, getModelDisplayInfo, compareModelIds, sortModelIds, formatCost, } from './modelInfo';
14
16
  export type { ModelPricing, CostTokenUsage, CostSource, CostProvenanceInput, CostWithProvenance, ModelProvider, ParsedModelId, ModelInfo, ModelDisplayInfo, } from './modelInfo';
package/dist/browser.js CHANGED
@@ -9,13 +9,17 @@
9
9
  * For pricing hydration (LiteLLM catalog refresh), use `sidekick-shared/node`.
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.getModelPricing = exports.parseModelId = exports.formatTokenCount = exports.formatDurationMs = exports.DEFAULT_CONTEXT_WINDOW = exports.getModelContextWindowSize = void 0;
12
+ exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.getModelPricing = exports.parseModelId = exports.createSessionContextProjector = exports.calculateSessionContextPressure = exports.buildSessionContextSnapshot = exports.formatTokenCount = exports.formatDurationMs = exports.DEFAULT_CONTEXT_WINDOW = exports.getModelContextWindowSize = void 0;
13
13
  var modelContext_1 = require("./modelContext");
14
14
  Object.defineProperty(exports, "getModelContextWindowSize", { enumerable: true, get: function () { return modelContext_1.getModelContextWindowSize; } });
15
15
  Object.defineProperty(exports, "DEFAULT_CONTEXT_WINDOW", { enumerable: true, get: function () { return modelContext_1.DEFAULT_CONTEXT_WINDOW; } });
16
16
  var formatting_1 = require("./formatting");
17
17
  Object.defineProperty(exports, "formatDurationMs", { enumerable: true, get: function () { return formatting_1.formatDurationMs; } });
18
18
  Object.defineProperty(exports, "formatTokenCount", { enumerable: true, get: function () { return formatting_1.formatTokenCount; } });
19
+ var sessionContext_1 = require("./context/sessionContext");
20
+ Object.defineProperty(exports, "buildSessionContextSnapshot", { enumerable: true, get: function () { return sessionContext_1.buildSessionContextSnapshot; } });
21
+ Object.defineProperty(exports, "calculateSessionContextPressure", { enumerable: true, get: function () { return sessionContext_1.calculateSessionContextPressure; } });
22
+ Object.defineProperty(exports, "createSessionContextProjector", { enumerable: true, get: function () { return sessionContext_1.createSessionContextProjector; } });
19
23
  var modelInfo_1 = require("./modelInfo");
20
24
  Object.defineProperty(exports, "parseModelId", { enumerable: true, get: function () { return modelInfo_1.parseModelId; } });
21
25
  Object.defineProperty(exports, "getModelPricing", { enumerable: true, get: function () { return modelInfo_1.getModelPricing; } });
@@ -46,6 +46,36 @@ const codex_1 = require("./providers/codex");
46
46
  const DEFAULT_TAIL_BYTES = 2 * 1024 * 1024;
47
47
  const DEFAULT_MAX_SESSION_FILES = 50;
48
48
  const CHATGPT_USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage';
49
+ function timestampMs(value, fallbackMs = 0) {
50
+ if (!value)
51
+ return fallbackMs;
52
+ const ms = Date.parse(value);
53
+ return Number.isFinite(ms) ? ms : fallbackMs;
54
+ }
55
+ // Codex can report slightly different percentages for the same reset window
56
+ // across recent sessions. Prefer newer reset windows, then the highest observed
57
+ // same-window utilization so local fallbacks do not under-report quota usage.
58
+ function isPreferredQuotaHit(candidate, current) {
59
+ if (!current)
60
+ return true;
61
+ const candidatePrimaryReset = timestampMs(candidate.quota.fiveHour.resetsAt);
62
+ const currentPrimaryReset = timestampMs(current.quota.fiveHour.resetsAt);
63
+ if (candidatePrimaryReset !== currentPrimaryReset)
64
+ return candidatePrimaryReset > currentPrimaryReset;
65
+ const candidateSecondaryReset = timestampMs(candidate.quota.sevenDay.resetsAt);
66
+ const currentSecondaryReset = timestampMs(current.quota.sevenDay.resetsAt);
67
+ if (candidateSecondaryReset !== currentSecondaryReset)
68
+ return candidateSecondaryReset > currentSecondaryReset;
69
+ const candidateUtilization = candidate.quota.fiveHour.utilization + candidate.quota.sevenDay.utilization;
70
+ const currentUtilization = current.quota.fiveHour.utilization + current.quota.sevenDay.utilization;
71
+ if (candidateUtilization !== currentUtilization)
72
+ return candidateUtilization > currentUtilization;
73
+ const candidateMs = timestampMs(candidate.quota.capturedAt, candidate.mtimeMs);
74
+ const currentMs = timestampMs(current.quota.capturedAt, current.mtimeMs);
75
+ if (candidateMs !== currentMs)
76
+ return candidateMs > currentMs;
77
+ return candidate.mtimeMs > current.mtimeMs;
78
+ }
49
79
  function normalizePercent(value) {
50
80
  return typeof value === 'number' && Number.isFinite(value) ? value : 0;
51
81
  }
@@ -175,14 +205,53 @@ function quotaFromCodexRateLimits(rateLimits, source = 'session', capturedAt = n
175
205
  };
176
206
  }
177
207
  function readLatestCodexQuotaFromRollouts(sessionPaths, options = {}) {
208
+ return readLatestCodexQuotaHitFromRollouts(sessionPaths, options)?.quota ?? null;
209
+ }
210
+ function readLatestCodexQuotaHitFromRollouts(sessionPaths, options = {}) {
178
211
  const maxSessionFiles = options.maxSessionFiles ?? DEFAULT_MAX_SESSION_FILES;
179
212
  const maxTailBytes = options.maxTailBytes ?? DEFAULT_TAIL_BYTES;
213
+ let latest = null;
180
214
  for (const sessionPath of sessionPaths.slice(0, maxSessionFiles)) {
181
215
  const hit = readLatestQuotaFromRollout(sessionPath, maxTailBytes, options.source ?? 'session');
182
- if (hit)
183
- return hit.quota;
216
+ if (hit && isPreferredQuotaHit(hit, latest)) {
217
+ latest = hit;
218
+ }
184
219
  }
185
- return null;
220
+ return latest;
221
+ }
222
+ function dedupePaths(paths) {
223
+ const seen = new Set();
224
+ const unique = [];
225
+ for (const filePath of paths) {
226
+ if (seen.has(filePath))
227
+ continue;
228
+ seen.add(filePath);
229
+ unique.push(filePath);
230
+ }
231
+ return unique;
232
+ }
233
+ function sortPathsByMtimeDesc(paths) {
234
+ return [...paths].sort((a, b) => {
235
+ const aMtime = safeMtimeMs(a);
236
+ const bMtime = safeMtimeMs(b);
237
+ return bMtime - aMtime;
238
+ });
239
+ }
240
+ function safeMtimeMs(filePath) {
241
+ try {
242
+ return fs.statSync(filePath).mtime.getTime();
243
+ }
244
+ catch {
245
+ return 0;
246
+ }
247
+ }
248
+ function findAccountRolloutFiles(codexHome) {
249
+ const homes = codexHome ? [codexHome] : (0, codexProfiles_1.getCodexMonitoringHomes)();
250
+ const files = [];
251
+ for (const home of homes) {
252
+ files.push(...findRolloutFiles(path.join(home, 'sessions')));
253
+ }
254
+ return sortPathsByMtimeDesc(dedupePaths(files));
186
255
  }
187
256
  function resolveCodexQuotaFromLocalSources(options = {}) {
188
257
  const account = options.activeAccount !== undefined ? options.activeAccount : (0, codexProfiles_1.getActiveCodexAccount)();
@@ -196,20 +265,22 @@ function resolveCodexQuotaFromLocalSources(options = {}) {
196
265
  return new codex_1.CodexProvider();
197
266
  })();
198
267
  try {
199
- const workspaceSessions = options.workspacePath ? provider.findAllSessions(options.workspacePath) : [];
200
- const workspaceQuota = readLatestCodexQuotaFromRollouts(workspaceSessions, { maxTailBytes, maxSessionFiles });
201
- if (workspaceQuota) {
202
- if (account)
203
- writeSnapshot('codex', account.id, workspaceQuota);
204
- return enrichCodexQuota(workspaceQuota, account);
268
+ const candidates = [];
269
+ if (options.workspacePath) {
270
+ const workspaceSessions = provider.findAllSessions(options.workspacePath);
271
+ const workspaceHit = readLatestCodexQuotaHitFromRollouts(workspaceSessions, { maxTailBytes, maxSessionFiles });
272
+ if (workspaceHit)
273
+ candidates.push(workspaceHit);
205
274
  }
206
- const codexHome = options.codexHome ?? (0, codexProfiles_1.resolveSidekickCodexHome)();
207
- const accountSessions = findRolloutFiles(path.join(codexHome, 'sessions'));
208
- const accountQuota = readLatestCodexQuotaFromRollouts(accountSessions, { maxTailBytes, maxSessionFiles });
209
- if (accountQuota) {
275
+ const accountSessions = findAccountRolloutFiles(options.codexHome);
276
+ const accountHit = readLatestCodexQuotaHitFromRollouts(accountSessions, { maxTailBytes, maxSessionFiles });
277
+ if (accountHit)
278
+ candidates.push(accountHit);
279
+ const latestHit = candidates.reduce((latest, candidate) => isPreferredQuotaHit(candidate, latest) ? candidate : latest, null);
280
+ if (latestHit) {
210
281
  if (account)
211
- writeSnapshot('codex', account.id, accountQuota);
212
- return enrichCodexQuota(accountQuota, account);
282
+ writeSnapshot('codex', account.id, latestHit.quota);
283
+ return enrichCodexQuota(latestHit.quota, account);
213
284
  }
214
285
  const cached = account ? readSnapshot('codex', account.id) : null;
215
286
  if (cached) {
@@ -378,7 +449,7 @@ function readLatestQuotaFromRollout(sessionPath, maxTailBytes, source) {
378
449
  continue;
379
450
  const quota = quotaFromCodexRateLimits(parsed.payload.rate_limits, source, parsed.timestamp ?? new Date(stat.mtime).toISOString());
380
451
  if (quota)
381
- return { quota, filePath: sessionPath };
452
+ return { quota, filePath: sessionPath, mtimeMs: stat.mtime.getTime() };
382
453
  }
383
454
  catch {
384
455
  // Ignore malformed or partial lines.
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Provider-neutral session context evidence projection.
3
+ *
4
+ * Converts canonical SessionEvent streams into a compact manifest suitable for
5
+ * UI surfaces that need to explain what an assistant/provider has seen.
6
+ */
7
+ import type { AggregatedMetrics } from '../aggregation/types';
8
+ import type { ProviderId, SessionProviderBase } from '../providers/types';
9
+ import type { CompactionEvent, ContextAttribution, PermissionMode, SessionEvent, TruncationEvent } from '../types/sessionEvent';
10
+ export type SessionContextPressure = 'low' | 'medium' | 'high';
11
+ export type SessionContextSourceType = 'system' | 'runtime' | 'rate_limit' | 'user_prompt' | 'assistant_response' | 'thinking' | 'tool_input' | 'tool_output' | 'summary' | 'error' | 'other';
12
+ export interface SessionContextLayerBreakdown {
13
+ layer: string;
14
+ tokenEstimate: number;
15
+ sourceCount: number;
16
+ }
17
+ export interface SessionContextSource {
18
+ id: string;
19
+ providerId?: ProviderId;
20
+ sessionId?: string;
21
+ sessionPath?: string;
22
+ eventType: SessionEvent['type'];
23
+ timestamp: string;
24
+ layer: string;
25
+ sourceType: SessionContextSourceType;
26
+ title: string;
27
+ sourceRef?: string;
28
+ sourceFile?: string;
29
+ toolName?: string;
30
+ score?: number;
31
+ tokenEstimate: number;
32
+ snippet: string;
33
+ body?: string;
34
+ metadata?: Record<string, unknown>;
35
+ }
36
+ export interface SessionContextCapabilities {
37
+ providerId?: ProviderId;
38
+ providerLabel?: string;
39
+ model?: string;
40
+ observedTools: string[];
41
+ mcpServers: string[];
42
+ permissionMode?: PermissionMode;
43
+ rateLimits?: SessionEvent['rateLimits'];
44
+ }
45
+ export interface SessionContextSnapshot {
46
+ schemaVersion: 1;
47
+ providerId?: ProviderId;
48
+ sessionId?: string;
49
+ sessionPath?: string;
50
+ model?: string;
51
+ contextWindow: number;
52
+ contextTokens: number;
53
+ pressure: SessionContextPressure;
54
+ pressureRatio: number;
55
+ layers: string[];
56
+ breakdown: SessionContextLayerBreakdown[];
57
+ sources: SessionContextSource[];
58
+ capabilities: SessionContextCapabilities;
59
+ attribution: ContextAttribution;
60
+ tokens: AggregatedMetrics['tokens'];
61
+ compactionCount: number;
62
+ compactionEvents: CompactionEvent[];
63
+ truncationCount: number;
64
+ truncationEvents: TruncationEvent[];
65
+ }
66
+ export interface BuildSessionContextSnapshotOptions {
67
+ providerId?: ProviderId;
68
+ providerLabel?: string;
69
+ sessionId?: string;
70
+ sessionPath?: string;
71
+ model?: string;
72
+ contextWindow?: number;
73
+ contextWindowForModel?: (modelId?: string) => number;
74
+ computeContextSize?: (usage: {
75
+ inputTokens: number;
76
+ outputTokens: number;
77
+ cacheWriteTokens: number;
78
+ cacheReadTokens: number;
79
+ reasoningTokens?: number;
80
+ }) => number;
81
+ includeBodies?: boolean;
82
+ bodyMaxChars?: number;
83
+ snippetMaxChars?: number;
84
+ sourceLimit?: number;
85
+ }
86
+ export type ReadSessionContextSnapshotOptions = Omit<BuildSessionContextSnapshotOptions, 'providerId' | 'providerLabel' | 'sessionId' | 'sessionPath' | 'contextWindowForModel' | 'computeContextSize'>;
87
+ export interface SessionContextProjector {
88
+ processEvent(event: SessionEvent): SessionContextSnapshot;
89
+ processEvents(events: readonly SessionEvent[]): SessionContextSnapshot;
90
+ getSnapshot(): SessionContextSnapshot;
91
+ reset(): void;
92
+ }
93
+ export declare function calculateSessionContextPressure(contextTokens: number, contextWindow: number): {
94
+ pressure: SessionContextPressure;
95
+ ratio: number;
96
+ };
97
+ export declare function buildSessionContextSnapshot(events: readonly SessionEvent[], options?: BuildSessionContextSnapshotOptions): SessionContextSnapshot;
98
+ export declare function createSessionContextProjector(options?: BuildSessionContextSnapshotOptions): SessionContextProjector;
99
+ export declare function readSessionContextSnapshot(provider: SessionProviderBase, sessionPath: string, options?: ReadSessionContextSnapshotOptions): SessionContextSnapshot;