sidekick-shared 0.22.0 → 0.23.1

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.
Files changed (120) hide show
  1. package/README.md +75 -63
  2. package/dist/accountManager.js +10 -0
  3. package/dist/accountRegistry.d.ts +2 -0
  4. package/dist/accountStatus.d.ts +1 -0
  5. package/dist/accountStatus.js +6 -0
  6. package/dist/accounts.js +2 -0
  7. package/dist/aggregation/EventAggregator.d.ts +36 -2
  8. package/dist/aggregation/EventAggregator.js +259 -33
  9. package/dist/aggregation/snapshot.d.ts +1 -1
  10. package/dist/aggregation/snapshot.js +2 -1
  11. package/dist/aggregation/types.d.ts +25 -1
  12. package/dist/analytics/codeImpact.d.ts +20 -0
  13. package/dist/analytics/codeImpact.js +21 -0
  14. package/dist/analytics/compactionLedger.d.ts +11 -0
  15. package/dist/analytics/compactionLedger.js +31 -0
  16. package/dist/analytics/qualityScore.d.ts +26 -0
  17. package/dist/analytics/qualityScore.js +115 -0
  18. package/dist/autoSwitch.d.ts +1 -1
  19. package/dist/autoSwitch.js +14 -3
  20. package/dist/codexProfiles.js +2 -0
  21. package/dist/codexQuota.d.ts +2 -0
  22. package/dist/codexQuota.js +27 -0
  23. package/dist/codexQuotaWatcher.d.ts +7 -0
  24. package/dist/codexQuotaWatcher.js +37 -19
  25. package/dist/context/composer.d.ts +3 -2
  26. package/dist/context/composer.js +5 -5
  27. package/dist/context/sessionContext.js +35 -9
  28. package/dist/credentialIO.js +19 -5
  29. package/dist/doctor.d.ts +55 -0
  30. package/dist/doctor.js +244 -0
  31. package/dist/errorHistory.d.ts +33 -0
  32. package/dist/errorHistory.js +95 -0
  33. package/dist/extractors/errorTaxonomy.d.ts +6 -0
  34. package/dist/extractors/errorTaxonomy.js +47 -0
  35. package/dist/extractors/toolCall.d.ts +9 -0
  36. package/dist/extractors/toolCall.js +79 -0
  37. package/dist/handoffUrl.d.ts +7 -0
  38. package/dist/handoffUrl.js +26 -0
  39. package/dist/index.d.ts +32 -9
  40. package/dist/index.js +64 -24
  41. package/dist/modelInfo.js +6 -0
  42. package/dist/multiProviderQuotaService.js +13 -2
  43. package/dist/parsers/codexParser.js +17 -1
  44. package/dist/parsers/openCodeParser.js +13 -0
  45. package/dist/parsers/planExtractor.js +17 -6
  46. package/dist/parsers/sessionActivityDetector.js +2 -0
  47. package/dist/parsers/sessionPathResolver.js +19 -16
  48. package/dist/parsers/subagentScanner.d.ts +1 -1
  49. package/dist/parsers/subagentScanner.js +4 -5
  50. package/dist/parsers/subagentTraceParser.js +4 -7
  51. package/dist/paths.d.ts +10 -0
  52. package/dist/paths.js +18 -5
  53. package/dist/projectMigration.d.ts +23 -0
  54. package/dist/projectMigration.js +95 -0
  55. package/dist/providerStatus.js +13 -2
  56. package/dist/providers/claudeCode.js +18 -4
  57. package/dist/providers/codex.js +42 -13
  58. package/dist/providers/codexDatabase.d.ts +1 -11
  59. package/dist/providers/detect.js +32 -3
  60. package/dist/providers/openCodeDatabase.d.ts +1 -31
  61. package/dist/providers/openCodeDatabase.js +21 -11
  62. package/dist/providers/types.d.ts +1 -0
  63. package/dist/quota.js +8 -0
  64. package/dist/quotaPoller.js +17 -6
  65. package/dist/quotaSnapshots.js +80 -26
  66. package/dist/readers/decisions.d.ts +2 -1
  67. package/dist/readers/decisions.js +5 -5
  68. package/dist/readers/handoff.d.ts +2 -1
  69. package/dist/readers/handoff.js +13 -8
  70. package/dist/readers/helpers.d.ts +3 -0
  71. package/dist/readers/helpers.js +12 -0
  72. package/dist/readers/notes.d.ts +2 -1
  73. package/dist/readers/notes.js +11 -8
  74. package/dist/readers/plans.js +3 -5
  75. package/dist/readers/tasks.d.ts +2 -1
  76. package/dist/readers/tasks.js +5 -5
  77. package/dist/report/htmlHelpers.js +6 -1
  78. package/dist/report/openBrowser.js +3 -3
  79. package/dist/schemas/index.d.ts +2 -0
  80. package/dist/schemas/index.js +10 -1
  81. package/dist/schemas/observedSessionV1.d.ts +499 -0
  82. package/dist/schemas/observedSessionV1.js +69 -0
  83. package/dist/schemas/sessionEvent.d.ts +13 -0
  84. package/dist/schemas/sessionEvent.js +19 -0
  85. package/dist/search/sessionSearch.d.ts +2 -2
  86. package/dist/search/sessionSearch.js +2 -3
  87. package/dist/sessionMonitor.d.ts +29 -0
  88. package/dist/sessionMonitor.js +71 -0
  89. package/dist/statusline/BurnRateCalculator.d.ts +14 -0
  90. package/dist/statusline/BurnRateCalculator.js +45 -0
  91. package/dist/statusline/formatter.d.ts +16 -0
  92. package/dist/statusline/formatter.js +84 -0
  93. package/dist/statusline/index.d.ts +7 -0
  94. package/dist/statusline/index.js +13 -0
  95. package/dist/terminalSync.js +16 -7
  96. package/dist/types/codex.d.ts +2 -0
  97. package/dist/types/historicalData.d.ts +49 -1
  98. package/dist/types/historicalData.js +28 -2
  99. package/dist/types/observedSessionV1.d.ts +70 -0
  100. package/dist/types/observedSessionV1.js +186 -0
  101. package/dist/types/opencode.d.ts +1 -0
  102. package/dist/types/sessionEvent.d.ts +19 -1
  103. package/dist/watchers/eventBridge.js +1 -0
  104. package/dist/watchers/factory.js +14 -3
  105. package/dist/watchers/jsonlTail.js +34 -14
  106. package/dist/watchers/jsonlWatcher.js +5 -3
  107. package/dist/watchers/providerReaderWatcher.js +4 -2
  108. package/dist/watchers/sqliteWatcher.d.ts +2 -0
  109. package/dist/watchers/sqliteWatcher.js +27 -14
  110. package/dist/watchers/types.d.ts +5 -0
  111. package/dist/writers/atomic.d.ts +4 -0
  112. package/dist/writers/atomic.js +200 -0
  113. package/dist/writers/decisions.d.ts +12 -0
  114. package/dist/writers/decisions.js +88 -0
  115. package/dist/writers/notes.d.ts +8 -0
  116. package/dist/writers/notes.js +82 -0
  117. package/dist/writers/tasks.d.ts +6 -0
  118. package/dist/writers/tasks.js +100 -0
  119. package/dist/zaiQuotaWatcher.js +6 -0
  120. package/package.json +5 -1
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SessionMonitor = void 0;
4
+ const EventAggregator_1 = require("./aggregation/EventAggregator");
5
+ /**
6
+ * UI-independent session monitor for library consumers. It owns one provider
7
+ * reader and the canonical aggregator; hosts decide how often to call `poll()`.
8
+ */
9
+ class SessionMonitor {
10
+ provider;
11
+ aggregator;
12
+ reader = null;
13
+ sessionPath = null;
14
+ listeners = new Set();
15
+ replayOnAttach;
16
+ constructor(provider, options = {}) {
17
+ this.provider = provider;
18
+ this.replayOnAttach = options.replayOnAttach ?? true;
19
+ this.aggregator = new EventAggregator_1.EventAggregator({ ...options, providerId: provider.id });
20
+ }
21
+ attach(sessionPath, replay = this.replayOnAttach) {
22
+ this.aggregator.reset();
23
+ this.reader = this.provider.createReader(sessionPath);
24
+ this.sessionPath = sessionPath;
25
+ if (replay)
26
+ this.consume(this.reader.readAll());
27
+ return this.getMetrics();
28
+ }
29
+ attachLatest(workspacePath, replay = this.replayOnAttach) {
30
+ const sessionPath = this.provider.findActiveSession(workspacePath);
31
+ return sessionPath ? this.attach(sessionPath, replay) : null;
32
+ }
33
+ poll() {
34
+ if (!this.reader)
35
+ return [];
36
+ if (this.reader.wasTruncated())
37
+ this.aggregator.reset();
38
+ const events = this.reader.readNew();
39
+ this.consume(events);
40
+ return events;
41
+ }
42
+ process(event) {
43
+ this.aggregator.processEvent(event);
44
+ for (const listener of this.listeners)
45
+ listener(event);
46
+ }
47
+ onEvent(listener) {
48
+ this.listeners.add(listener);
49
+ return () => this.listeners.delete(listener);
50
+ }
51
+ getMetrics() {
52
+ return this.aggregator.getMetrics();
53
+ }
54
+ getSessionPath() {
55
+ return this.sessionPath;
56
+ }
57
+ reset() {
58
+ this.reader?.reset();
59
+ this.aggregator.reset();
60
+ }
61
+ dispose() {
62
+ this.listeners.clear();
63
+ this.reader = null;
64
+ this.provider.dispose();
65
+ }
66
+ consume(events) {
67
+ for (const event of events)
68
+ this.process(event);
69
+ }
70
+ }
71
+ exports.SessionMonitor = SessionMonitor;
@@ -0,0 +1,14 @@
1
+ /** Sliding-window burn-rate calculator shared by every Sidekick surface. */
2
+ export declare class BurnRateCalculator {
3
+ private events;
4
+ private readonly windowMs;
5
+ constructor(windowMinutes?: number);
6
+ addEvent(tokens: number, timestamp?: Date): void;
7
+ calculateBurnRate(now?: Date): number;
8
+ estimateTimeToQuota(currentTokens: number, quotaLimit: number, now?: Date): number | null;
9
+ getEventCount(): number;
10
+ reset(): void;
11
+ private pruneOldEvents;
12
+ }
13
+ /** Pure ETA helper for cached or externally computed burn rates. */
14
+ export declare function estimateTimeToQuota(currentUsage: number, quotaLimit: number, burnRatePerMinute: number): number | null;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BurnRateCalculator = void 0;
4
+ exports.estimateTimeToQuota = estimateTimeToQuota;
5
+ /** Sliding-window burn-rate calculator shared by every Sidekick surface. */
6
+ class BurnRateCalculator {
7
+ events = [];
8
+ windowMs;
9
+ constructor(windowMinutes = 5) {
10
+ this.windowMs = windowMinutes * 60_000;
11
+ }
12
+ addEvent(tokens, timestamp = new Date()) {
13
+ this.events.push({ tokens, timestamp });
14
+ this.pruneOldEvents(timestamp);
15
+ }
16
+ calculateBurnRate(now = new Date()) {
17
+ this.pruneOldEvents(now);
18
+ if (this.events.length === 0)
19
+ return 0;
20
+ const totalTokens = this.events.reduce((sum, event) => sum + event.tokens, 0);
21
+ const elapsedMinutes = Math.max((now.getTime() - this.events[0].timestamp.getTime()) / 60_000, 1);
22
+ return totalTokens / elapsedMinutes;
23
+ }
24
+ estimateTimeToQuota(currentTokens, quotaLimit, now = new Date()) {
25
+ return estimateTimeToQuota(currentTokens, quotaLimit, this.calculateBurnRate(now));
26
+ }
27
+ getEventCount() {
28
+ return this.events.length;
29
+ }
30
+ reset() {
31
+ this.events = [];
32
+ }
33
+ pruneOldEvents(now) {
34
+ const cutoff = now.getTime() - this.windowMs;
35
+ this.events = this.events.filter((event) => event.timestamp.getTime() >= cutoff);
36
+ }
37
+ }
38
+ exports.BurnRateCalculator = BurnRateCalculator;
39
+ /** Pure ETA helper for cached or externally computed burn rates. */
40
+ function estimateTimeToQuota(currentUsage, quotaLimit, burnRatePerMinute) {
41
+ if (burnRatePerMinute <= 0)
42
+ return null;
43
+ const remaining = quotaLimit - currentUsage;
44
+ return remaining <= 0 ? 0 : remaining / burnRatePerMinute;
45
+ }
@@ -0,0 +1,16 @@
1
+ import type { ActiveAccountStatus } from '../accountStatus';
2
+ import type { QuotaState } from '../quota';
3
+ export interface StatuslineInput {
4
+ accounts: ActiveAccountStatus;
5
+ claudeQuota?: QuotaState | null;
6
+ codexQuota?: QuotaState | null;
7
+ now?: Date;
8
+ }
9
+ export interface StatuslineSelection {
10
+ providerId: 'claude-code' | 'codex';
11
+ accountId?: string;
12
+ accountLabel: string;
13
+ quota: QuotaState | null;
14
+ }
15
+ export declare function selectStatuslineAccount(input: StatuslineInput): StatuslineSelection | null;
16
+ export declare function formatStatusline(input: StatuslineInput): string;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.selectStatuslineAccount = selectStatuslineAccount;
4
+ exports.formatStatusline = formatStatusline;
5
+ const quota_1 = require("../quota");
6
+ const BurnRateCalculator_1 = require("./BurnRateCalculator");
7
+ function selectStatuslineAccount(input) {
8
+ if (input.accounts.claude.present) {
9
+ return {
10
+ providerId: 'claude-code',
11
+ accountId: input.accounts.claude.accountId,
12
+ accountLabel: input.accounts.claude.label ?? input.accounts.claude.email ?? 'claude',
13
+ quota: input.claudeQuota ?? null,
14
+ };
15
+ }
16
+ if (input.accounts.codex.present) {
17
+ return {
18
+ providerId: 'codex',
19
+ accountId: input.accounts.codex.accountId,
20
+ accountLabel: input.accounts.codex.label ?? input.accounts.codex.email ?? 'codex',
21
+ quota: input.codexQuota ?? null,
22
+ };
23
+ }
24
+ return null;
25
+ }
26
+ function formatStatusline(input) {
27
+ const selected = selectStatuslineAccount(input);
28
+ if (!selected)
29
+ return 'acct:none · quota unavailable';
30
+ const account = compactLabel(selected.accountLabel);
31
+ const quota = selected.quota;
32
+ if (!quota?.available)
33
+ return `acct:${account} · quota unavailable`;
34
+ const now = input.now ?? new Date();
35
+ const utilization = clampPercent(quota.fiveHour.utilization);
36
+ const reset = formatReset(quota.fiveHour.resetsAt, now);
37
+ const eta = estimateWindowEta(quota, now);
38
+ const etaText = eta == null ? '' : ` · ~${formatMinutes(eta)} left`;
39
+ return `acct:${account} · 5h ${Math.round(utilization)}% resets ${reset}${etaText}`;
40
+ }
41
+ function estimateWindowEta(quota, now) {
42
+ const resetAt = Date.parse(quota.fiveHour.resetsAt);
43
+ if (!Number.isFinite(resetAt))
44
+ return null;
45
+ const nowMs = now.getTime();
46
+ if (resetAt <= nowMs)
47
+ return null;
48
+ const capturedAt = quota.capturedAt ? Date.parse(quota.capturedAt) : now.getTime();
49
+ const sampleAt = Number.isFinite(capturedAt) ? capturedAt : now.getTime();
50
+ if (sampleAt >= resetAt)
51
+ return null;
52
+ const elapsedMinutes = (sampleAt - (resetAt - quota_1.FIVE_HOUR_WINDOW_MS)) / 60_000;
53
+ if (elapsedMinutes <= 0 || quota.fiveHour.utilization <= 0)
54
+ return null;
55
+ const burnRate = quota.fiveHour.utilization / elapsedMinutes;
56
+ const eta = (0, BurnRateCalculator_1.estimateTimeToQuota)(quota.fiveHour.utilization, 100, burnRate);
57
+ const minutesUntilReset = (resetAt - nowMs) / 60_000;
58
+ return eta !== null && eta < minutesUntilReset ? eta : null;
59
+ }
60
+ function compactLabel(value) {
61
+ const trimmed = value.trim();
62
+ const localPart = trimmed.includes('@') ? trimmed.split('@')[0] : trimmed;
63
+ return localPart.replace(/\s+/g, '-').slice(0, 24) || 'unknown';
64
+ }
65
+ function clampPercent(value) {
66
+ return Number.isFinite(value) ? Math.min(100, Math.max(0, value)) : 0;
67
+ }
68
+ function formatReset(value, fallback) {
69
+ const reset = new Date(value);
70
+ const date = Number.isNaN(reset.getTime()) ? fallback : reset;
71
+ return new Intl.DateTimeFormat(undefined, {
72
+ hour: '2-digit',
73
+ minute: '2-digit',
74
+ hour12: false,
75
+ }).format(date);
76
+ }
77
+ function formatMinutes(value) {
78
+ const minutes = Math.max(0, Math.round(value));
79
+ if (minutes < 60)
80
+ return `${minutes}min`;
81
+ const hours = Math.floor(minutes / 60);
82
+ const remainder = minutes % 60;
83
+ return remainder ? `${hours}h${remainder}m` : `${hours}h`;
84
+ }
@@ -0,0 +1,7 @@
1
+ export { getActiveAccountStatus } from '../accountStatus';
2
+ export type { ActiveAccountStatus, ActiveProviderAccountStatus } from '../accountStatus';
3
+ export { readQuotaSnapshot } from '../quotaSnapshots';
4
+ export type { QuotaSnapshotProviderId } from '../quotaSnapshots';
5
+ export { BurnRateCalculator, estimateTimeToQuota } from './BurnRateCalculator';
6
+ export { formatStatusline, selectStatuslineAccount } from './formatter';
7
+ export type { StatuslineInput, StatuslineSelection } from './formatter';
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.selectStatuslineAccount = exports.formatStatusline = exports.estimateTimeToQuota = exports.BurnRateCalculator = exports.readQuotaSnapshot = exports.getActiveAccountStatus = void 0;
4
+ var accountStatus_1 = require("../accountStatus");
5
+ Object.defineProperty(exports, "getActiveAccountStatus", { enumerable: true, get: function () { return accountStatus_1.getActiveAccountStatus; } });
6
+ var quotaSnapshots_1 = require("../quotaSnapshots");
7
+ Object.defineProperty(exports, "readQuotaSnapshot", { enumerable: true, get: function () { return quotaSnapshots_1.readQuotaSnapshot; } });
8
+ var BurnRateCalculator_1 = require("./BurnRateCalculator");
9
+ Object.defineProperty(exports, "BurnRateCalculator", { enumerable: true, get: function () { return BurnRateCalculator_1.BurnRateCalculator; } });
10
+ Object.defineProperty(exports, "estimateTimeToQuota", { enumerable: true, get: function () { return BurnRateCalculator_1.estimateTimeToQuota; } });
11
+ var formatter_1 = require("./formatter");
12
+ Object.defineProperty(exports, "formatStatusline", { enumerable: true, get: function () { return formatter_1.formatStatusline; } });
13
+ Object.defineProperty(exports, "selectStatuslineAccount", { enumerable: true, get: function () { return formatter_1.selectStatuslineAccount; } });
@@ -107,20 +107,28 @@ function buildShellHookBlock() {
107
107
  function getShellRcPaths() {
108
108
  const zshrc = path.join(os.homedir(), '.zshrc');
109
109
  const bashrc = path.join(os.homedir(), '.bashrc');
110
- const paths = [zshrc];
111
- if (fs.existsSync(bashrc))
112
- paths.push(bashrc);
113
- return paths;
110
+ const existing = [zshrc, bashrc].filter((filePath) => fs.existsSync(filePath));
111
+ if (existing.length > 0)
112
+ return existing;
113
+ const shell = path.basename(process.env.SHELL ?? 'zsh');
114
+ return shell === 'bash' ? [bashrc] : [zshrc];
115
+ }
116
+ function resolveShellRcTarget(filePath) {
117
+ if (!fs.existsSync(filePath))
118
+ return { filePath, mode: 0o600 };
119
+ const realPath = fs.realpathSync(filePath);
120
+ return { filePath: realPath, mode: fs.statSync(realPath).mode & 0o777 };
114
121
  }
115
122
  function stripShellHook(content) {
116
123
  const pattern = new RegExp(`${HOOK_START}[\\s\\S]*?${HOOK_END}\\n?`, 'g');
117
124
  return content.replace(pattern, '').replace(/\n{3,}/g, '\n\n');
118
125
  }
119
126
  function installHookInFile(filePath) {
120
- const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
127
+ const target = resolveShellRcTarget(filePath);
128
+ const existing = fs.existsSync(target.filePath) ? fs.readFileSync(target.filePath, 'utf8') : '';
121
129
  const cleaned = stripShellHook(existing).replace(/\s*$/, '');
122
130
  const next = cleaned ? `${cleaned}\n\n${buildShellHookBlock()}` : buildShellHookBlock();
123
- atomicWriteFile(filePath, next, 0o600);
131
+ atomicWriteFile(target.filePath, next, target.mode);
124
132
  }
125
133
  function installShellHook() {
126
134
  for (const rcPath of getShellRcPaths()) {
@@ -130,7 +138,8 @@ function installShellHook() {
130
138
  function uninstallHookInFile(filePath) {
131
139
  if (!fs.existsSync(filePath))
132
140
  return;
133
- atomicWriteFile(filePath, stripShellHook(fs.readFileSync(filePath, 'utf8')), 0o600);
141
+ const target = resolveShellRcTarget(filePath);
142
+ atomicWriteFile(target.filePath, stripShellHook(fs.readFileSync(target.filePath, 'utf8')), target.mode);
134
143
  }
135
144
  function uninstallShellHook() {
136
145
  for (const rcPath of getShellRcPaths()) {
@@ -124,6 +124,8 @@ export interface CodexCustomToolCallOutputItem {
124
124
  /** Context compaction marker. */
125
125
  export interface CodexCompacted {
126
126
  summary?: string;
127
+ tokens_before?: number;
128
+ tokens_after?: number;
127
129
  }
128
130
  /** Turn context providing model and policy info. */
129
131
  export interface CodexTurnContext {
@@ -10,8 +10,12 @@
10
10
  * $0 because pricing was unknown — surfaced in the dashboard footer.
11
11
  * Legacy records (schemaVersion 1 or missing) have neither field and should
12
12
  * be treated as fully priced.
13
+ *
14
+ * Schema version 3 (2026-07-18) adds a capped per-session record list with
15
+ * provider/project dimensions, beta quality factors, churn, and impact cost.
13
16
  */
14
- export declare const HISTORICAL_DATA_SCHEMA_VERSION = 2;
17
+ export declare const HISTORICAL_DATA_SCHEMA_VERSION = 3;
18
+ export declare const HISTORICAL_SESSION_RETENTION_LIMIT = 500;
15
19
  export interface TokenTotals {
16
20
  inputTokens: number;
17
21
  outputTokens: number;
@@ -47,6 +51,13 @@ export interface DailyData {
47
51
  toolUsage: ToolUsageRecord[];
48
52
  updatedAt: string;
49
53
  }
54
+ export interface HourlyData {
55
+ hour: number;
56
+ tokens: TokenTotals;
57
+ totalCost: number;
58
+ messageCount: number;
59
+ sessionCount: number;
60
+ }
50
61
  export interface MonthlyData {
51
62
  month: string;
52
63
  tokens: TokenTotals;
@@ -71,12 +82,41 @@ export interface AllTimeStats {
71
82
  export interface HistoricalDataStore {
72
83
  schemaVersion: number;
73
84
  daily: Record<string, DailyData>;
85
+ /** Hourly buckets keyed by local YYYY-MM-DD. */
86
+ hourly?: Record<string, HourlyData[]>;
74
87
  monthly: Record<string, MonthlyData>;
75
88
  allTime: AllTimeStats;
76
89
  lastSaved: string;
90
+ /** Capped durable per-session records, newest last (schema v3). */
91
+ sessions?: SessionHistoryRecord[];
77
92
  importedFiles?: string[];
78
93
  lastImportTimestamp?: string;
79
94
  }
95
+ export interface SessionHistoryRecord {
96
+ sessionId: string;
97
+ provider: string;
98
+ project: string;
99
+ startTime: string;
100
+ endTime: string;
101
+ tokens: TokenTotals;
102
+ totalCost: number;
103
+ messageCount: number;
104
+ /** Schema v3 additive fields used to make aggregate replacement idempotent. */
105
+ modelUsage?: ModelUsageRecord[];
106
+ toolUsage?: ToolUsageRecord[];
107
+ unpricedModelIds?: string[];
108
+ qualityScore: number;
109
+ qualityFactors: Array<{
110
+ id: string;
111
+ label: string;
112
+ contribution: number;
113
+ maximum: number;
114
+ detail: string;
115
+ }>;
116
+ additions: number;
117
+ deletions: number;
118
+ costPerChangedLine: number | null;
119
+ }
80
120
  export interface SessionSummary {
81
121
  sessionId: string;
82
122
  startTime: string;
@@ -96,5 +136,13 @@ export interface SessionSummary {
96
136
  * Omitted on v1 records or when every model was priced.
97
137
  */
98
138
  unpricedModelIds?: string[];
139
+ provider?: string;
140
+ project?: string;
141
+ qualityScore?: number;
142
+ qualityFactors?: SessionHistoryRecord['qualityFactors'];
143
+ additions?: number;
144
+ deletions?: number;
145
+ costPerChangedLine?: number | null;
99
146
  }
100
147
  export declare function createEmptyTokenTotals(): TokenTotals;
148
+ export declare function createEmptyDataStore(): HistoricalDataStore;
@@ -11,11 +11,16 @@
11
11
  * $0 because pricing was unknown — surfaced in the dashboard footer.
12
12
  * Legacy records (schemaVersion 1 or missing) have neither field and should
13
13
  * be treated as fully priced.
14
+ *
15
+ * Schema version 3 (2026-07-18) adds a capped per-session record list with
16
+ * provider/project dimensions, beta quality factors, churn, and impact cost.
14
17
  */
15
18
  Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.HISTORICAL_DATA_SCHEMA_VERSION = void 0;
19
+ exports.HISTORICAL_SESSION_RETENTION_LIMIT = exports.HISTORICAL_DATA_SCHEMA_VERSION = void 0;
17
20
  exports.createEmptyTokenTotals = createEmptyTokenTotals;
18
- exports.HISTORICAL_DATA_SCHEMA_VERSION = 2;
21
+ exports.createEmptyDataStore = createEmptyDataStore;
22
+ exports.HISTORICAL_DATA_SCHEMA_VERSION = 3;
23
+ exports.HISTORICAL_SESSION_RETENTION_LIMIT = 500;
19
24
  function createEmptyTokenTotals() {
20
25
  return {
21
26
  inputTokens: 0,
@@ -24,3 +29,24 @@ function createEmptyTokenTotals() {
24
29
  cacheReadTokens: 0,
25
30
  };
26
31
  }
32
+ function createEmptyDataStore() {
33
+ const now = new Date().toISOString();
34
+ return {
35
+ schemaVersion: exports.HISTORICAL_DATA_SCHEMA_VERSION,
36
+ daily: {},
37
+ monthly: {},
38
+ sessions: [],
39
+ allTime: {
40
+ tokens: createEmptyTokenTotals(),
41
+ totalCost: 0,
42
+ messageCount: 0,
43
+ sessionCount: 0,
44
+ firstDate: '',
45
+ lastDate: '',
46
+ modelUsage: [],
47
+ toolUsage: [],
48
+ updatedAt: now,
49
+ },
50
+ lastSaved: now,
51
+ };
52
+ }
@@ -0,0 +1,70 @@
1
+ import type { SessionEvent } from './sessionEvent';
2
+ import type { ProviderId, SessionProviderBase } from '../providers/types';
3
+ export type ObservationProvenanceV1 = 'reported' | 'estimated' | 'inferred';
4
+ export interface SessionEvidenceRefV1 {
5
+ schemaVersion: 1;
6
+ provider: ProviderId;
7
+ sessionId: string;
8
+ sourcePath?: string;
9
+ eventIndex?: number;
10
+ eventId?: string;
11
+ timestamp?: string;
12
+ }
13
+ export interface ObservedValueV1<T> {
14
+ value: T;
15
+ provenance: ObservationProvenanceV1;
16
+ confidence: number;
17
+ evidence: SessionEvidenceRefV1[];
18
+ }
19
+ export interface PendingUserRequestV1 {
20
+ schemaVersion: 1;
21
+ id: string;
22
+ provider: ProviderId;
23
+ sessionId: string;
24
+ kind: ObservedValueV1<'prompt_response' | 'tool_approval' | 'question'>;
25
+ requestedAt: ObservedValueV1<string>;
26
+ prompt: ObservedValueV1<string | null>;
27
+ }
28
+ export interface ObservedAgentSessionV1 {
29
+ schemaVersion: 1;
30
+ identity: {
31
+ provider: ProviderId;
32
+ sessionId: string;
33
+ sourcePath: string;
34
+ };
35
+ cwd: ObservedValueV1<string>;
36
+ model: ObservedValueV1<string | null>;
37
+ activity: ObservedValueV1<'active' | 'idle' | 'ended' | 'unknown'>;
38
+ usage: {
39
+ inputTokens: ObservedValueV1<number>;
40
+ outputTokens: ObservedValueV1<number>;
41
+ cacheReadTokens: ObservedValueV1<number>;
42
+ cacheWriteTokens: ObservedValueV1<number>;
43
+ costUsd: ObservedValueV1<number>;
44
+ };
45
+ pendingUserRequest: PendingUserRequestV1 | null;
46
+ observedAt: string;
47
+ }
48
+ export interface ProviderCapabilitiesV1 {
49
+ schemaVersion: 1;
50
+ provider: ProviderId;
51
+ resume: ObservedValueV1<boolean>;
52
+ forkLineage: ObservedValueV1<boolean>;
53
+ quotaSource: ObservedValueV1<'api' | 'session' | 'mixed' | 'none'>;
54
+ assetExtraction: ObservedValueV1<boolean>;
55
+ }
56
+ export interface ProviderSessionAdapterV1 {
57
+ readonly schemaVersion: 1;
58
+ readonly provider: ProviderId;
59
+ readonly capabilities: ProviderCapabilitiesV1;
60
+ discover(cwd: string): Promise<ObservedAgentSessionV1[]>;
61
+ read(sessionPath: string, cwd?: string): Promise<ObservedAgentSessionV1>;
62
+ watch(sessionPath: string, listener: (session: ObservedAgentSessionV1) => void, cwd?: string): {
63
+ dispose(): void;
64
+ };
65
+ dispose(): void;
66
+ }
67
+ export declare function derivePendingUserRequestV1(provider: ProviderId, sessionId: string, sourcePath: string, events: SessionEvent[]): PendingUserRequestV1 | null;
68
+ export declare function createProviderSessionAdapterV1(sessionProvider: SessionProviderBase, options?: {
69
+ pollIntervalMs?: number;
70
+ }): ProviderSessionAdapterV1;