sidekick-shared 0.24.4 → 0.25.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +200 -52
  3. package/dist/accountChangeSignal.d.ts +3 -0
  4. package/dist/accountChangeSignal.js +19 -0
  5. package/dist/accountChanges.d.ts +21 -0
  6. package/dist/accountChanges.js +151 -0
  7. package/dist/accountManager.d.ts +13 -0
  8. package/dist/accountManager.js +60 -32
  9. package/dist/accountRegistry.d.ts +3 -1
  10. package/dist/accountRegistry.js +64 -41
  11. package/dist/accounts.js +40 -19
  12. package/dist/autoSwitch.d.ts +2 -1
  13. package/dist/autoSwitch.js +32 -7
  14. package/dist/browser.d.ts +10 -3
  15. package/dist/browser.js +17 -1
  16. package/dist/codexProfiles.d.ts +14 -0
  17. package/dist/codexProfiles.js +268 -116
  18. package/dist/codexQuotaWatcher.d.ts +6 -0
  19. package/dist/codexQuotaWatcher.js +101 -47
  20. package/dist/context/sessionContext.js +2 -4
  21. package/dist/ensureDefaultAccounts.js +4 -4
  22. package/dist/index.d.ts +32 -15
  23. package/dist/index.js +54 -9
  24. package/dist/modelAliases.d.ts +13 -0
  25. package/dist/modelAliases.js +58 -0
  26. package/dist/modelCatalog.d.ts +32 -0
  27. package/dist/modelCatalog.js +145 -0
  28. package/dist/modelContext.d.ts +36 -3
  29. package/dist/modelContext.js +109 -34
  30. package/dist/modelInfo.d.ts +30 -1
  31. package/dist/modelInfo.js +116 -26
  32. package/dist/multiProviderQuotaService.d.ts +8 -0
  33. package/dist/multiProviderQuotaService.js +64 -3
  34. package/dist/node.d.ts +8 -2
  35. package/dist/node.js +11 -1
  36. package/dist/observedContextWindows.d.ts +13 -0
  37. package/dist/observedContextWindows.js +13 -0
  38. package/dist/observedSessionCollector.d.ts +75 -4
  39. package/dist/observedSessionCollector.js +452 -42
  40. package/dist/parsers/codexHistory.d.ts +32 -0
  41. package/dist/parsers/codexHistory.js +125 -0
  42. package/dist/parsers/mcpToolName.d.ts +26 -0
  43. package/dist/parsers/mcpToolName.js +31 -0
  44. package/dist/parsers/sessionActivityDetector.d.ts +6 -0
  45. package/dist/parsers/sessionActivityDetector.js +17 -0
  46. package/dist/parsers/sessionPathResolver.d.ts +11 -4
  47. package/dist/parsers/sessionPathResolver.js +11 -4
  48. package/dist/paths.d.ts +7 -3
  49. package/dist/paths.js +7 -3
  50. package/dist/providerIds.d.ts +11 -0
  51. package/dist/providerIds.js +9 -0
  52. package/dist/providerQuota.d.ts +3 -1
  53. package/dist/providerQuota.js +3 -0
  54. package/dist/providers/claudeCode.d.ts +11 -1
  55. package/dist/providers/claudeCode.js +222 -39
  56. package/dist/providers/codex.d.ts +39 -1
  57. package/dist/providers/codex.js +326 -39
  58. package/dist/providers/codexDatabase.d.ts +9 -0
  59. package/dist/providers/codexDatabase.js +107 -2
  60. package/dist/providers/diagnostics.d.ts +13 -0
  61. package/dist/providers/diagnostics.js +88 -0
  62. package/dist/providers/factory.d.ts +14 -0
  63. package/dist/providers/factory.js +57 -0
  64. package/dist/providers/openCode.d.ts +20 -1
  65. package/dist/providers/openCode.js +361 -11
  66. package/dist/providers/openCodeDatabase.d.ts +10 -0
  67. package/dist/providers/openCodeDatabase.js +69 -0
  68. package/dist/providers/types.d.ts +57 -2
  69. package/dist/providers/types.js +4 -0
  70. package/dist/quota.d.ts +4 -1
  71. package/dist/quota.js +3 -7
  72. package/dist/quotaHistory.d.ts +3 -2
  73. package/dist/quotaHistory.js +2 -2
  74. package/dist/quotaPoller.d.ts +13 -1
  75. package/dist/quotaPoller.js +65 -3
  76. package/dist/quotaSnapshots.js +8 -77
  77. package/dist/schemas/accountManager.d.ts +2 -2
  78. package/dist/schemas/accountManager.js +2 -4
  79. package/dist/schemas/accountStatus.d.ts +3 -0
  80. package/dist/schemas/accountStatus.js +1 -0
  81. package/dist/schemas/index.d.ts +1 -0
  82. package/dist/schemas/index.js +7 -2
  83. package/dist/schemas/observedSessionV1.d.ts +27 -26
  84. package/dist/schemas/observedSessionV1.js +6 -4
  85. package/dist/schemas/quota.d.ts +2 -2
  86. package/dist/schemas/quota.js +3 -6
  87. package/dist/schemas/quotaHistory.d.ts +2 -2
  88. package/dist/schemas/tokenEstimation.d.ts +1 -1
  89. package/dist/sessionMonitor.d.ts +15 -0
  90. package/dist/sessionMonitor.js +142 -0
  91. package/dist/sessionPreviews.d.ts +85 -0
  92. package/dist/sessionPreviews.js +549 -0
  93. package/dist/sessionTranscripts.js +10 -4
  94. package/dist/types/observedSessionV1.d.ts +6 -2
  95. package/dist/types/observedSessionV1.js +14 -8
  96. package/dist/writers/atomic.d.ts +20 -0
  97. package/dist/writers/atomic.js +190 -19
  98. package/package.json +22 -1
