@leo000001/opencode-quota-sidebar 2.0.0 → 2.0.2

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.
@@ -17,14 +17,32 @@ export declare function createTitleApplicator(deps: {
17
17
  scheduleParentRefreshIfSafe: (sessionID: string, parentID?: string) => void;
18
18
  restoreConcurrency: number;
19
19
  }): {
20
- applyTitle: (sessionID: string) => Promise<void>;
20
+ applyTitle: (sessionID: string) => Promise<boolean>;
21
21
  handleSessionUpdatedTitle: (args: {
22
22
  sessionID: string;
23
23
  incomingTitle: string;
24
24
  sessionState: SessionState;
25
25
  scheduleRefresh: (sessionID: string, delay?: number) => void;
26
26
  }) => Promise<void>;
27
- restoreSessionTitle: (sessionID: string) => Promise<void>;
28
- restoreAllVisibleTitles: () => Promise<void>;
27
+ restoreSessionTitle: (sessionID: string, options?: {
28
+ abortIfEnabled?: boolean;
29
+ }) => Promise<boolean>;
30
+ restoreAllVisibleTitles: (options?: {
31
+ abortIfEnabled?: boolean;
32
+ }) => Promise<{
33
+ attempted: number;
34
+ restored: number;
35
+ listFailed: boolean;
36
+ }>;
37
+ refreshAllTouchedTitles: () => Promise<{
38
+ attempted: number;
39
+ refreshed: number;
40
+ listFailed: boolean;
41
+ }>;
42
+ refreshAllVisibleTitles: () => Promise<{
43
+ attempted: number;
44
+ refreshed: number;
45
+ listFailed: boolean;
46
+ }>;
29
47
  forgetSession: (sessionID: string) => void;
30
48
  };
@@ -1,13 +1,15 @@
1
- import { canonicalizeTitle, canonicalizeTitleForCompare, looksDecorated, normalizeBaseTitle, } from './title.js';
1
+ import { canonicalizeTitle, canonicalizeTitleForCompare, looksDecorated, } from './title.js';
2
2
  import { swallow, debug, mapConcurrent } from './helpers.js';
