crewx 0.9.0-rc.63 → 0.9.0-rc.64

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.
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.kvGet = kvGet;
4
+ exports.kvSet = kvSet;
5
+ exports.kvDel = kvDel;
6
+ exports.kvHas = kvHas;
7
+ exports.close = close;
8
+ const node_fs_1 = require("node:fs");
9
+ const node_path_1 = require("node:path");
10
+ const sdk_1 = require("@crewx/sdk");
11
+ const MEMORY_MISS = Symbol('cache-miss');
12
+ const memory = new Map();
13
+ let database;
14
+ let databasePath;
15
+ function loadDatabaseConstructor() {
16
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
17
+ return require('better-sqlite3');
18
+ }
19
+ function secureDatabaseFiles(filePath) {
20
+ for (const path of [filePath, `${filePath}-wal`, `${filePath}-shm`]) {
21
+ try {
22
+ (0, node_fs_1.chmodSync)(path, 0o600);
23
+ }
24
+ catch {
25
+ // SQLite may not have created a sidecar file yet.
26
+ }
27
+ }
28
+ }
29
+ function removeDatabaseFiles(filePath) {
30
+ for (const path of [filePath, `${filePath}-wal`, `${filePath}-shm`]) {
31
+ try {
32
+ (0, node_fs_1.unlinkSync)(path);
33
+ }
34
+ catch {
35
+ // The file may not exist or may already be gone.
36
+ }
37
+ }
38
+ }
39
+ function initializeDatabase(filePath) {
40
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(filePath, '..'), { recursive: true, mode: 0o700 });
41
+ const db = new (loadDatabaseConstructor())(filePath);
42
+ try {
43
+ db.exec(`
44
+ PRAGMA journal_mode = WAL;
45
+ PRAGMA busy_timeout = 5000;
46
+ PRAGMA synchronous = NORMAL;
47
+ PRAGMA user_version = 1;
48
+
49
+ CREATE TABLE IF NOT EXISTS kv (
50
+ key TEXT PRIMARY KEY NOT NULL,
51
+ value TEXT NOT NULL,
52
+ expires_at INTEGER NOT NULL,
53
+ CHECK (length(key) BETWEEN 1 AND 1024),
54
+ CHECK (instr(key, char(0)) = 0),
55
+ CHECK (expires_at >= 0)
56
+ ) WITHOUT ROWID;
57
+
58
+ CREATE INDEX IF NOT EXISTS idx_kv_expires_at
59
+ ON kv (expires_at);
60
+ `);
61
+ db.prepare('DELETE FROM kv WHERE expires_at <= ?').run(Date.now());
62
+ secureDatabaseFiles(filePath);
63
+ return db;
64
+ }
65
+ catch (error) {
66
+ try {
67
+ db.close();
68
+ }
69
+ catch {
70
+ // Ignore close errors while recovering a broken cache database.
71
+ }
72
+ throw error;
73
+ }
74
+ }
75
+ function openDatabase(filePath) {
76
+ try {
77
+ return initializeDatabase(filePath);
78
+ }
79
+ catch {
80
+ removeDatabaseFiles(filePath);
81
+ try {
82
+ return initializeDatabase(filePath);
83
+ }
84
+ catch {
85
+ return undefined;
86
+ }
87
+ }
88
+ }
89
+ function getDatabase() {
90
+ const filePath = (0, node_path_1.join)((0, sdk_1.getCrewxHome)(), 'cache.db');
91
+ if (database && databasePath !== filePath) {
92
+ close();
93
+ }
94
+ if (!database) {
95
+ databasePath = filePath;
96
+ database = openDatabase(filePath);
97
+ }
98
+ return database;
99
+ }
100
+ function deletePersistent(key) {
101
+ const db = getDatabase();
102
+ if (!db)
103
+ return;
104
+ try {
105
+ db.prepare('DELETE FROM kv WHERE key = ?').run(key);
106
+ secureDatabaseFiles(databasePath ?? '');
107
+ }
108
+ catch {
109
+ // Cache file writes are best effort.
110
+ }
111
+ }
112
+ function getMemoryEntry(key, now) {
113
+ const entry = memory.get(key);
114
+ if (!entry)
115
+ return undefined;
116
+ if (entry.expiresAt <= now) {
117
+ memory.delete(key);
118
+ deletePersistent(key);
119
+ return undefined;
120
+ }
121
+ return entry;
122
+ }
123
+ function readPersistent(key, now) {
124
+ const db = getDatabase();
125
+ if (!db)
126
+ return MEMORY_MISS;
127
+ let row;
128
+ try {
129
+ row = db.prepare('SELECT value, expires_at FROM kv WHERE key = ?').get(key);
130
+ }
131
+ catch {
132
+ return MEMORY_MISS;
133
+ }
134
+ if (!row)
135
+ return MEMORY_MISS;
136
+ if (row.expires_at <= now) {
137
+ deletePersistent(key);
138
+ return MEMORY_MISS;
139
+ }
140
+ try {
141
+ return JSON.parse(row.value);
142
+ }
143
+ catch {
144
+ deletePersistent(key);
145
+ return MEMORY_MISS;
146
+ }
147
+ }
148
+ function kvGet(key) {
149
+ const now = Date.now();
150
+ const memoryEntry = getMemoryEntry(key, now);
151
+ if (memoryEntry)
152
+ return memoryEntry.value;
153
+ const value = readPersistent(key, now);
154
+ if (value === MEMORY_MISS)
155
+ return undefined;
156
+ const row = getDatabase()?.prepare('SELECT expires_at FROM kv WHERE key = ?').get(key);
157
+ if (row)
158
+ memory.set(key, { value, expiresAt: row.expires_at });
159
+ return value;
160
+ }
161
+ function kvSet(key, value, ttlMs) {
162
+ const expiresAt = Date.now() + ttlMs;
163
+ memory.set(key, { value, expiresAt });
164
+ let serialized;
165
+ try {
166
+ serialized = JSON.stringify(value);
167
+ }
168
+ catch {
169
+ return;
170
+ }
171
+ if (serialized === undefined)
172
+ return;
173
+ const db = getDatabase();
174
+ if (!db)
175
+ return;
176
+ try {
177
+ db.prepare(`
178
+ INSERT INTO kv (key, value, expires_at) VALUES (?, ?, ?)
179
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at
180
+ `).run(key, serialized, expiresAt);
181
+ secureDatabaseFiles(databasePath ?? '');
182
+ }
183
+ catch {
184
+ // The in-memory value remains available when the cache file is unavailable.
185
+ }
186
+ }
187
+ function kvDel(key) {
188
+ memory.delete(key);
189
+ deletePersistent(key);
190
+ }
191
+ function kvHas(key) {
192
+ const now = Date.now();
193
+ if (getMemoryEntry(key, now))
194
+ return true;
195
+ const value = readPersistent(key, now);
196
+ if (value === MEMORY_MISS)
197
+ return false;
198
+ const row = getDatabase()?.prepare('SELECT expires_at FROM kv WHERE key = ?').get(key);
199
+ if (row)
200
+ memory.set(key, { value, expiresAt: row.expires_at });
201
+ return true;
202
+ }
203
+ function close() {
204
+ memory.clear();
205
+ try {
206
+ database?.close();
207
+ }
208
+ catch {
209
+ // Ignore close errors during shutdown and test cleanup.
210
+ }
211
+ database = undefined;
212
+ databasePath = undefined;
213
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Cached = Cached;
4
+ const cache_db_js_1 = require("../cache-db.js");
5
+ function isPromiseLike(value) {
6
+ return typeof value === 'object' && value !== null && 'then' in value && typeof value.then === 'function';
7
+ }
8
+ function Cached(options) {
9
+ return (_target, _propertyKey, descriptor) => {
10
+ if (!descriptor || typeof descriptor.value !== 'function') {
11
+ throw new TypeError('@Cached can only decorate methods');
12
+ }
13
+ const original = descriptor.value;
14
+ const returnsPromise = original.constructor.name === 'AsyncFunction';
15
+ const wrapped = function (...args) {
16
+ const cacheKey = options.key.replace(/\{(\d+)\}/g, (_match, index) => {
17
+ const argument = args[Number(index)];
18
+ return argument === undefined || argument === null ? '' : String(argument);
19
+ });
20
+ const cached = (0, cache_db_js_1.kvGet)(cacheKey);
21
+ if (cached !== undefined)
22
+ return returnsPromise ? Promise.resolve(cached) : cached;
23
+ const result = original.apply(this, args);
24
+ if (isPromiseLike(result)) {
25
+ return Promise.resolve(result).then((value) => {
26
+ (0, cache_db_js_1.kvSet)(cacheKey, value, options.ttlMs);
27
+ return value;
28
+ });
29
+ }
30
+ (0, cache_db_js_1.kvSet)(cacheKey, result, options.ttlMs);
31
+ return result;
32
+ };
33
+ descriptor.value = wrapped;
34
+ };
35
+ }
@@ -1,26 +1,102 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SessionStore = void 0;
3
4
  exports.createSession = createSession;
4
5
  exports.validateSession = validateSession;
5
6
  exports.destroySession = destroySession;
6
- const crypto_1 = require("crypto");
7
- const sessions = new Map();
7
+ exports.closeSessionStore = closeSessionStore;
8
+ const node_crypto_1 = require("node:crypto");
9
+ const node_path_1 = require("node:path");
10
+ const crewx_home_js_1 = require("../../shared/crewx-home.js");
11
+ const sessions_db_js_1 = require("./sessions-db.js");
8
12
  const SESSION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days (matches cookie maxAge)
13
+ function hashSessionId(sessionId) {
14
+ return (0, node_crypto_1.createHash)('sha256').update(sessionId).digest('hex');
15
+ }
16
+ function defaultSessionsPath() {
17
+ return (0, node_path_1.join)((0, crewx_home_js_1.getCrewxHome)(), 'sessions.db');
18
+ }
19
+ /** Owns one sessions.db handle and its in-memory first-level session cache. */
20
+ class SessionStore {
21
+ database;
22
+ sessions = new Map();
23
+ dbPath;
24
+ constructor(dbPath) {
25
+ this.dbPath = dbPath;
26
+ }
27
+ createSession() {
28
+ const now = Date.now();
29
+ const expiresAt = now + SESSION_MAX_AGE_MS;
30
+ const rawSessionId = (0, node_crypto_1.randomBytes)(32).toString('hex');
31
+ const hashedSessionId = hashSessionId(rawSessionId);
32
+ // The database write must complete before the raw cookie value is returned.
33
+ this.databaseHandle()
34
+ .prepare('INSERT INTO web_sessions (id, created_at, expires_at) VALUES (?, ?, ?)')
35
+ .run(hashedSessionId, now, expiresAt);
36
+ this.sessions.set(rawSessionId, { expiresAt });
37
+ return rawSessionId;
38
+ }
39
+ validateSession(rawSessionId) {
40
+ const now = Date.now();
41
+ const memorySession = this.sessions.get(rawSessionId);
42
+ if (memorySession) {
43
+ if (memorySession.expiresAt > now)
44
+ return true;
45
+ this.sessions.delete(rawSessionId);
46
+ this.deleteByHash(hashSessionId(rawSessionId));
47
+ return false;
48
+ }
49
+ const hashedSessionId = hashSessionId(rawSessionId);
50
+ const row = this.databaseHandle()
51
+ .prepare('SELECT id, created_at, expires_at FROM web_sessions WHERE id = ?')
52
+ .get(hashedSessionId);
53
+ if (!row)
54
+ return false;
55
+ if (row.expires_at <= now) {
56
+ this.deleteByHash(hashedSessionId);
57
+ return false;
58
+ }
59
+ this.sessions.set(rawSessionId, { expiresAt: row.expires_at });
60
+ return true;
61
+ }
62
+ destroySession(rawSessionId) {
63
+ this.sessions.delete(rawSessionId);
64
+ this.deleteByHash(hashSessionId(rawSessionId));
65
+ }
66
+ /** Close the handle and clear the memory cache, simulating a process restart. */
67
+ close() {
68
+ this.sessions.clear();
69
+ if (!this.database)
70
+ return;
71
+ this.database.close();
72
+ this.database = undefined;
73
+ }
74
+ databaseHandle() {
75
+ if (!this.database) {
76
+ this.database = (0, sessions_db_js_1.openSessionsDatabase)(this.dbPath ?? defaultSessionsPath());
77
+ this.database.prepare('DELETE FROM web_sessions WHERE expires_at <= ?').run(Date.now());
78
+ }
79
+ return this.database;
80
+ }
81
+ deleteByHash(hashedSessionId) {
82
+ this.databaseHandle().prepare('DELETE FROM web_sessions WHERE id = ?').run(hashedSessionId);
83
+ }
84
+ }
85
+ exports.SessionStore = SessionStore;
86
+ let defaultStore;
87
+ function getDefaultStore() {
88
+ return (defaultStore ??= new SessionStore());
89
+ }
9
90
  function createSession() {
10
- const sessionId = (0, crypto_1.randomBytes)(32).toString('hex');
11
- sessions.set(sessionId, { createdAt: Date.now() });
12
- return sessionId;
91
+ return getDefaultStore().createSession();
13
92
  }
14
93
  function validateSession(sessionId) {
15
- const session = sessions.get(sessionId);
16
- if (!session)
17
- return false;
18
- if (Date.now() - session.createdAt > SESSION_MAX_AGE_MS) {
19
- sessions.delete(sessionId);
20
- return false;
21
- }
22
- return true;
94
+ return getDefaultStore().validateSession(sessionId);
23
95
  }
24
96
  function destroySession(sessionId) {
25
- sessions.delete(sessionId);
97
+ getDefaultStore().destroySession(sessionId);
98
+ }
99
+ /** Close the module singleton so the next operation reopens sessions.db. */
100
+ function closeSessionStore() {
101
+ defaultStore?.close();
26
102
  }
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.openSessionsDatabase = openSessionsDatabase;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
9
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
10
+ const DATABASE_FILE_MODE = 0o600;
11
+ const DATABASE_DIRECTORY_MODE = 0o700;
12
+ const WEB_SESSIONS_SCHEMA = `
13
+ CREATE TABLE IF NOT EXISTS web_sessions (
14
+ id TEXT PRIMARY KEY NOT NULL,
15
+ created_at INTEGER NOT NULL,
16
+ expires_at INTEGER NOT NULL,
17
+ CHECK (
18
+ length(id) = 64
19
+ AND id = lower(id)
20
+ AND id NOT GLOB '*[^0-9a-f]*'
21
+ ),
22
+ CHECK (created_at >= 0),
23
+ CHECK (expires_at > created_at)
24
+ ) WITHOUT ROWID;
25
+
26
+ CREATE INDEX IF NOT EXISTS idx_web_sessions_expires_at
27
+ ON web_sessions (expires_at);
28
+ `;
29
+ function chmodIfPresent(filePath) {
30
+ if ((0, node_fs_1.existsSync)(filePath))
31
+ (0, node_fs_1.chmodSync)(filePath, DATABASE_FILE_MODE);
32
+ }
33
+ function setFilePermissions(dbPath) {
34
+ chmodIfPresent(dbPath);
35
+ chmodIfPresent(`${dbPath}-wal`);
36
+ chmodIfPresent(`${dbPath}-shm`);
37
+ }
38
+ function isCorruptionError(error) {
39
+ if (!(error instanceof Error))
40
+ return false;
41
+ return /not a database|database disk image is malformed|file is encrypted|malformed/i.test(error.message);
42
+ }
43
+ function removeDatabaseFiles(dbPath) {
44
+ for (const filePath of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
45
+ try {
46
+ (0, node_fs_1.unlinkSync)(filePath);
47
+ }
48
+ catch (error) {
49
+ if (error.code !== 'ENOENT')
50
+ throw error;
51
+ }
52
+ }
53
+ }
54
+ function initializeDatabase(dbPath) {
55
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(dbPath), { recursive: true, mode: DATABASE_DIRECTORY_MODE });
56
+ const database = new better_sqlite3_1.default(dbPath);
57
+ try {
58
+ database.pragma('journal_mode = WAL');
59
+ database.pragma('busy_timeout = 5000');
60
+ database.pragma('synchronous = FULL');
61
+ database.pragma('user_version = 1');
62
+ database.exec(WEB_SESSIONS_SCHEMA);
63
+ setFilePermissions(dbPath);
64
+ return database;
65
+ }
66
+ catch (error) {
67
+ database.close();
68
+ throw error;
69
+ }
70
+ }
71
+ /** Open the dedicated web-session database with its durability and permission policy. */
72
+ function openSessionsDatabase(dbPath) {
73
+ try {
74
+ return initializeDatabase(dbPath);
75
+ }
76
+ catch (error) {
77
+ if (!isCorruptionError(error))
78
+ throw error;
79
+ removeDatabaseFiles(dbPath);
80
+ return initializeDatabase(dbPath);
81
+ }
82
+ }
@@ -59,15 +59,13 @@ const project_repository_js_1 = require("../../repository/project.repository.js"
59
59
  const usage_limit_snapshot_repository_js_1 = require("../../repository/usage-limit-snapshot.repository.js");
60
60
  const repository_1 = require("@crewx/sdk/repository");
61
61
  const sdk_1 = require("@crewx/sdk");
62
- const CACHE_TTL = 5 * 60 * 1000; // 5분
63
- const CACHE_MAX_SIZE = 10;
64
- const CACHE_CLEANUP_INTERVAL = 5 * 60 * 1000; // 5분
62
+ const cache_db_js_1 = require("../../common/cache-db.js");
63
+ const cached_decorator_js_1 = require("../../common/decorator/cached.decorator.js");
65
64
  const CLAUDE_OAUTH_CACHE_TTL = 5 * 60 * 1000; // 5분
66
65
  const GLM_OAUTH_CACHE_TTL = 5 * 60 * 1000; // 5분
67
66
  const GROK_OAUTH_CACHE_TTL = 5 * 60 * 1000; // 5분
68
67
  const CODEX_CACHE_TTL = 5 * 60 * 1000; // 5분
69
- const AGG_CACHE_TTL_MS = 60_000;
70
- const AGG_CACHE_MAX_SIZE = 64;
68
+ const AGG_CACHE_TTL_MS = 30 * 60 * 1000;
71
69
  // WI-20260718-002: exponential backoff for negative (failed) provider caches.
72
70
  // 30s → 1m → 2m → 5m (max), reset to step 0 on the next success.
73
71
  const FAIL_BACKOFF_STEPS_MS = [30 * 1000, 60 * 1000, 2 * 60 * 1000, 5 * 60 * 1000];
@@ -90,9 +88,6 @@ let UsageService = UsageService_1 = class UsageService {
90
88
  projectRepository;
91
89
  snapshotRepo;
92
90
  logger = new common_1.Logger(UsageService_1.name);
93
- cache = new Map();
94
- aggCache = new Map();
95
- cleanupInterval;
96
91
  claudeOAuthCache = createProviderCache();
97
92
  glmCache = createProviderCache();
98
93
  grokCache = createProviderCache();
@@ -112,32 +107,22 @@ let UsageService = UsageService_1 = class UsageService {
112
107
  this.taskRepository = taskRepository;
113
108
  this.projectRepository = projectRepository;
114
109
  this.snapshotRepo = snapshotRepo;
115
- this.cleanupInterval = setInterval(() => this.evictExpired(), CACHE_CLEANUP_INTERVAL);
116
- }
117
- onModuleDestroy() {
118
- clearInterval(this.cleanupInterval);
119
110
  }
120
111
  async getDailyUsage(provider, force) {
121
- const cacheKey = `daily:${provider}`;
122
- if (!force) {
123
- const cached = this.getFromCache(cacheKey);
124
- if (cached)
125
- return { data: cached, cached: true };
126
- }
112
+ const cacheKey = `usage:daily:${provider}`;
113
+ if (force)
114
+ (0, cache_db_js_1.kvDel)(cacheKey);
115
+ const cached = !force && (0, cache_db_js_1.kvHas)(cacheKey);
127
116
  const data = await this.loadDaily(provider);
128
- this.setCache(cacheKey, data);
129
- return { data, cached: false };
117
+ return { data, cached };
130
118
  }
131
119
  async getMonthlyUsage(provider, force) {
132
- const cacheKey = `monthly:${provider}`;
133
- if (!force) {
134
- const cached = this.getFromCache(cacheKey);
135
- if (cached)
136
- return { data: cached, cached: true };
137
- }
120
+ const cacheKey = `usage:monthly:${provider}`;
121
+ if (force)
122
+ (0, cache_db_js_1.kvDel)(cacheKey);
123
+ const cached = !force && (0, cache_db_js_1.kvHas)(cacheKey);
138
124
  const data = await this.loadMonthly(provider);
139
- this.setCache(cacheKey, data);
140
- return { data, cached: false };
125
+ return { data, cached };
141
126
  }
142
127
  async loadDaily(provider) {
143
128
  const warnings = [];
@@ -177,10 +162,6 @@ let UsageService = UsageService_1 = class UsageService {
177
162
  }
178
163
  // --- USG.AGENT: Agent usage aggregation ---
179
164
  getAgentUsage(from, to, workspace) {
180
- const cacheKey = this.aggCacheKey('agent', from, to, workspace);
181
- const cached = this.getAggCache(cacheKey);
182
- if (cached)
183
- return cached;
184
165
  const rows = this.taskRepository.getAgentUsage(from, to, workspace);
185
166
  const wsNameMap = this.resolveWorkspaceNames(rows.map((r) => r.workspace_id).filter((id) => id !== null));
186
167
  const grandTotalTokens = rows.reduce((sum, r) => sum + r.total_tokens, 0);
@@ -207,15 +188,10 @@ let UsageService = UsageService_1 = class UsageService {
207
188
  totalActiveDurationMs: rows.reduce((sum, r) => sum + r.active_duration_ms, 0),
208
189
  };
209
190
  const data = { from, to, agents, totals };
210
- this.setAggCache(cacheKey, data);
211
191
  return data;
212
192
  }
213
193
  // --- USG.AGENT.TREND: Agent daily trend ---
214
194
  getAgentUsageTrend(from, to, tz, workspace) {
215
- const cacheKey = this.aggCacheKey('trend', from, to, workspace, tz);
216
- const cached = this.getAggCache(cacheKey);
217
- if (cached)
218
- return cached;
219
195
  const rows = this.taskRepository.getAgentUsageTrendRaw(from, to, workspace);
220
196
  // Group by local date (tz-converted) × agent_id
221
197
  const dailyMap = new Map();
@@ -250,15 +226,10 @@ let UsageService = UsageService_1 = class UsageService {
250
226
  .sort((a, b) => b.totalTokens - a.totalTokens),
251
227
  }));
252
228
  const data = { from, to, timezone: tz, daily };
253
- this.setAggCache(cacheKey, data);
254
229
  return data;
255
230
  }
256
231
  // --- USG.PROVIDER: Provider usage aggregation ---
257
232
  getProviderUsage(from, to, workspace) {
258
- const cacheKey = this.aggCacheKey('provider', from, to, workspace);
259
- const cached = this.getAggCache(cacheKey);
260
- if (cached)
261
- return cached;
262
233
  const rows = this.taskRepository.getProviderUsage(from, to, workspace);
263
234
  const grandTotalTokens = rows.reduce((sum, r) => sum + r.totalTokens, 0);
264
235
  const providers = rows.map((r) => ({
@@ -282,15 +253,10 @@ let UsageService = UsageService_1 = class UsageService {
282
253
  totalActiveDurationMs: rows.reduce((sum, r) => sum + r.activeDurationMs, 0),
283
254
  };
284
255
  const data = { from, to, providers, totals };
285
- this.setAggCache(cacheKey, data);
286
256
  return data;
287
257
  }
288
258
  // --- USG.MODEL: Model usage aggregation ---
289
259
  getModelUsage(from, to, workspace) {
290
- const cacheKey = this.aggCacheKey('model', from, to, workspace);
291
- const cached = this.getAggCache(cacheKey);
292
- if (cached)
293
- return cached;
294
260
  const rows = this.taskRepository.getModelUsage(from, to, workspace);
295
261
  const grandTotalTokens = rows.reduce((sum, r) => sum + r.totalTokens, 0);
296
262
  const models = rows
@@ -317,7 +283,6 @@ let UsageService = UsageService_1 = class UsageService {
317
283
  totalActiveDurationMs: rows.reduce((sum, r) => sum + r.activeDurationMs, 0),
318
284
  };
319
285
  const data = { from, to, models, totals };
320
- this.setAggCache(cacheKey, data);
321
286
  return data;
322
287
  }
323
288
  // --- USR: Rework rate (thread consecutive-call heuristic) ---
@@ -588,77 +553,6 @@ let UsageService = UsageService_1 = class UsageService {
588
553
  }
589
554
  return map;
590
555
  }
591
- // --- Cache with size limit ---
592
- getFromCache(key) {
593
- const entry = this.cache.get(key);
594
- if (!entry)
595
- return null;
596
- if (Date.now() - entry.timestamp > CACHE_TTL) {
597
- this.cache.delete(key);
598
- return null;
599
- }
600
- return entry.data;
601
- }
602
- setCache(key, data) {
603
- if (this.cache.size >= CACHE_MAX_SIZE && !this.cache.has(key)) {
604
- const oldestKey = this.findOldestKey();
605
- if (oldestKey)
606
- this.cache.delete(oldestKey);
607
- }
608
- this.cache.set(key, { data, timestamp: Date.now() });
609
- }
610
- aggCacheKey(kind, from, to, workspace, tz) {
611
- return `${kind}:${from}:${to}:${workspace ?? ''}:${tz ?? ''}`;
612
- }
613
- getAggCache(key) {
614
- const entry = this.aggCache.get(key);
615
- if (!entry)
616
- return null;
617
- if (Date.now() - entry.timestamp > AGG_CACHE_TTL_MS) {
618
- this.aggCache.delete(key);
619
- return null;
620
- }
621
- return entry.data;
622
- }
623
- setAggCache(key, data) {
624
- if (this.aggCache.size >= AGG_CACHE_MAX_SIZE && !this.aggCache.has(key)) {
625
- let oldestKey = null;
626
- let oldestTime = Infinity;
627
- for (const [entryKey, entry] of this.aggCache) {
628
- if (entry.timestamp < oldestTime) {
629
- oldestTime = entry.timestamp;
630
- oldestKey = entryKey;
631
- }
632
- }
633
- if (oldestKey)
634
- this.aggCache.delete(oldestKey);
635
- }
636
- this.aggCache.set(key, { data, timestamp: Date.now() });
637
- }
638
- findOldestKey() {
639
- let oldestKey = null;
640
- let oldestTime = Infinity;
641
- for (const [key, entry] of this.cache) {
642
- if (entry.timestamp < oldestTime) {
643
- oldestTime = entry.timestamp;
644
- oldestKey = key;
645
- }
646
- }
647
- return oldestKey;
648
- }
649
- evictExpired() {
650
- const now = Date.now();
651
- for (const [key, entry] of this.cache) {
652
- if (now - entry.timestamp > CACHE_TTL) {
653
- this.cache.delete(key);
654
- }
655
- }
656
- for (const [key, entry] of this.aggCache) {
657
- if (now - entry.timestamp > AGG_CACHE_TTL_MS) {
658
- this.aggCache.delete(key);
659
- }
660
- }
661
- }
662
556
  // --- USG.LIMITS: Subscription limit windows ---
663
557
  async getSubscriptionLimits() {
664
558
  const [codex, claude, glm, grok] = await Promise.all([
@@ -792,7 +686,7 @@ let UsageService = UsageService_1 = class UsageService {
792
686
  return { fromIso: from.toISOString(), toIso };
793
687
  }
794
688
  async parseCodexLimits() {
795
- return this.getCachedWithDedupe(this.codexCache, CODEX_CACHE_TTL, () => this.computeCodexLimits());
689
+ return this.getCachedWithDedupe(this.codexCache, CODEX_CACHE_TTL, () => this.computeCodexLimits(), 'codex');
796
690
  }
797
691
  computeCodexLimits() {
798
692
  // Tracks whether a session file was actually found this call — only a found-but-unreadable
@@ -1000,7 +894,7 @@ let UsageService = UsageService_1 = class UsageService {
1000
894
  return { usedPercent: Math.round(usedPercent), resetsAt };
1001
895
  }
1002
896
  async parseClaudeLimits() {
1003
- return this.getCachedWithDedupe(this.claudeOAuthCache, CLAUDE_OAUTH_CACHE_TTL, () => this.computeClaudeLimits());
897
+ return this.getCachedWithDedupe(this.claudeOAuthCache, CLAUDE_OAUTH_CACHE_TTL, () => this.computeClaudeLimits(), 'claude');
1004
898
  }
1005
899
  async computeClaudeLimits() {
1006
900
  try {
@@ -1071,7 +965,7 @@ let UsageService = UsageService_1 = class UsageService {
1071
965
  return { fiveHour, sevenDay };
1072
966
  }
1073
967
  async parseGrokLimits() {
1074
- return this.getCachedWithDedupe(this.grokCache, GROK_OAUTH_CACHE_TTL, () => this.computeGrokLimits());
968
+ return this.getCachedWithDedupe(this.grokCache, GROK_OAUTH_CACHE_TTL, () => this.computeGrokLimits(), 'grok');
1075
969
  }
1076
970
  async computeGrokLimits() {
1077
971
  try {
@@ -1202,7 +1096,7 @@ let UsageService = UsageService_1 = class UsageService {
1202
1096
  return Math.min(100, Math.max(0, Math.round(value)));
1203
1097
  }
1204
1098
  async parseGlmLimits() {
1205
- return this.getCachedWithDedupe(this.glmCache, GLM_OAUTH_CACHE_TTL, () => this.computeGlmLimits());
1099
+ return this.getCachedWithDedupe(this.glmCache, GLM_OAUTH_CACHE_TTL, () => this.computeGlmLimits(), 'glm');
1206
1100
  }
1207
1101
  async computeGlmLimits() {
1208
1102
  try {
@@ -1262,26 +1156,36 @@ let UsageService = UsageService_1 = class UsageService {
1262
1156
  * - Concurrent callers hitting a cache miss share the same in-flight promise instead of
1263
1157
  * firing duplicate external calls.
1264
1158
  */
1265
- async getCachedWithDedupe(cache, successTtl, fetcher) {
1159
+ async getCachedWithDedupe(cache, successTtl, fetcher, provider) {
1266
1160
  const now = Date.now();
1267
1161
  if (!cache.inFlight && !this.isProviderCacheValid(cache, now, successTtl)) {
1268
- cache.inFlight = Promise.resolve()
1269
- .then(() => fetcher())
1270
- .then((result) => {
1271
- cache.data = result;
1272
- cache.ts = Date.now();
1273
- if (result !== null) {
1274
- cache.failCount = 0;
1275
- cache.lastGood = result;
1276
- }
1277
- else {
1278
- cache.failCount += 1;
1279
- }
1280
- return result;
1281
- })
1282
- .finally(() => {
1283
- cache.inFlight = null;
1284
- });
1162
+ const persisted = (0, cache_db_js_1.kvGet)(`usage:limits:${provider}`);
1163
+ if (persisted !== undefined) {
1164
+ cache.data = persisted;
1165
+ cache.ts = now;
1166
+ cache.failCount = 0;
1167
+ cache.lastGood = persisted;
1168
+ }
1169
+ else {
1170
+ cache.inFlight = Promise.resolve()
1171
+ .then(() => fetcher())
1172
+ .then((result) => {
1173
+ cache.data = result;
1174
+ cache.ts = Date.now();
1175
+ if (result !== null) {
1176
+ cache.failCount = 0;
1177
+ cache.lastGood = result;
1178
+ (0, cache_db_js_1.kvSet)(`usage:limits:${provider}`, result, successTtl);
1179
+ }
1180
+ else {
1181
+ cache.failCount += 1;
1182
+ }
1183
+ return result;
1184
+ })
1185
+ .finally(() => {
1186
+ cache.inFlight = null;
1187
+ });
1188
+ }
1285
1189
  }
1286
1190
  if (cache.inFlight) {
1287
1191
  const result = await cache.inFlight;
@@ -1311,6 +1215,42 @@ let UsageService = UsageService_1 = class UsageService {
1311
1215
  }
1312
1216
  };
1313
1217
  exports.UsageService = UsageService;
1218
+ __decorate([
1219
+ (0, cached_decorator_js_1.Cached)({ key: 'usage:daily:{0}', ttlMs: 5 * 60 * 1000 }),
1220
+ __metadata("design:type", Function),
1221
+ __metadata("design:paramtypes", [String]),
1222
+ __metadata("design:returntype", Promise)
1223
+ ], UsageService.prototype, "loadDaily", null);
1224
+ __decorate([
1225
+ (0, cached_decorator_js_1.Cached)({ key: 'usage:monthly:{0}', ttlMs: 5 * 60 * 1000 }),
1226
+ __metadata("design:type", Function),
1227
+ __metadata("design:paramtypes", [String]),
1228
+ __metadata("design:returntype", Promise)
1229
+ ], UsageService.prototype, "loadMonthly", null);
1230
+ __decorate([
1231
+ (0, cached_decorator_js_1.Cached)({ key: 'usage:agg:agent:{0}:{1}:{2}', ttlMs: AGG_CACHE_TTL_MS }),
1232
+ __metadata("design:type", Function),
1233
+ __metadata("design:paramtypes", [String, String, String]),
1234
+ __metadata("design:returntype", Object)
1235
+ ], UsageService.prototype, "getAgentUsage", null);
1236
+ __decorate([
1237
+ (0, cached_decorator_js_1.Cached)({ key: 'usage:agg:trend:{0}:{1}:{3}:{2}', ttlMs: AGG_CACHE_TTL_MS }),
1238
+ __metadata("design:type", Function),
1239
+ __metadata("design:paramtypes", [String, String, String, String]),
1240
+ __metadata("design:returntype", Object)
1241
+ ], UsageService.prototype, "getAgentUsageTrend", null);
1242
+ __decorate([
1243
+ (0, cached_decorator_js_1.Cached)({ key: 'usage:agg:provider:{0}:{1}:{2}', ttlMs: AGG_CACHE_TTL_MS }),
1244
+ __metadata("design:type", Function),
1245
+ __metadata("design:paramtypes", [String, String, String]),
1246
+ __metadata("design:returntype", Object)
1247
+ ], UsageService.prototype, "getProviderUsage", null);
1248
+ __decorate([
1249
+ (0, cached_decorator_js_1.Cached)({ key: 'usage:agg:model:{0}:{1}:{2}', ttlMs: AGG_CACHE_TTL_MS }),
1250
+ __metadata("design:type", Function),
1251
+ __metadata("design:paramtypes", [String, String, String]),
1252
+ __metadata("design:returntype", Object)
1253
+ ], UsageService.prototype, "getModelUsage", null);
1314
1254
  exports.UsageService = UsageService = UsageService_1 = __decorate([
1315
1255
  (0, common_1.Injectable)(),
1316
1256
  __metadata("design:paramtypes", [task_repository_js_1.TaskRepository,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crewx",
3
- "version": "0.9.0-rc.63",
3
+ "version": "0.9.0-rc.64",
4
4
  "description": "CrewX — AI agent team dashboard with Electron UI and CLI (Web + Electron + Global CLI)",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -82,20 +82,20 @@
82
82
  "wink-nlp-utils": "2.1.0",
83
83
  "yargs": "17.7.0",
84
84
  "zod": "3.25.76",
85
- "@crewx/chromex": "0.1.0-rc.95",
86
- "@crewx/cli": "0.9.0-rc.63",
87
- "@crewx/cron": "0.1.10-rc.97",
88
- "@crewx/doc": "0.1.9-rc.55",
89
- "@crewx/dreaming": "0.1.0-rc.88",
90
- "@crewx/sdk": "0.9.0-rc.63",
91
- "@crewx/memory": "0.1.23-rc.79",
92
- "@crewx/search": "0.1.10-rc.58",
93
- "@crewx/knowledge-core": "0.1.17-rc.72",
85
+ "@crewx/chromex": "0.1.0-rc.96",
86
+ "@crewx/cron": "0.1.10-rc.98",
87
+ "@crewx/cli": "0.9.0-rc.64",
88
+ "@crewx/doc": "0.1.9-rc.56",
89
+ "@crewx/dreaming": "0.1.0-rc.89",
90
+ "@crewx/knowledge-core": "0.1.17-rc.73",
91
+ "@crewx/memory": "0.1.23-rc.80",
92
+ "@crewx/sdk": "0.9.0-rc.64",
93
+ "@crewx/search": "0.1.10-rc.59",
94
94
  "@crewx/shared": "0.0.6",
95
95
  "@crewx/skill": "0.1.20",
96
- "@crewx/wbs": "0.1.10-rc.88",
97
- "@crewx/wi": "0.1.10-rc.83",
98
- "@crewx/workflow": "0.3.22-rc.109"
96
+ "@crewx/wbs": "0.1.10-rc.89",
97
+ "@crewx/wi": "0.1.10-rc.84",
98
+ "@crewx/workflow": "0.3.22-rc.110"
99
99
  },
100
100
  "devDependencies": {
101
101
  "@anthropic-ai/claude-agent-sdk": "0.2.132",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewx/cli",
3
- "version": "0.9.0-rc.63",
3
+ "version": "0.9.0-rc.64",
4
4
  "license": "UNLICENSED",
5
5
  "engines": {
6
6
  "node": ">=20.19.0"