@@ -41,9 +41,15 @@ exports.CodexDatabase = void 0;
41
41
  const fs = __importStar(require("fs"));
42
42
  const path = __importStar(require("path"));
43
43
  const child_process_1 = require("child_process");
44
+ /**
45
+ * Ids per inlined `IN (…)` query. Values are passed inside a single argv
46
+ * element to sqlite3, so an unbounded list would hit E2BIG on large stores.
47
+ */
48
+ const SQL_ID_CHUNK_SIZE = 400;
44
49
  class CodexDatabase {
45
50
  dbPath;
46
51
  sqlite3Available = null;
52
+ runtimeStatus = null;
47
53
  constructor(codexHome) {
48
54
  this.dbPath = findLatestStateDatabase(codexHome) ?? path.join(codexHome, 'state.sqlite');
49
55
  }
@@ -58,6 +64,11 @@ class CodexDatabase {
58
64
  open() {
59
65
  if (this.sqlite3Available !== null)
60
66
  return this.sqlite3Available;
67
+ if (!this.isAvailable()) {
68
+ this.sqlite3Available = false;
69
+ this.runtimeStatus = { available: false, kind: 'db_missing' };
70
+ return false;
71
+ }
61
72
  try {
62
73
  (0, child_process_1.execFileSync)('sqlite3', ['--version'], {
63
74
  encoding: 'utf-8',
@@ -66,13 +77,26 @@ class CodexDatabase {
66
77
  stdio: ['pipe', 'pipe', 'pipe'],
67
78
  });
68
79
  this.sqlite3Available = true;
80
+ this.runtimeStatus = { available: true, kind: 'available' };
69
81
  return true;
70
82
  }
71
- catch {
83
+ catch (error) {
72
84
  this.sqlite3Available = false;
85
+ this.runtimeStatus = classifySqliteError(error);
73
86
  return false;
74
87
  }
75
88
  }
89
+ getRuntimeStatus() {
90
+ if (this.runtimeStatus)
91
+ return this.runtimeStatus;
92
+ if (!this.isAvailable())
93
+ return { available: false, kind: 'db_missing' };
94
+ return {
95
+ available: false,
96
+ kind: 'query_failed',
97
+ message: 'Codex database has not been initialized yet.',
98
+ };
99
+ }
76
100
  close() { }
77
101
  query(sql, params = []) {
78
102
  if (!this.sqlite3Available)
@@ -103,7 +127,34 @@ class CodexDatabase {
103
127
  return [];
104
128
  return JSON.parse(trimmed);
105
129
  }
106
- catch {
130
+ catch (error) {
131
+ this.runtimeStatus = {
132
+ ...classifySqliteError(error, 'query_failed'),
133
+ available: this.sqlite3Available === true,
134
+ };
135
+ return [];
136
+ }
137
+ }
138
+ async queryAsync(sql, params = []) {
139
+ if (!this.isAvailable()) {
140
+ this.runtimeStatus = { available: false, kind: 'db_missing' };
141
+ return [];
142
+ }
143
+ const query = bindSqlParams(sql, params);
144
+ try {
145
+ const result = await execSqlite(this.dbPath, query, 10 * 1024 * 1024);
146
+ this.sqlite3Available = true;
147
+ this.runtimeStatus = { available: true, kind: 'available' };
148
+ const trimmed = result.trim();
149
+ return trimmed ? JSON.parse(trimmed) : [];
150
+ }
151
+ catch (error) {
152
+ const status = classifySqliteError(error, 'query_failed');
153
+ // Only a missing sqlite3 binary gates future attempts. A failed query
154
+ // is transient and must not latch the database off (mirrors query()).
155
+ if (status.kind === 'sqlite_missing')
156
+ this.sqlite3Available = false;
157
+ this.runtimeStatus = status;
107
158
  return [];
108
159
  }
109
160
  }
@@ -132,6 +183,19 @@ class CodexDatabase {
132
183
  getThread(id) {
133
184
  return this.queryOne('SELECT * FROM threads WHERE id = ?', [id]);
134
185
  }
186
+ /**
187
+ * Resolve many thread ids with non-blocking sqlite3 subprocesses — one per
188
+ * chunk of ids, so the inlined query stays clear of argv length limits.
189
+ */
190
+ async getThreadsByIdsAsync(ids) {
191
+ const results = [];
192
+ for (let i = 0; i < ids.length; i += SQL_ID_CHUNK_SIZE) {
193
+ const chunk = ids.slice(i, i + SQL_ID_CHUNK_SIZE);
194
+ const placeholders = chunk.map(() => '?').join(', ');
195
+ results.push(...(await this.queryAsync(`SELECT * FROM threads WHERE id IN (${placeholders})`, [...chunk])));
196
+ }
197
+ return results;
198
+ }
135
199
  /** Get all threads forked from a given session ID. */
136
200
  getThreadsByForkedFromId(parentId) {
137
201
  return this.query('SELECT * FROM threads WHERE forked_from_id = ? ORDER BY created_at ASC', [parentId]);
@@ -147,6 +211,47 @@ class CodexDatabase {
147
211
  }
148
212
  }
149
213
  exports.CodexDatabase = CodexDatabase;
214
+ function classifySqliteError(error, fallback = 'sqlite_blocked') {
215
+ const code = error?.code;
216
+ if (code === 'ENOENT') {
217
+ return {
218
+ available: false,
219
+ kind: 'sqlite_missing',
220
+ message: 'The sqlite3 executable was not found.',
221
+ };
222
+ }
223
+ return {
224
+ available: false,
225
+ kind: fallback,
226
+ message: error instanceof Error ? error.message : 'sqlite3 could not be executed.',
227
+ };
228
+ }
229
+ function bindSqlParams(sql, params) {
230
+ let paramIndex = 0;
231
+ return sql.replace(/\?/g, () => {
232
+ if (paramIndex >= params.length)
233
+ return '?';
234
+ const param = params[paramIndex++];
235
+ if (typeof param === 'number')
236
+ return Number.isFinite(param) ? String(param) : '0';
237
+ return `'${String(param).replace(/'/g, "''")}'`;
238
+ });
239
+ }
240
+ function execSqlite(dbPath, query, maxBuffer) {
241
+ return new Promise((resolve, reject) => {
242
+ (0, child_process_1.execFile)('sqlite3', ['-json', '-readonly', dbPath, query], {
243
+ encoding: 'utf8',
244
+ timeout: 4_000,
245
+ killSignal: 'SIGKILL',
246
+ maxBuffer,
247
+ }, (error, stdout) => {
248
+ if (error)
249
+ reject(error);
250
+ else
251
+ resolve(stdout);
252
+ });
253
+ });
254
+ }
150
255
  function findLatestStateDatabase(codexHome) {
151
256
  try {
152
257
  const entries = fs.readdirSync(codexHome, { withFileTypes: true });
@@ -0,0 +1,13 @@
1
+ import type { ProviderId, ProviderOperationStatus, ProviderRuntimeStatus, SessionProviderDiagnostic, SessionProviderOptions } from './types';
2
+ /** Small per-provider diagnostic state; it never stores transcript or environment contents. */
3
+ export declare class ProviderDiagnosticTracker {
4
+ private readonly providerId;
5
+ private readonly options;
6
+ private lastEmittedSignature;
7
+ private lastStatus;
8
+ constructor(providerId: ProviderId, options?: SessionProviderOptions);
9
+ available(operation: string): void;
10
+ degraded(operation: string, runtimeStatus: ProviderRuntimeStatus, diagnostic: Omit<SessionProviderDiagnostic, 'providerId'>, usable?: boolean): void;
11
+ getLastOperationStatus(): ProviderOperationStatus;
12
+ }
13
+ export declare function diagnosticFromRuntimeStatus(providerId: ProviderId, phase: SessionProviderDiagnostic['phase'], status: ProviderRuntimeStatus): SessionProviderDiagnostic | null;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProviderDiagnosticTracker = void 0;
4
+ exports.diagnosticFromRuntimeStatus = diagnosticFromRuntimeStatus;
5
+ const AVAILABLE_STATUS = { available: true, kind: 'available' };
6
+ /** Small per-provider diagnostic state; it never stores transcript or environment contents. */
7
+ class ProviderDiagnosticTracker {
8
+ providerId;
9
+ options;
10
+ lastEmittedSignature = null;
11
+ lastStatus = {
12
+ operation: 'construct',
13
+ usable: true,
14
+ degraded: false,
15
+ runtimeStatus: AVAILABLE_STATUS,
16
+ diagnostics: [],
17
+ };
18
+ constructor(providerId, options = {}) {
19
+ this.providerId = providerId;
20
+ this.options = options;
21
+ }
22
+ available(operation) {
23
+ this.lastEmittedSignature = null;
24
+ this.lastStatus = {
25
+ operation,
26
+ usable: true,
27
+ degraded: false,
28
+ runtimeStatus: AVAILABLE_STATUS,
29
+ diagnostics: [],
30
+ };
31
+ }
32
+ degraded(operation, runtimeStatus, diagnostic, usable = true) {
33
+ const value = { providerId: this.providerId, ...diagnostic };
34
+ this.lastStatus = {
35
+ operation,
36
+ usable,
37
+ degraded: true,
38
+ runtimeStatus,
39
+ diagnostics: [value],
40
+ };
41
+ const signature = `${value.kind}\0${value.severity}\0${value.phase}\0${value.message}`;
42
+ if (signature === this.lastEmittedSignature)
43
+ return;
44
+ this.lastEmittedSignature = signature;
45
+ try {
46
+ this.options.onDiagnostic?.(value);
47
+ }
48
+ catch {
49
+ // Diagnostics are observational and cannot break provider operations.
50
+ }
51
+ }
52
+ getLastOperationStatus() {
53
+ return {
54
+ ...this.lastStatus,
55
+ runtimeStatus: { ...this.lastStatus.runtimeStatus },
56
+ diagnostics: [...this.lastStatus.diagnostics],
57
+ };
58
+ }
59
+ }
60
+ exports.ProviderDiagnosticTracker = ProviderDiagnosticTracker;
61
+ function diagnosticFromRuntimeStatus(providerId, phase, status) {
62
+ if (status.kind === 'available')
63
+ return null;
64
+ const kind = status.kind;
65
+ return {
66
+ providerId,
67
+ kind,
68
+ severity: status.kind === 'query_failed' ? 'error' : 'warning',
69
+ phase,
70
+ message: status.message ?? defaultRuntimeMessage(providerId, status.kind),
71
+ };
72
+ }
73
+ function defaultRuntimeMessage(providerId, kind) {
74
+ switch (kind) {
75
+ case 'home_unavailable':
76
+ return `${providerId} data home is unavailable.`;
77
+ case 'config_unreadable':
78
+ return `${providerId} configuration is unreadable.`;
79
+ case 'db_missing':
80
+ return `${providerId} database is unavailable; file fallback is active.`;
81
+ case 'sqlite_missing':
82
+ return 'The sqlite3 executable is unavailable; file fallback is active.';
83
+ case 'sqlite_blocked':
84
+ return 'The sqlite3 executable could not be started; file fallback is active.';
85
+ case 'query_failed':
86
+ return `${providerId} database query failed; fallback data may be incomplete.`;
87
+ }
88
+ }
@@ -0,0 +1,14 @@
1
+ import type { ProviderId, SessionProviderBase, SessionProviderDiagnostic } from './types';
2
+ export interface CreateSessionProvidersOptions {
3
+ providerIds?: readonly ProviderId[];
4
+ onDiagnostic?: (diagnostic: SessionProviderDiagnostic) => void;
5
+ }
6
+ export interface CreateSessionProvidersResult {
7
+ providers: SessionProviderBase[];
8
+ diagnostics: SessionProviderDiagnostic[];
9
+ }
10
+ /**
11
+ * Constructs built-in providers without probing the filesystem or binaries.
12
+ * A broken provider is isolated and reported instead of aborting host startup.
13
+ */
14
+ export declare function createSessionProviders(options?: CreateSessionProvidersOptions): CreateSessionProvidersResult;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSessionProviders = createSessionProviders;
4
+ const claudeCode_1 = require("./claudeCode");
5
+ const codex_1 = require("./codex");
6
+ const openCode_1 = require("./openCode");
7
+ const types_1 = require("./types");
8
+ /**
9
+ * Constructs built-in providers without probing the filesystem or binaries.
10
+ * A broken provider is isolated and reported instead of aborting host startup.
11
+ */
12
+ function createSessionProviders(options = {}) {
13
+ const diagnostics = [];
14
+ const diagnosticIndexes = new Map();
15
+ const providers = [];
16
+ const selected = new Set(options.providerIds ?? types_1.SESSION_PROVIDER_IDS);
17
+ const emit = (diagnostic) => {
18
+ const key = `${diagnostic.providerId}\0${diagnostic.kind}\0${diagnostic.phase}`;
19
+ const existingIndex = diagnosticIndexes.get(key);
20
+ if (existingIndex === undefined) {
21
+ diagnosticIndexes.set(key, diagnostics.length);
22
+ diagnostics.push(diagnostic);
23
+ }
24
+ else {
25
+ diagnostics[existingIndex] = diagnostic;
26
+ }
27
+ try {
28
+ options.onDiagnostic?.(diagnostic);
29
+ }
30
+ catch {
31
+ // A host diagnostic callback cannot break provider setup.
32
+ }
33
+ };
34
+ for (const providerId of types_1.SESSION_PROVIDER_IDS) {
35
+ if (!selected.has(providerId))
36
+ continue;
37
+ try {
38
+ const providerOptions = { onDiagnostic: emit };
39
+ providers.push(providerId === 'claude-code'
40
+ ? new claudeCode_1.ClaudeCodeProvider(providerOptions)
41
+ : providerId === 'opencode'
42
+ ? new openCode_1.OpenCodeProvider(providerOptions)
43
+ : new codex_1.CodexProvider(providerOptions));
44
+ }
45
+ catch {
46
+ const diagnostic = {
47
+ providerId,
48
+ kind: 'construction_failed',
49
+ severity: 'error',
50
+ phase: 'construct',
51
+ message: `${providerId} could not be constructed and was omitted.`,
52
+ };
53
+ emit(diagnostic);
54
+ }
55
+ }
56
+ return { providers, diagnostics };
57
+ }
@@ -18,7 +18,7 @@
18
18
  import type { ReadSessionContextSnapshotOptions, SessionContextSnapshot } from '../context/sessionContext';
19
19
  import type { ZaiTier } from '../zaiQuota';
20
20
  import type { QuotaState } from '../quota';
21
- import type { SessionProviderBase, SessionReader, ProjectFolderInfo, SearchHit, SessionFileStats, ProviderId, ProviderRuntimeStatus } from './types';
21
+ import type { SessionProviderBase, SessionReader, ProjectFolderInfo, SearchHit, SessionFileStats, ProviderId, ProviderRuntimeStatus, ProviderOperationStatus, SessionFileInfo, SessionProviderOptions } from './types';
22
22
  import type { TokenUsage, SubagentStats, ContextAttribution } from '../types/sessionEvent';
23
23
  /**
24
24
  * Gets the OpenCode data directory.
@@ -42,13 +42,23 @@ export declare class OpenCodeProvider implements SessionProviderBase {
42
42
  private db;
43
43
  private dbInitialized;
44
44
  private dbStatus;
45
+ private readonly diagnostics;
45
46
  /** Cache of session metadata populated during listing */
46
47
  private sessionMetaCache;
47
48
  /** Runtime-reported context window limit (overrides static map when set). */
48
49
  private dynamicContextWindowLimit;
50
+ constructor(options?: SessionProviderOptions);
49
51
  /** Lazy-initialize the database connection. */
50
52
  private ensureDb;
53
+ private ensureDbAsync;
54
+ /**
55
+ * Cache an async-created database only after an operation proved it usable.
56
+ * Sync paths treat a cached `db` as "open() succeeded", so adopting an
57
+ * unproven instance would cut them off from the file-storage fallback.
58
+ */
59
+ private adoptDbIfAvailable;
51
60
  getRuntimeStatus(): ProviderRuntimeStatus;
61
+ getLastOperationStatus(): ProviderOperationStatus;
52
62
  canMonitorDirectory(dir: string): boolean;
53
63
  private mapDbSessions;
54
64
  getSessionDirectory(workspacePath: string): string;
@@ -58,14 +68,18 @@ export declare class OpenCodeProvider implements SessionProviderBase {
58
68
  /** Backward-compatible alias for findAllSessions. */
59
69
  findSessionFiles(workspacePath: string): string[];
60
70
  findAllSessions(workspacePath: string): string[];
71
+ findSessionById(workspacePath: string, sessionId: string): string | null;
61
72
  findSessionsInDirectory(dir: string): string[];
62
73
  private findSessionsInDirectoryFromFiles;
74
+ listAllSessionFiles(): SessionFileInfo[];
75
+ listSessionFilesAsync(workspacePath?: string): Promise<SessionFileInfo[]>;
63
76
  getAllProjectFolders(workspacePath?: string): ProjectFolderInfo[];
64
77
  private getAllProjectFoldersFromFiles;
65
78
  isSessionFile(filename: string): boolean;
66
79
  getSessionId(sessionPath: string): string;
67
80
  encodeWorkspacePath(workspacePath: string): string;
68
81
  extractSessionLabel(sessionPath: string): string | null;
82
+ extractSessionLabelsAsync(sessionPaths: readonly string[]): Promise<Map<string, string | null>>;
69
83
  private extractSessionLabelFromFiles;
70
84
  createReader(sessionPath: string): SessionReader;
71
85
  scanSubagents(_sessionDir: string, sessionId: string): SubagentStats[];
@@ -73,11 +87,15 @@ export declare class OpenCodeProvider implements SessionProviderBase {
73
87
  private searchInSessionFromDb;
74
88
  private searchInSessionFromFiles;
75
89
  getProjectsBaseDir(): string;
90
+ getWatchRoots(): string[];
76
91
  readSessionStats(sessionPath: string): SessionFileStats;
77
92
  readSessionContextSnapshot(sessionPath: string, options?: ReadSessionContextSnapshotOptions): SessionContextSnapshot;
78
93
  getSessionMetadata(sessionPath: string): {
79
94
  mtime: Date;
80
95
  } | null;
96
+ getSessionMetadataAsync(sessionPath: string): Promise<{
97
+ mtime: Date;
98
+ } | null>;
81
99
  /**
82
100
  * Returns the latest assistant message's token snapshot for context window sizing.
83
101
  * Cumulative session totals are derived from readNew() events processed by SessionMonitor.
@@ -89,6 +107,7 @@ export declare class OpenCodeProvider implements SessionProviderBase {
89
107
  /** Set a runtime-reported context window limit (overrides static map). */
90
108
  setDynamicContextWindowLimit(limit: number): void;
91
109
  dispose(): void;
110
+ private recordDatabaseStatus;
92
111
  /**
93
112
  * Derives a z.ai coding-plan `QuotaState` from OpenCode assistant turns
94
113
  * tagged with `providerID ∈ {zai, zai-coding-plan}`.