3
3
  export function createTitleApplicator(deps) {
4
4
  const pendingAppliedTitle = new Map();
5
+ const recentRestore = new Map();
5
6
  const forgetSession = (sessionID) => {
6
7
  pendingAppliedTitle.delete(sessionID);
8
+ recentRestore.delete(sessionID);
7
9
  };
8
10
  const applyTitle = async (sessionID) => {
9
11
  if (!deps.config.sidebar.enabled || !deps.state.titleEnabled)
10
- return;
12
+ return false;
11
13
  let stateMutated = false;
12
14
  const session = await deps.client.session
13
15
  .get({
@@ -17,7 +19,14 @@ export function createTitleApplicator(deps) {
17
19
  })
18
20
  .catch(swallow('applyTitle:getSession'));
19
21
  if (!session)
20
- return;
22
+ return false;
23
+ if (!session.data ||
24
+ typeof session.data.title !== 'string' ||
25
+ !session.data.time ||
26
+ typeof session.data.time.created !== 'number') {
27
+ debug(`applyTitle skipped malformed session payload for ${sessionID}`);
28
+ return false;
29
+ }
21
30
  const sessionState = deps.ensureSessionState(sessionID, session.data.title, session.data.time.created, session.data.parentID ?? null);
22
31
  // Detect whether the current title is our own decorated form.
23
32
  const currentTitle = session.data.title;
@@ -44,7 +53,7 @@ export function createTitleApplicator(deps) {
44
53
  }
45
54
  }
46
55
  else {
47
- const nextBase = normalizeBaseTitle(currentTitle);
56
+ const nextBase = canonicalizeTitle(currentTitle) || 'Session';
48
57
  if (sessionState.baseTitle !== nextBase) {
49
58
  sessionState.baseTitle = nextBase;
50
59
  stateMutated = true;
@@ -61,6 +70,8 @@ export function createTitleApplicator(deps) {
61
70
  ? await deps.getQuotaSnapshots(quotaProviders)
62
71
  : [];
63
72
  const nextTitle = deps.renderSidebarTitle(sessionState.baseTitle, usage, quotas, deps.config);
73
+ if (!deps.config.sidebar.enabled || !deps.state.titleEnabled)
74
+ return false;
64
75
  if (canonicalizeTitleForCompare(nextTitle) ===
65
76
  canonicalizeTitleForCompare(session.data.title)) {
66
77
  if (looksDecorated(session.data.title)) {
@@ -74,7 +85,7 @@ export function createTitleApplicator(deps) {
74
85
  }
75
86
  deps.scheduleSave();
76
87
  deps.scheduleParentRefreshIfSafe(sessionID, sessionState.parentID);
77
- return;
88
+ return true;
78
89
  }
79
90
  // Mark pending title to ignore the immediate echo `session.updated` event.
80
91
  // H3 fix: use longer TTL (15s) and add decoration detection as backup.
@@ -98,11 +109,12 @@ export function createTitleApplicator(deps) {
98
109
  sessionState.lastAppliedTitle = previousApplied;
99
110
  deps.scheduleSave();
100
111
  deps.scheduleParentRefreshIfSafe(sessionID, sessionState.parentID);
101
- return;
112
+ return false;
102
113
  }
103
114
  pendingAppliedTitle.delete(sessionID);
104
115
  deps.scheduleSave();
105
116
  deps.scheduleParentRefreshIfSafe(sessionID, sessionState.parentID);
117
+ return true;
106
118
  };
107
119
  const handleSessionUpdatedTitle = async (args) => {
108
120
  const pending = pendingAppliedTitle.get(args.sessionID);
@@ -129,17 +141,39 @@ export function createTitleApplicator(deps) {
129
141
  canonicalizeTitleForCompare(args.sessionState.lastAppliedTitle || '')) {
130
142
  return;
131
143
  }
132
- if (looksDecorated(args.incomingTitle)) {
133
- debug(`ignoring late decorated echo for session ${args.sessionID}`);
144
+ if (looksDecorated(args.incomingTitle) && args.sessionState.lastAppliedTitle) {
145
+ if (canonicalizeTitleForCompare(args.incomingTitle) ===
146
+ canonicalizeTitleForCompare(args.sessionState.lastAppliedTitle)) {
147
+ debug(`ignoring late decorated echo for session ${args.sessionID}`);
148
+ return;
149
+ }
150
+ }
151
+ if (looksDecorated(args.incomingTitle) && !args.sessionState.lastAppliedTitle) {
152
+ debug(`ignoring untracked decorated title for session ${args.sessionID}`);
134
153
  return;
135
154
  }
136
- args.sessionState.baseTitle = normalizeBaseTitle(args.incomingTitle);
155
+ const restored = recentRestore.get(args.sessionID);
156
+ if (restored) {
157
+ if (restored.expiresAt <= Date.now()) {
158
+ recentRestore.delete(args.sessionID);
159
+ }
160
+ else if (looksDecorated(args.incomingTitle) &&
161
+ (!restored.decoratedTitle ||
162
+ canonicalizeTitleForCompare(args.incomingTitle) ===
163
+ canonicalizeTitleForCompare(restored.decoratedTitle))) {
164
+ debug(`ignoring decorated echo after restore for session ${args.sessionID}`);
165
+ return;
166
+ }
167
+ }
168
+ args.sessionState.baseTitle = canonicalizeTitle(args.incomingTitle) || 'Session';
137
169
  args.sessionState.lastAppliedTitle = undefined;
138
170
  deps.markDirty(deps.state.sessionDateMap[args.sessionID]);
139
171
  deps.scheduleSave();
140
172
  args.scheduleRefresh(args.sessionID);
141
173
  };
142
- const restoreSessionTitle = async (sessionID) => {
174
+ const restoreSessionTitle = async (sessionID, options) => {
175
+ if (options?.abortIfEnabled && deps.state.titleEnabled)
176
+ return false;
143
177
  const session = await deps.client.session
144
178
  .get({
145
179
  path: { id: sessionID },
@@ -148,12 +182,27 @@ export function createTitleApplicator(deps) {
148
182
  })
149
183
  .catch(swallow('restoreSessionTitle:get'));
150
184
  if (!session)
151
- return;
185
+ return false;
186
+ if (!session.data ||
187
+ typeof session.data.title !== 'string' ||
188
+ !session.data.time ||
189
+ typeof session.data.time.created !== 'number') {
190
+ debug(`restoreSessionTitle skipped malformed session payload for ${sessionID}`);
191
+ return false;
192
+ }
152
193
  const sessionState = deps.ensureSessionState(sessionID, session.data.title, session.data.time.created, session.data.parentID ?? null);
153
- const baseTitle = normalizeBaseTitle(sessionState.baseTitle);
154
- if (session.data.title === baseTitle)
155
- return;
156
- await deps.client.session
194
+ const baseTitle = canonicalizeTitle(sessionState.baseTitle) || 'Session';
195
+ if (session.data.title === baseTitle) {
196
+ if (sessionState.lastAppliedTitle !== undefined) {
197
+ sessionState.lastAppliedTitle = undefined;
198
+ deps.markDirty(deps.state.sessionDateMap[sessionID]);
199
+ deps.scheduleSave();
200
+ }
201
+ return true;
202
+ }
203
+ if (options?.abortIfEnabled && deps.state.titleEnabled)
204
+ return false;
205
+ const updated = await deps.client.session
157
206
  .update({
158
207
  path: { id: sessionID },
159
208
  query: { directory: deps.directory },
@@ -161,29 +210,66 @@ export function createTitleApplicator(deps) {
161
210
  throwOnError: true,
162
211
  })
163
212
  .catch(swallow('restoreSessionTitle:update'));
213
+ if (!updated)
214
+ return false;
215
+ pendingAppliedTitle.delete(sessionID);
216
+ recentRestore.set(sessionID, {
217
+ baseTitle,
218
+ decoratedTitle: sessionState.lastAppliedTitle,
219
+ expiresAt: Date.now() + 15_000,
220
+ });
164
221
  sessionState.lastAppliedTitle = undefined;
165
222
  deps.markDirty(deps.state.sessionDateMap[sessionID]);
166
223
  deps.scheduleSave();
224
+ return true;
225
+ };
226
+ const restoreAllVisibleTitles = async (options) => {
227
+ const touched = Object.entries(deps.state.sessions)
228
+ .filter(([, sessionState]) => Boolean(sessionState.lastAppliedTitle))
229
+ .map(([sessionID]) => sessionID);
230
+ const results = await mapConcurrent(touched, deps.restoreConcurrency, async (sessionID) => restoreSessionTitle(sessionID, options));
231
+ return {
232
+ attempted: touched.length,
233
+ restored: results.filter(Boolean).length,
234
+ listFailed: false,
235
+ };
236
+ };
237
+ const refreshAllTouchedTitles = async () => {
238
+ const touched = Object.entries(deps.state.sessions)
239
+ .filter(([, sessionState]) => Boolean(sessionState.lastAppliedTitle))
240
+ .map(([sessionID]) => sessionID);
241
+ const results = await mapConcurrent(touched, deps.restoreConcurrency, async (sessionID) => applyTitle(sessionID));
242
+ return {
243
+ attempted: touched.length,
244
+ refreshed: results.filter(Boolean).length,
245
+ listFailed: false,
246
+ };
167
247
  };
168
- const restoreAllVisibleTitles = async () => {
248
+ const refreshAllVisibleTitles = async () => {
169
249
  const list = await deps.client.session
170
250
  .list({
171
251
  query: { directory: deps.directory },
172
252
  throwOnError: true,
173
253
  })
174
- .catch(swallow('restoreAllVisibleTitles:list'));
175
- if (!list?.data)
176
- return;
177
- const touched = list.data.filter((s) => deps.state.sessions[s.id]?.lastAppliedTitle);
178
- await mapConcurrent(touched, deps.restoreConcurrency, async (s) => {
179
- await restoreSessionTitle(s.id);
180
- });
254
+ .catch(swallow('refreshAllVisibleTitles:list'));
255
+ if (!list?.data || !Array.isArray(list.data)) {
256
+ return { attempted: 0, refreshed: 0, listFailed: true };
257
+ }
258
+ const sessions = list.data.filter((session) => Boolean(session && typeof session.id === 'string'));
259
+ const results = await mapConcurrent(sessions, deps.restoreConcurrency, async (session) => applyTitle(session.id));
260
+ return {
261
+ attempted: sessions.length,
262
+ refreshed: results.filter(Boolean).length,
263
+ listFailed: false,
264
+ };
181
265
  };
182
266
  return {
183
267
  applyTitle,
184
268
  handleSessionUpdatedTitle,
185
269
  restoreSessionTitle,
186
270
  restoreAllVisibleTitles,
271
+ refreshAllTouchedTitles,
272
+ refreshAllVisibleTitles,
187
273
  forgetSession,
188
274
  };
189
275
  }
@@ -5,5 +5,9 @@ export declare function createTitleRefreshScheduler(options: {
5
5
  schedule: (sessionID: string, delay?: number) => void;
6
6
  apply: (sessionID: string) => Promise<void>;
7
7
  cancel: (sessionID: string) => void;
8
+ cancelAll: () => void;
9
+ flushScheduled: () => Promise<void>;
10
+ waitForIdle: (timeoutMs?: number) => Promise<void>;
11
+ waitForQuiescence: (budgetMs?: number) => Promise<void>;
8
12
  dispose: () => void;
9
13
  };
@@ -31,16 +31,50 @@ export function createTitleRefreshScheduler(options) {
31
31
  clearTimeout(timer);
32
32
  refreshTimer.delete(sessionID);
33
33
  };
34
- const dispose = () => {
34
+ const cancelAll = () => {
35
35
  for (const timer of refreshTimer.values())
36
36
  clearTimeout(timer);
37
37
  refreshTimer.clear();
38
+ };
39
+ const flushScheduled = async () => {
40
+ const pending = Array.from(refreshTimer.keys());
41
+ cancelAll();
42
+ await Promise.allSettled(pending.map((sessionID) => applyLocked(sessionID)));
43
+ };
44
+ const waitForIdle = async (timeoutMs) => {
45
+ const inflight = Array.from(applyLocks.values());
46
+ if (inflight.length === 0)
47
+ return;
48
+ if (timeoutMs === undefined) {
49
+ await Promise.allSettled(inflight);
50
+ return;
51
+ }
52
+ await Promise.race([
53
+ Promise.allSettled(inflight),
54
+ new Promise((resolve) => setTimeout(resolve, timeoutMs)),
55
+ ]);
56
+ };
57
+ const waitForQuiescence = async (budgetMs = 10_000) => {
58
+ const deadline = Date.now() + budgetMs;
59
+ while (Date.now() < deadline) {
60
+ await flushScheduled();
61
+ await waitForIdle(Math.max(0, deadline - Date.now()));
62
+ if (refreshTimer.size === 0 && applyLocks.size === 0)
63
+ return;
64
+ }
65
+ };
66
+ const dispose = () => {
67
+ cancelAll();
38
68
  applyLocks.clear();
39
69
  };
40
70
  return {
41
71
  schedule,
42
72
  apply: applyLocked,
43
73
  cancel,
74
+ cancelAll,
75
+ flushScheduled,
76
+ waitForIdle,
77
+ waitForQuiescence,
44
78
  dispose,
45
79
  };
46
80
  }
package/dist/tools.d.ts CHANGED
@@ -4,8 +4,28 @@ export declare function createQuotaSidebarTools(deps: {
4
4
  getTitleEnabled: () => boolean;
5
5
  setTitleEnabled: (enabled: boolean) => void;
6
6
  scheduleSave: () => void;
7
+ flushSave: () => Promise<void>;
8
+ waitForStartupTitleWork: () => Promise<void>;
7
9
  refreshSessionTitle: (sessionID: string, delay?: number) => void;
8
- restoreAllVisibleTitles: () => Promise<void>;
10
+ cancelAllTitleRefreshes: () => void;
11
+ flushScheduledTitleRefreshes: () => Promise<void>;
12
+ waitForTitleRefreshIdle: () => Promise<void>;
13
+ waitForTitleRefreshQuiescence: () => Promise<void>;
14
+ restoreAllVisibleTitles: () => Promise<{
15
+ attempted: number;
16
+ restored: number;
17
+ listFailed: boolean;
18
+ }>;
19
+ refreshAllTouchedTitles: () => Promise<{
20
+ attempted: number;
21
+ refreshed: number;
22
+ listFailed: boolean;
23
+ }>;
24
+ refreshAllVisibleTitles: () => Promise<{
25
+ attempted: number;
26
+ refreshed: number;
27
+ listFailed: boolean;
28
+ }>;
9
29
  showToast: (period: 'session' | 'day' | 'week' | 'month' | 'toggle', message: string) => Promise<void>;
10
30
  summarizeForTool: (period: 'session' | 'day' | 'week' | 'month', sessionID: string, includeChildren: boolean) => Promise<UsageSummary>;
11
31
  getQuotaSnapshots: (providerIDs: string[], options?: {
@@ -24,6 +44,7 @@ export declare function createQuotaSidebarTools(deps: {
24
44
  width: number;
25
45
  includeChildren: boolean;
26
46
  };
47
+ sidebarEnabled: boolean;
27
48
  };
28
49
  }): {
29
50
  quota_summary: {
package/dist/tools.js CHANGED
@@ -1,6 +1,14 @@
1
1
  import { tool } from '@opencode-ai/plugin/tool';
2
2
  const z = tool.schema;
3
3
  export function createQuotaSidebarTools(deps) {
4
+ let toggleLock = Promise.resolve();
5
+ const waitForStartupTitleWork = async () => {
6
+ const timedOut = await Promise.race([
7
+ deps.waitForStartupTitleWork(),
8
+ new Promise((resolve) => setTimeout(() => resolve('timeout'), 3_000)),
9
+ ]);
10
+ return timedOut === 'timeout';
11
+ };
4
12
  return {
5
13
  quota_summary: tool({
6
14
  description: 'Show usage and quota summary for session/day/week/month.',
@@ -18,7 +26,6 @@ export function createQuotaSidebarTools(deps) {
18
26
  ? (args.includeChildren ?? deps.config.sidebar.includeChildren)
19
27
  : false;
20
28
  const usage = await deps.summarizeForTool(period, context.sessionID, includeChildren);
21
- deps.scheduleSave();
22
29
  // For quota_summary, always show all subscription quota balances,
23
30
  // regardless of which providers were used in the session.
24
31
  const quotas = await deps.getQuotaSnapshots([], { allowDefault: true });
@@ -43,20 +50,59 @@ export function createQuotaSidebarTools(deps) {
43
50
  .describe('Explicit on/off. Omit to toggle current state.'),
44
51
  },
45
52
  execute: async (args, context) => {
46
- const current = deps.getTitleEnabled();
47
- const next = args.enabled !== undefined ? args.enabled : !current;
48
- deps.setTitleEnabled(next);
49
- deps.scheduleSave();
50
- if (next) {
51
- // Turning on re-render current session immediately
53
+ const run = async () => {
54
+ const current = deps.getTitleEnabled();
55
+ const next = args.enabled !== undefined ? args.enabled : !current;
56
+ if (next) {
57
+ if (!deps.config.sidebarEnabled) {
58
+ return 'Sidebar usage display cannot be enabled because `sidebar.enabled=false` in config. Re-enable the sidebar feature first.';
59
+ }
60
+ const startupTimedOut = await waitForStartupTitleWork();
61
+ deps.setTitleEnabled(true);
62
+ deps.scheduleSave();
63
+ await deps.flushSave();
64
+ const visible = await deps.refreshAllVisibleTitles();
65
+ const touched = await deps.refreshAllTouchedTitles();
66
+ deps.refreshSessionTitle(context.sessionID, 0);
67
+ if (startupTimedOut) {
68
+ void deps.waitForStartupTitleWork().then(() => {
69
+ if (!deps.getTitleEnabled())
70
+ return;
71
+ void deps.refreshAllVisibleTitles();
72
+ void deps.refreshAllTouchedTitles();
73
+ deps.refreshSessionTitle(context.sessionID, 0);
74
+ });
75
+ }
76
+ await deps.showToast('toggle', 'Sidebar usage display: ON');
77
+ if (visible.listFailed ||
78
+ visible.refreshed < visible.attempted ||
79
+ touched.refreshed < touched.attempted) {
80
+ return 'Sidebar usage display is now ON. Visible-session refresh failed, so only touched/current session titles are guaranteed to refresh immediately.';
81
+ }
82
+ return 'Sidebar usage display is now ON. Visible session titles are refreshing to show token usage and quota.';
83
+ }
84
+ deps.setTitleEnabled(false);
85
+ deps.scheduleSave();
86
+ await deps.flushSave();
87
+ deps.cancelAllTitleRefreshes();
88
+ await deps.waitForTitleRefreshQuiescence();
89
+ const restore = await deps.restoreAllVisibleTitles();
90
+ if (restore.restored === restore.attempted) {
91
+ await deps.showToast('toggle', 'Sidebar usage display: OFF');
92
+ return 'Sidebar usage display is now OFF. Touched session titles were restored to base titles.';
93
+ }
94
+ deps.setTitleEnabled(true);
95
+ deps.scheduleSave();
96
+ await deps.flushSave();
97
+ await deps.refreshAllVisibleTitles();
98
+ await deps.refreshAllTouchedTitles();
52
99
  deps.refreshSessionTitle(context.sessionID, 0);
53
- await deps.showToast('toggle', 'Sidebar usage display: ON');
54
- return 'Sidebar usage display is now ON. Session titles will show token usage and quota.';
55
- }
56
- // Turning off restore all touched sessions to base titles
57
- await deps.restoreAllVisibleTitles();
58
- await deps.showToast('toggle', 'Sidebar usage display: OFF');
59
- return 'Sidebar usage display is now OFF. Session titles restored to original.';
100
+ await deps.showToast('toggle', 'Sidebar usage display: OFF failed');
101
+ return 'Sidebar usage display remains ON because some touched session titles could not be restored. Try again after the session service recovers.';
102
+ };
103
+ const pending = toggleLock.then(run, run);
104
+ toggleLock = pending.then(() => undefined, () => undefined);
105
+ return pending;
60
106
  },
61
107
  }),
62
108
  };
package/dist/types.d.ts CHANGED
@@ -47,6 +47,20 @@ export type CacheUsageBuckets = {
47
47
  readOnly: CacheUsageBucket;
48
48
  readWrite: CacheUsageBucket;
49
49
  };
50
+ /**
51
+ * Derived cache coverage metrics.
52
+ *
53
+ * - `cacheCoverage`: fraction of prompt surface covered by read-write cache
54
+ * (`(cacheRead + cacheWrite) / (input + cacheRead + cacheWrite)`).
55
+ * Only defined when the read-write bucket has traffic.
56
+ * - `cacheReadCoverage`: fraction of prompt surface served from read-only cache
57
+ * (`cacheRead / (input + cacheRead)`).
58
+ * Only defined when the read-only bucket has traffic.
59
+ */
60
+ export type CacheCoverageMetrics = {
61
+ cacheCoverage: number | undefined;
62
+ cacheReadCoverage: number | undefined;
63
+ };
50
64
  export type CachedProviderUsage = {
51
65
  input: number;
52
66
  output: number;
@@ -58,6 +72,8 @@ export type CachedProviderUsage = {
58
72
  /** Equivalent API billing cost (USD) computed from model pricing. */
59
73
  apiCost: number;
60
74
  assistantMessages: number;
75
+ /** Provider-level cache coverage buckets grouped by model cache behavior. */
76
+ cacheBuckets?: CacheUsageBuckets;
61
77
  };
62
78
  export type CachedSessionUsage = {
63
79
  /** Billing aggregation cache version for cost/apiCost refresh migrations. */
@@ -72,7 +88,13 @@ export type CachedSessionUsage = {
72
88
  /** Equivalent API billing cost (USD) computed from model pricing. */
73
89
  apiCost: number;
74
90
  assistantMessages: number;
75
- /** Cache coverage buckets grouped by model cache behavior. */
91
+ /**
92
+ * Cache coverage buckets grouped by model cache behavior.
93
+ *
94
+ * `undefined` when no cache-capable models were used or data predates
95
+ * billingVersion 3. The fallback in `resolvedCacheUsageBuckets()` derives
96
+ * approximate buckets from top-level `cacheRead`/`cacheWrite` when missing.
97
+ */
76
98
  cacheBuckets?: CacheUsageBuckets;
77
99
  providers: Record<string, CachedProviderUsage>;
78
100
  };
@@ -90,6 +112,8 @@ export type SessionState = SessionTitleState & {
90
112
  /** Parent session ID for subagent child sessions. */
91
113
  parentID?: string;
92
114
  usage?: CachedSessionUsage;
115
+ /** Persisted dirtiness flag so descendant aggregation survives restart. */
116
+ dirty?: boolean;
93
117
  /** Incremental aggregation cursor (P1). */
94
118
  cursor?: IncrementalCursor;
95
119
  };
@@ -104,6 +128,8 @@ export type QuotaSidebarState = {
104
128
  titleEnabled: boolean;
105
129
  sessionDateMap: Record<string, string>;
106
130
  sessions: Record<string, SessionState>;
131
+ /** Tombstones for sessions deleted from memory but not yet purged from day chunks. */
132
+ deletedSessionDateMap: Record<string, string>;
107
133
  quotaCache: Record<string, QuotaSnapshot>;
108
134
  };
109
135
  export type QuotaSidebarConfig = {
package/dist/usage.d.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import type { AssistantMessage, Message } from '@opencode-ai/sdk';
2
- import type { CacheCoverageMode, CacheUsageBuckets, CachedSessionUsage, IncrementalCursor } from './types.js';
3
- export declare const USAGE_BILLING_CACHE_VERSION = 3;
2
+ import type { CacheCoverageMetrics, CacheCoverageMode, CacheUsageBuckets, CachedSessionUsage, IncrementalCursor } from './types.js';
3
+ /**
4
+ * Billing cache version — bump this whenever the persisted `CachedSessionUsage`
5
+ * shape changes in a way that requires recomputation (e.g. new aggregate
6
+ * fields). This is distinct from the plugin *state* version managed by the
7
+ * persistence layer; billing version only governs usage-cache staleness.
8
+ */
9
+ export declare const USAGE_BILLING_CACHE_VERSION = 4;
4
10
  export type ProviderUsage = {
5
11
  providerID: string;
6
12
  input: number;
@@ -13,6 +19,7 @@ export type ProviderUsage = {
13
19
  cost: number;
14
20
  apiCost: number;
15
21
  assistantMessages: number;
22
+ cacheBuckets?: CacheUsageBuckets;
16
23
  };
17
24
  export type UsageSummary = {
18
25
  input: number;
@@ -35,14 +42,15 @@ export type UsageOptions = {
35
42
  /** Cache-behavior classifier for the message model/provider. */
36
43
  classifyCacheMode?: (message: AssistantMessage) => CacheCoverageMode;
37
44
  };
38
- export declare function getCacheCoverageMetrics(usage: Pick<UsageSummary, 'input' | 'cacheRead' | 'cacheWrite' | 'assistantMessages' | 'cacheBuckets'>): {
39
- cacheCoverage: number | undefined;
40
- cacheReadCoverage: number | undefined;
41
- };
45
+ export declare function getCacheCoverageMetrics(usage: Pick<UsageSummary, 'input' | 'cacheRead' | 'cacheWrite' | 'assistantMessages' | 'cacheBuckets'>): CacheCoverageMetrics;
46
+ export declare function getProviderCacheCoverageMetrics(usage: Pick<ProviderUsage, 'input' | 'cacheRead' | 'cacheWrite' | 'assistantMessages' | 'cacheBuckets'>): CacheCoverageMetrics;
42
47
  export declare function emptyUsageSummary(): UsageSummary;
43
48
  export declare function summarizeMessages(entries: Array<{
44
49
  info: Message;
45
50
  }>, startAt?: number, sessionCount?: number, options?: UsageOptions): UsageSummary;
51
+ export declare function summarizeMessagesInCompletedRange(entries: Array<{
52
+ info: Message;
53
+ }>, startAt: number, endAt: number, sessionCount?: number, options?: UsageOptions): UsageSummary;
46
54
  /**
47
55
  * P1: Incremental usage aggregation.
48
56
  * Only processes messages newer than the cursor. Returns updated cursor.