@foxden-app/foxclaw 0.7.3 → 0.11.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 (63) hide show
  1. package/.env.example +22 -8
  2. package/CHANGELOG.md +134 -0
  3. package/README.md +21 -1
  4. package/README_EN.md +21 -1
  5. package/dist/antigravity/adapter.d.ts +18 -0
  6. package/dist/antigravity/adapter.js +394 -0
  7. package/dist/antigravity/auth.d.ts +126 -0
  8. package/dist/antigravity/auth.js +933 -0
  9. package/dist/antigravity/client.d.ts +32 -0
  10. package/dist/antigravity/client.js +244 -0
  11. package/dist/antigravity/controller.d.ts +101 -0
  12. package/dist/antigravity/controller.js +2007 -0
  13. package/dist/antigravity/conversations.d.ts +29 -0
  14. package/dist/antigravity/conversations.js +289 -0
  15. package/dist/antigravity/events.d.ts +102 -0
  16. package/dist/antigravity/events.js +143 -0
  17. package/dist/antigravity/runtime.d.ts +49 -0
  18. package/dist/antigravity/runtime.js +82 -0
  19. package/dist/antigravity/subagents.d.ts +66 -0
  20. package/dist/antigravity/subagents.js +459 -0
  21. package/dist/channels/telegram/telegram_channel_adapter.d.ts +10 -3
  22. package/dist/channels/telegram/telegram_channel_adapter.js +2 -1
  23. package/dist/channels/weixin/weixin_channel_adapter.d.ts +5 -1
  24. package/dist/codex_app/adapter.d.ts +20 -0
  25. package/dist/codex_app/adapter.js +213 -0
  26. package/dist/codex_app/client.d.ts +1 -1
  27. package/dist/codex_app/client.js +12 -0
  28. package/dist/config.d.ts +14 -0
  29. package/dist/config.js +65 -16
  30. package/dist/controller/controller.d.ts +12 -5
  31. package/dist/controller/controller.js +109 -8
  32. package/dist/core/attachments.d.ts +11 -0
  33. package/dist/core/attachments.js +56 -0
  34. package/dist/core/engine_spi.d.ts +77 -0
  35. package/dist/core/engine_spi.js +1 -0
  36. package/dist/core/orchestrator.d.ts +106 -0
  37. package/dist/core/orchestrator.js +1556 -0
  38. package/dist/core/stream_preview.d.ts +29 -0
  39. package/dist/core/stream_preview.js +146 -0
  40. package/dist/core/turn_queue.d.ts +19 -0
  41. package/dist/core/turn_queue.js +41 -0
  42. package/dist/i18n.d.ts +6 -0
  43. package/dist/i18n.js +49 -0
  44. package/dist/main.js +375 -446
  45. package/dist/opencode/adapter.d.ts +11 -0
  46. package/dist/opencode/adapter.js +169 -0
  47. package/dist/opencode/controller.js +2 -2
  48. package/dist/store/database.d.ts +44 -0
  49. package/dist/store/database.js +275 -43
  50. package/dist/store/token_usage.d.ts +23 -0
  51. package/dist/store/token_usage.js +61 -0
  52. package/dist/telegram/api.d.ts +1 -0
  53. package/dist/telegram/api.js +80 -3
  54. package/dist/telegram/bot_home.js +5 -4
  55. package/dist/telegram/gateway.d.ts +4 -0
  56. package/dist/telegram/gateway.js +11 -0
  57. package/dist/types.d.ts +3 -0
  58. package/dist/update.d.ts +6 -0
  59. package/dist/update.js +95 -7
  60. package/dist/voice/tts.js +4 -2
  61. package/docs/devlogs//346/212/200/346/234/257/351/200/232/350/256/257/02./347/273/237/344/270/200/351/200/232/351/201/223/350/260/203/345/272/246/345/231/250/344/270/216/345/217/257/346/217/222/346/213/224/345/274/225/346/223/216SPI/357/274/232FoxClaw/344/273/216/345/215/225/344/275/223/350/265/260/345/220/221/345/244/232Agent/345/272/225/345/272/247.md +192 -0
  62. package/docs/devlogs//347/254/254/344/270/200/344/272/272/347/247/260/347/211/210/02./347/257/207/344/272/214_/345/275/223/344/270/200/344/270/252/346/241/245/346/216/245/345/231/250/345/274/200/345/247/213/345/255/246/344/274/232/345/220/254/346/207/202/346/211/200/346/234/211Agent.md +45 -0
  63. package/package.json +2 -2
@@ -7,7 +7,7 @@ export class BridgeStore {
7
7
  constructor(dbPath) {
8
8
  fs.mkdirSync(path.dirname(dbPath), { recursive: true });
9
9
  this.db = new DatabaseSync(dbPath);
10
- this.db.exec(`
10
+ this.db.exec(`
11
11
  CREATE TABLE IF NOT EXISTS telegram_offsets (
12
12
  bot_key TEXT PRIMARY KEY,
13
13
  update_id INTEGER NOT NULL
@@ -18,23 +18,32 @@ export class BridgeStore {
18
18
  chat_id TEXT NOT NULL,
19
19
  updated_at INTEGER NOT NULL
20
20
  );
21
- CREATE TABLE IF NOT EXISTS chat_bindings (
22
- chat_id TEXT PRIMARY KEY,
23
- thread_id TEXT NOT NULL,
24
- cwd TEXT,
25
- updated_at INTEGER NOT NULL
26
- );
27
- CREATE TABLE IF NOT EXISTS chat_settings (
28
- chat_id TEXT PRIMARY KEY,
29
- model TEXT,
21
+ CREATE TABLE IF NOT EXISTS chat_bindings (
22
+ chat_id TEXT PRIMARY KEY,
23
+ thread_id TEXT NOT NULL,
24
+ cwd TEXT,
25
+ updated_at INTEGER NOT NULL
26
+ );
27
+ CREATE TABLE IF NOT EXISTS chat_settings (
28
+ chat_id TEXT PRIMARY KEY,
29
+ model TEXT,
30
30
  reasoning_effort TEXT,
31
31
  locale TEXT,
32
32
  access_preset TEXT,
33
33
  collaboration_mode TEXT,
34
34
  service_tier TEXT,
35
35
  active_turn_message_mode TEXT,
36
+ active_backend_id TEXT,
36
37
  updated_at INTEGER NOT NULL
37
38
  );
39
+ CREATE TABLE IF NOT EXISTS scope_backend_bindings (
40
+ scope_id TEXT NOT NULL,
41
+ backend_id TEXT NOT NULL,
42
+ thread_id TEXT NOT NULL,
43
+ cwd TEXT,
44
+ updated_at INTEGER NOT NULL,
45
+ PRIMARY KEY (scope_id, backend_id)
46
+ );
38
47
  CREATE TABLE IF NOT EXISTS thread_cache (
39
48
  chat_id TEXT NOT NULL,
40
49
  idx INTEGER NOT NULL,
@@ -48,15 +57,15 @@ export class BridgeStore {
48
57
  updated_at INTEGER NOT NULL,
49
58
  PRIMARY KEY (chat_id, idx)
50
59
  );
51
- CREATE TABLE IF NOT EXISTS pending_approvals (
52
- local_id TEXT PRIMARY KEY,
53
- server_request_id TEXT NOT NULL,
54
- kind TEXT NOT NULL,
55
- chat_id TEXT NOT NULL,
56
- thread_id TEXT NOT NULL,
57
- turn_id TEXT NOT NULL,
58
- item_id TEXT NOT NULL,
59
- approval_id TEXT,
60
+ CREATE TABLE IF NOT EXISTS pending_approvals (
61
+ local_id TEXT PRIMARY KEY,
62
+ server_request_id TEXT NOT NULL,
63
+ kind TEXT NOT NULL,
64
+ chat_id TEXT NOT NULL,
65
+ thread_id TEXT NOT NULL,
66
+ turn_id TEXT NOT NULL,
67
+ item_id TEXT NOT NULL,
68
+ approval_id TEXT,
60
69
  reason TEXT,
61
70
  command TEXT,
62
71
  cwd TEXT,
@@ -64,7 +73,7 @@ export class BridgeStore {
64
73
  message_id INTEGER,
65
74
  created_at INTEGER NOT NULL,
66
75
  resolved_at INTEGER
67
- );
76
+ );
68
77
  CREATE TABLE IF NOT EXISTS active_turn_previews (
69
78
  turn_id TEXT PRIMARY KEY,
70
79
  scope_id TEXT NOT NULL,
@@ -75,6 +84,11 @@ export class BridgeStore {
75
84
  created_at INTEGER NOT NULL,
76
85
  updated_at INTEGER NOT NULL
77
86
  );
87
+ CREATE TABLE IF NOT EXISTS watched_threads (
88
+ scope_id TEXT PRIMARY KEY,
89
+ thread_id TEXT NOT NULL,
90
+ updated_at INTEGER NOT NULL
91
+ );
78
92
  CREATE TABLE IF NOT EXISTS pending_user_inputs (
79
93
  local_id TEXT PRIMARY KEY,
80
94
  server_request_id TEXT NOT NULL,
@@ -160,9 +174,9 @@ export class BridgeStore {
160
174
  direction TEXT NOT NULL,
161
175
  chat_id TEXT NOT NULL,
162
176
  event_type TEXT NOT NULL,
163
- summary TEXT NOT NULL,
164
- created_at INTEGER NOT NULL
165
- );
177
+ summary TEXT NOT NULL,
178
+ created_at INTEGER NOT NULL
179
+ );
166
180
  CREATE TABLE IF NOT EXISTS weixin_context_tokens (
167
181
  scope_id TEXT PRIMARY KEY,
168
182
  context_token TEXT NOT NULL,
@@ -208,6 +222,15 @@ export class BridgeStore {
208
222
  delete_reason TEXT,
209
223
  invalid_delete_count INTEGER NOT NULL DEFAULT 0
210
224
  );
225
+ CREATE TABLE IF NOT EXISTS cumulative_token_usage (
226
+ backend_id TEXT PRIMARY KEY,
227
+ input_tokens INTEGER NOT NULL DEFAULT 0,
228
+ output_tokens INTEGER NOT NULL DEFAULT 0,
229
+ cached_tokens INTEGER NOT NULL DEFAULT 0,
230
+ total_tokens INTEGER NOT NULL DEFAULT 0,
231
+ turns_count INTEGER NOT NULL DEFAULT 0,
232
+ updated_at INTEGER NOT NULL
233
+ );
211
234
  `);
212
235
  this.ensureColumn('thread_cache', 'name', 'TEXT');
213
236
  this.ensureColumn('thread_cache', 'model_provider', 'TEXT');
@@ -218,6 +241,7 @@ export class BridgeStore {
218
241
  this.ensureColumn('chat_settings', 'collaboration_mode', 'TEXT');
219
242
  this.ensureColumn('chat_settings', 'service_tier', 'TEXT');
220
243
  this.ensureColumn('chat_settings', 'active_turn_message_mode', 'TEXT');
244
+ this.ensureColumn('chat_settings', 'active_backend_id', 'TEXT');
221
245
  this.ensureColumn('pending_approvals', 'payload_json', 'TEXT');
222
246
  this.ensureColumn('pending_user_inputs', 'status', "TEXT NOT NULL DEFAULT 'pending'");
223
247
  this.ensureColumn('pending_user_inputs', 'submitted_at', 'INTEGER');
@@ -226,12 +250,17 @@ export class BridgeStore {
226
250
  this.ensureColumn('codex_auth_candidates', 'state', "TEXT NOT NULL DEFAULT 'active'");
227
251
  this.ensureColumn('codex_auth_candidate_runtime', 'state', "TEXT NOT NULL DEFAULT 'active'");
228
252
  this.ensureColumn('codex_auth_quota_snapshots', 'plan_type', 'TEXT');
253
+ this.ensureColumn('codex_auth_quota_snapshots', 'quota_identity_id', "TEXT NOT NULL DEFAULT ''");
229
254
  this.ensureColumn('codex_auth_quota_snapshots', 'primary_window_duration_mins', 'REAL');
230
255
  this.ensureColumn('codex_auth_quota_snapshots', 'primary_resets_at', 'INTEGER');
231
256
  this.ensureColumn('codex_auth_quota_snapshots', 'secondary_window_duration_mins', 'REAL');
232
257
  this.ensureColumn('codex_auth_quota_snapshots', 'secondary_resets_at', 'INTEGER');
233
- this.ensureColumn('codex_auth_quota_snapshots', 'quota_identity_id', "TEXT NOT NULL DEFAULT ''");
234
258
  this.ensureColumn('codex_auth_pool_history', 'invalid_delete_count', 'INTEGER NOT NULL DEFAULT 0');
259
+ this.ensureColumn('scope_backend_bindings', 'model', 'TEXT');
260
+ this.ensureColumn('scope_backend_bindings', 'reasoning_effort', 'TEXT');
261
+ this.ensureColumn('scope_backend_bindings', 'active_turn_message_mode', 'TEXT');
262
+ this.ensureColumn('scope_backend_bindings', 'service_tier', 'TEXT');
263
+ this.ensureColumn('scope_backend_bindings', 'access_preset', 'TEXT');
235
264
  this.db.prepare(`
236
265
  UPDATE codex_auth_quota_snapshots
237
266
  SET quota_identity_id = account_id
@@ -248,10 +277,10 @@ export class BridgeStore {
248
277
  return row?.update_id ?? 0;
249
278
  }
250
279
  setTelegramOffset(botKey, updateId) {
251
- this.db.prepare(`
252
- INSERT INTO telegram_offsets (bot_key, update_id)
253
- VALUES (?, ?)
254
- ON CONFLICT(bot_key) DO UPDATE SET update_id = excluded.update_id
280
+ this.db.prepare(`
281
+ INSERT INTO telegram_offsets (bot_key, update_id)
282
+ VALUES (?, ?)
283
+ ON CONFLICT(bot_key) DO UPDATE SET update_id = excluded.update_id
255
284
  `).run(botKey, updateId);
256
285
  }
257
286
  rememberTelegramPrivateScope(botId, scopeId, chatId) {
@@ -269,6 +298,31 @@ export class BridgeStore {
269
298
  const row = this.db.prepare('SELECT scope_id, chat_id FROM telegram_private_scopes WHERE bot_id = ?').get(botId);
270
299
  return row ? { scopeId: String(row.scope_id), chatId: String(row.chat_id) } : null;
271
300
  }
301
+ migrateTelegramUnnamespacedScope(botId, chatId) {
302
+ const legacyScope = `telegram:${chatId}::root`;
303
+ const namespacedScope = `telegram:${botId}:${chatId}::root`;
304
+ const existing = this.db.prepare('SELECT chat_id, thread_id, cwd, updated_at FROM chat_bindings WHERE chat_id = ?').get(legacyScope);
305
+ if (existing) {
306
+ const target = this.db.prepare('SELECT chat_id, thread_id, cwd, updated_at FROM chat_bindings WHERE chat_id = ?').get(namespacedScope);
307
+ if (!target) {
308
+ this.db.prepare('INSERT INTO chat_bindings (chat_id, thread_id, cwd, updated_at) VALUES (?, ?, ?, ?)').run(namespacedScope, existing.thread_id, existing.cwd, existing.updated_at);
309
+ }
310
+ else if (Number(existing.updated_at) > Number(target.updated_at)) {
311
+ this.db.prepare('UPDATE chat_bindings SET thread_id = ?, cwd = ?, updated_at = ? WHERE chat_id = ?').run(existing.thread_id, existing.cwd, existing.updated_at, namespacedScope);
312
+ }
313
+ }
314
+ const legacyBackends = this.db.prepare('SELECT * FROM scope_backend_bindings WHERE scope_id = ?').all(legacyScope);
315
+ for (const row of legacyBackends) {
316
+ const backendId = String(row.backend_id);
317
+ const targetRow = this.db.prepare('SELECT updated_at FROM scope_backend_bindings WHERE scope_id = ? AND backend_id = ?').get(namespacedScope, backendId);
318
+ if (!targetRow) {
319
+ this.db.prepare('INSERT INTO scope_backend_bindings (scope_id, backend_id, thread_id, cwd, updated_at, model, reasoning_effort, active_turn_message_mode, service_tier, access_preset) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(namespacedScope, backendId, row.thread_id, row.cwd, row.updated_at, row.model, row.reasoning_effort, row.active_turn_message_mode, row.service_tier, row.access_preset);
320
+ }
321
+ else if (Number(row.updated_at) > Number(targetRow.updated_at)) {
322
+ this.db.prepare('UPDATE scope_backend_bindings SET thread_id = ?, cwd = ?, updated_at = ?, model = ?, reasoning_effort = ?, active_turn_message_mode = ?, service_tier = ?, access_preset = ? WHERE scope_id = ? AND backend_id = ?').run(row.thread_id, row.cwd, row.updated_at, row.model, row.reasoning_effort, row.active_turn_message_mode, row.service_tier, row.access_preset, namespacedScope, backendId);
323
+ }
324
+ }
325
+ }
272
326
  getBinding(chatId) {
273
327
  const row = this.db.prepare('SELECT chat_id, thread_id, cwd, updated_at FROM chat_bindings WHERE chat_id = ?').get(chatId);
274
328
  if (!row)
@@ -299,8 +353,28 @@ export class BridgeStore {
299
353
  clearBinding(chatId) {
300
354
  this.db.prepare('DELETE FROM chat_bindings WHERE chat_id = ?').run(chatId);
301
355
  }
356
+ getWatchedThread(chatId) {
357
+ const row = this.db.prepare('SELECT thread_id FROM watched_threads WHERE scope_id = ?').get(chatId);
358
+ return row?.thread_id ? String(row.thread_id) : null;
359
+ }
360
+ setWatchedThread(chatId, threadId) {
361
+ if (threadId) {
362
+ this.db.prepare(`
363
+ INSERT INTO watched_threads (scope_id, thread_id, updated_at)
364
+ VALUES (?, ?, ?)
365
+ ON CONFLICT(scope_id) DO UPDATE SET thread_id = excluded.thread_id, updated_at = excluded.updated_at
366
+ `).run(chatId, threadId, Date.now());
367
+ }
368
+ else {
369
+ this.db.prepare('DELETE FROM watched_threads WHERE scope_id = ?').run(chatId);
370
+ }
371
+ }
372
+ listWatchedThreads() {
373
+ const rows = this.db.prepare('SELECT scope_id, thread_id FROM watched_threads').all();
374
+ return rows.map((r) => ({ scopeId: String(r.scope_id), threadId: String(r.thread_id) }));
375
+ }
302
376
  getChatSettings(chatId) {
303
- const row = this.db.prepare('SELECT chat_id, model, reasoning_effort, locale, access_preset, collaboration_mode, service_tier, active_turn_message_mode, updated_at FROM chat_settings WHERE chat_id = ?').get(chatId);
377
+ const row = this.db.prepare('SELECT chat_id, model, reasoning_effort, locale, access_preset, collaboration_mode, service_tier, active_turn_message_mode, active_backend_id, updated_at FROM chat_settings WHERE chat_id = ?').get(chatId);
304
378
  if (!row)
305
379
  return null;
306
380
  return {
@@ -312,33 +386,105 @@ export class BridgeStore {
312
386
  collaborationMode: normalizeCollaborationMode(row.collaboration_mode),
313
387
  serviceTier: row.service_tier === null ? null : String(row.service_tier),
314
388
  activeTurnMessageMode: normalizeActiveTurnMessageMode(row.active_turn_message_mode),
389
+ activeBackendId: row.active_backend_id === null || row.active_backend_id === undefined ? null : String(row.active_backend_id),
315
390
  updatedAt: Number(row.updated_at),
316
391
  };
317
392
  }
318
393
  setChatSettings(chatId, model, reasoningEffort, locale) {
319
394
  const current = this.getChatSettings(chatId);
320
395
  const nextLocale = locale === undefined ? current?.locale ?? null : locale;
321
- this.writeChatSettings(chatId, model, reasoningEffort, nextLocale, current?.accessPreset ?? null, current?.collaborationMode ?? null, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null);
396
+ this.writeChatSettings(chatId, model, reasoningEffort, nextLocale, current?.accessPreset ?? null, current?.collaborationMode ?? null, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null, current?.activeBackendId ?? null);
397
+ }
398
+ setChatModel(chatId, model) {
399
+ const current = this.getChatSettings(chatId);
400
+ this.setChatSettings(chatId, model, current?.reasoningEffort ?? null);
401
+ }
402
+ setChatEffort(chatId, effort) {
403
+ const current = this.getChatSettings(chatId);
404
+ this.setChatSettings(chatId, current?.model ?? null, effort);
322
405
  }
323
406
  setChatLocale(chatId, locale) {
324
407
  const current = this.getChatSettings(chatId);
325
- this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, locale, current?.accessPreset ?? null, current?.collaborationMode ?? null, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null);
408
+ this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, locale, current?.accessPreset ?? null, current?.collaborationMode ?? null, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null, current?.activeBackendId ?? null);
326
409
  }
327
410
  setChatAccessPreset(chatId, accessPreset) {
328
411
  const current = this.getChatSettings(chatId);
329
- this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, current?.locale ?? null, accessPreset, current?.collaborationMode ?? null, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null);
412
+ this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, current?.locale ?? null, accessPreset, current?.collaborationMode ?? null, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null, current?.activeBackendId ?? null);
330
413
  }
331
414
  setChatCollaborationMode(chatId, collaborationMode) {
332
415
  const current = this.getChatSettings(chatId);
333
- this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, current?.locale ?? null, current?.accessPreset ?? null, collaborationMode, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null);
416
+ this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, current?.locale ?? null, current?.accessPreset ?? null, collaborationMode, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null, current?.activeBackendId ?? null);
334
417
  }
335
418
  setChatServiceTier(chatId, serviceTier) {
336
419
  const current = this.getChatSettings(chatId);
337
- this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, current?.locale ?? null, current?.accessPreset ?? null, current?.collaborationMode ?? null, serviceTier, current?.activeTurnMessageMode ?? null);
420
+ this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, current?.locale ?? null, current?.accessPreset ?? null, current?.collaborationMode ?? null, serviceTier, current?.activeTurnMessageMode ?? null, current?.activeBackendId ?? null);
338
421
  }
339
422
  setChatActiveTurnMessageMode(chatId, activeTurnMessageMode) {
340
423
  const current = this.getChatSettings(chatId);
341
- this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, current?.locale ?? null, current?.accessPreset ?? null, current?.collaborationMode ?? null, current?.serviceTier ?? null, activeTurnMessageMode);
424
+ this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, current?.locale ?? null, current?.accessPreset ?? null, current?.collaborationMode ?? null, current?.serviceTier ?? null, activeTurnMessageMode, current?.activeBackendId ?? null);
425
+ }
426
+ getActiveBackend(chatId) {
427
+ return this.getChatSettings(chatId)?.activeBackendId ?? null;
428
+ }
429
+ setActiveBackend(chatId, backendId) {
430
+ const current = this.getChatSettings(chatId);
431
+ this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, current?.locale ?? null, current?.accessPreset ?? null, current?.collaborationMode ?? null, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null, backendId);
432
+ }
433
+ getScopeBackendBinding(scopeId, backendId) {
434
+ const row = this.db.prepare(`
435
+ SELECT thread_id, cwd, model, reasoning_effort, active_turn_message_mode, service_tier, access_preset
436
+ FROM scope_backend_bindings
437
+ WHERE scope_id = ? AND backend_id = ?
438
+ `).get(scopeId, backendId);
439
+ if (!row)
440
+ return null;
441
+ return {
442
+ threadId: String(row.thread_id),
443
+ cwd: row.cwd ? String(row.cwd) : null,
444
+ model: row.model === null || row.model === undefined ? null : String(row.model),
445
+ reasoningEffort: row.reasoning_effort === null || row.reasoning_effort === undefined ? null : String(row.reasoning_effort),
446
+ activeTurnMessageMode: row.active_turn_message_mode === null || row.active_turn_message_mode === undefined ? null : String(row.active_turn_message_mode),
447
+ serviceTier: row.service_tier === null || row.service_tier === undefined ? null : String(row.service_tier),
448
+ accessPreset: row.access_preset === null || row.access_preset === undefined ? null : String(row.access_preset),
449
+ };
450
+ }
451
+ setScopeBackendBinding(scopeId, backendId, threadId, cwd, settings) {
452
+ const current = this.getScopeBackendBinding(scopeId, backendId);
453
+ const model = settings && settings.model !== undefined ? settings.model : current?.model ?? null;
454
+ const reasoningEffort = settings && settings.reasoningEffort !== undefined ? settings.reasoningEffort : current?.reasoningEffort ?? null;
455
+ const activeTurnMessageMode = settings && settings.activeTurnMessageMode !== undefined ? settings.activeTurnMessageMode : current?.activeTurnMessageMode ?? null;
456
+ const serviceTier = settings && settings.serviceTier !== undefined ? settings.serviceTier : current?.serviceTier ?? null;
457
+ const accessPreset = settings && settings.accessPreset !== undefined ? settings.accessPreset : current?.accessPreset ?? null;
458
+ this.db.prepare(`
459
+ INSERT INTO scope_backend_bindings (
460
+ scope_id, backend_id, thread_id, cwd, model, reasoning_effort,
461
+ active_turn_message_mode, service_tier, access_preset, updated_at
462
+ )
463
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
464
+ ON CONFLICT(scope_id, backend_id) DO UPDATE SET
465
+ thread_id = excluded.thread_id,
466
+ cwd = excluded.cwd,
467
+ model = excluded.model,
468
+ reasoning_effort = excluded.reasoning_effort,
469
+ active_turn_message_mode = excluded.active_turn_message_mode,
470
+ service_tier = excluded.service_tier,
471
+ access_preset = excluded.access_preset,
472
+ updated_at = excluded.updated_at
473
+ `).run(scopeId, backendId, threadId, cwd, model, reasoningEffort, activeTurnMessageMode, serviceTier, accessPreset, Date.now());
474
+ }
475
+ listScopeBackendBindings(scopeId) {
476
+ const rows = this.db.prepare(`
477
+ SELECT backend_id, thread_id, cwd, updated_at
478
+ FROM scope_backend_bindings
479
+ WHERE scope_id = ?
480
+ ORDER BY updated_at DESC
481
+ `).all(scopeId);
482
+ return rows.map((r) => ({
483
+ backendId: String(r.backend_id),
484
+ threadId: String(r.thread_id),
485
+ cwd: r.cwd ? String(r.cwd) : null,
486
+ updatedAt: Number(r.updated_at),
487
+ }));
342
488
  }
343
489
  findChatIdByThreadId(threadId) {
344
490
  const row = this.db.prepare('SELECT chat_id FROM chat_bindings WHERE thread_id = ?').get(threadId);
@@ -365,7 +511,7 @@ export class BridgeStore {
365
511
  });
366
512
  }
367
513
  getCachedThread(chatId, index) {
368
- const row = this.db.prepare(`
514
+ const row = this.db.prepare(`
369
515
  SELECT idx, thread_id, name, preview, cwd, model_provider, status, archived, updated_at
370
516
  FROM thread_cache
371
517
  WHERE chat_id = ? AND idx = ?
@@ -385,11 +531,11 @@ export class BridgeStore {
385
531
  };
386
532
  }
387
533
  listCachedThreads(chatId) {
388
- const rows = this.db.prepare(`
534
+ const rows = this.db.prepare(`
389
535
  SELECT idx, thread_id, name, preview, cwd, model_provider, status, archived, updated_at
390
536
  FROM thread_cache
391
537
  WHERE chat_id = ?
392
- ORDER BY idx ASC
538
+ ORDER BY idx ASC
393
539
  `).all(chatId);
394
540
  return rows.map((row) => ({
395
541
  index: Number(row.idx),
@@ -404,7 +550,7 @@ export class BridgeStore {
404
550
  }));
405
551
  }
406
552
  savePendingApproval(record) {
407
- this.db.prepare(`
553
+ this.db.prepare(`
408
554
  INSERT INTO pending_approvals (
409
555
  local_id, server_request_id, kind, chat_id, thread_id, turn_id, item_id, approval_id, reason, command, cwd, payload_json, message_id, created_at, resolved_at
410
556
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
@@ -833,10 +979,10 @@ export class BridgeStore {
833
979
  resolvedAt: row.resolved_at === null ? null : Number(row.resolved_at),
834
980
  };
835
981
  }
836
- writeChatSettings(chatId, model, reasoningEffort, locale, accessPreset, collaborationMode, serviceTier, activeTurnMessageMode) {
982
+ writeChatSettings(chatId, model, reasoningEffort, locale, accessPreset, collaborationMode, serviceTier, activeTurnMessageMode, activeBackendId = null) {
837
983
  this.db.prepare(`
838
- INSERT INTO chat_settings (chat_id, model, reasoning_effort, locale, access_preset, collaboration_mode, service_tier, active_turn_message_mode, updated_at)
839
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
984
+ INSERT INTO chat_settings (chat_id, model, reasoning_effort, locale, access_preset, collaboration_mode, service_tier, active_turn_message_mode, active_backend_id, updated_at)
985
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
840
986
  ON CONFLICT(chat_id) DO UPDATE SET
841
987
  model = excluded.model,
842
988
  reasoning_effort = excluded.reasoning_effort,
@@ -845,8 +991,9 @@ export class BridgeStore {
845
991
  collaboration_mode = excluded.collaboration_mode,
846
992
  service_tier = excluded.service_tier,
847
993
  active_turn_message_mode = excluded.active_turn_message_mode,
994
+ active_backend_id = excluded.active_backend_id,
848
995
  updated_at = excluded.updated_at
849
- `).run(chatId, model, reasoningEffort, locale, accessPreset, collaborationMode, serviceTier, activeTurnMessageMode, Date.now());
996
+ `).run(chatId, model, reasoningEffort, locale, accessPreset, collaborationMode, serviceTier, activeTurnMessageMode, activeBackendId, Date.now());
850
997
  }
851
998
  getWeixinContextToken(scopeId) {
852
999
  const row = this.db.prepare('SELECT context_token FROM weixin_context_tokens WHERE scope_id = ?').get(scopeId);
@@ -1071,6 +1218,91 @@ export class BridgeStore {
1071
1218
  updatedAt: Number(row.updated_at),
1072
1219
  }));
1073
1220
  }
1221
+ recordTokenUsage(delta, backendId = 'default') {
1222
+ const input = Math.max(0, Math.round(delta.inputTokens ?? 0));
1223
+ const output = Math.max(0, Math.round(delta.outputTokens ?? 0));
1224
+ const cached = Math.max(0, Math.round(delta.cachedTokens ?? 0));
1225
+ const total = delta.totalTokens != null ? Math.max(0, Math.round(delta.totalTokens)) : input + output;
1226
+ const now = Date.now();
1227
+ this.db.prepare(`
1228
+ INSERT INTO cumulative_token_usage (backend_id, input_tokens, output_tokens, cached_tokens, total_tokens, turns_count, updated_at)
1229
+ VALUES (?, ?, ?, ?, ?, 1, ?)
1230
+ ON CONFLICT(backend_id) DO UPDATE SET
1231
+ input_tokens = cumulative_token_usage.input_tokens + excluded.input_tokens,
1232
+ output_tokens = cumulative_token_usage.output_tokens + excluded.output_tokens,
1233
+ cached_tokens = cumulative_token_usage.cached_tokens + excluded.cached_tokens,
1234
+ total_tokens = cumulative_token_usage.total_tokens + excluded.total_tokens,
1235
+ turns_count = cumulative_token_usage.turns_count + 1,
1236
+ updated_at = excluded.updated_at
1237
+ `).run(backendId, input, output, cached, total, now);
1238
+ }
1239
+ setBackendCumulativeTokenUsage(usage, backendId) {
1240
+ const input = Math.max(0, Math.round(usage.inputTokens));
1241
+ const output = Math.max(0, Math.round(usage.outputTokens));
1242
+ const cached = Math.max(0, Math.round(usage.cachedTokens ?? 0));
1243
+ const total = usage.totalTokens != null ? Math.max(0, Math.round(usage.totalTokens)) : input + output;
1244
+ const turns = Math.max(0, Math.round(usage.turnsCount ?? 0));
1245
+ const now = Date.now();
1246
+ this.db.prepare(`
1247
+ INSERT INTO cumulative_token_usage (backend_id, input_tokens, output_tokens, cached_tokens, total_tokens, turns_count, updated_at)
1248
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1249
+ ON CONFLICT(backend_id) DO UPDATE SET
1250
+ input_tokens = excluded.input_tokens,
1251
+ output_tokens = excluded.output_tokens,
1252
+ cached_tokens = excluded.cached_tokens,
1253
+ total_tokens = excluded.total_tokens,
1254
+ turns_count = excluded.turns_count,
1255
+ updated_at = excluded.updated_at
1256
+ `).run(backendId, input, output, cached, total, turns, now);
1257
+ }
1258
+ getCumulativeTokenUsage(backendId) {
1259
+ if (backendId && backendId !== 'all') {
1260
+ const row = this.db.prepare(`
1261
+ SELECT input_tokens, output_tokens, cached_tokens, total_tokens, turns_count
1262
+ FROM cumulative_token_usage
1263
+ WHERE backend_id = ?
1264
+ `).get(backendId);
1265
+ return {
1266
+ inputTokens: Number(row?.input_tokens ?? 0),
1267
+ outputTokens: Number(row?.output_tokens ?? 0),
1268
+ cachedTokens: Number(row?.cached_tokens ?? 0),
1269
+ totalTokens: Number(row?.total_tokens ?? 0),
1270
+ turnsCount: Number(row?.turns_count ?? 0),
1271
+ };
1272
+ }
1273
+ const row = this.db.prepare(`
1274
+ SELECT
1275
+ COALESCE(SUM(input_tokens), 0) AS input_tokens,
1276
+ COALESCE(SUM(output_tokens), 0) AS output_tokens,
1277
+ COALESCE(SUM(cached_tokens), 0) AS cached_tokens,
1278
+ COALESCE(SUM(total_tokens), 0) AS total_tokens,
1279
+ COALESCE(SUM(turns_count), 0) AS turns_count
1280
+ FROM cumulative_token_usage
1281
+ `).get();
1282
+ return {
1283
+ inputTokens: Number(row?.input_tokens ?? 0),
1284
+ outputTokens: Number(row?.output_tokens ?? 0),
1285
+ cachedTokens: Number(row?.cached_tokens ?? 0),
1286
+ totalTokens: Number(row?.total_tokens ?? 0),
1287
+ turnsCount: Number(row?.turns_count ?? 0),
1288
+ };
1289
+ }
1290
+ getAllBackendTokenUsages() {
1291
+ const rows = this.db.prepare(`
1292
+ SELECT backend_id, input_tokens, output_tokens, cached_tokens, total_tokens, turns_count, updated_at
1293
+ FROM cumulative_token_usage
1294
+ ORDER BY total_tokens DESC
1295
+ `).all();
1296
+ return rows.map((r) => ({
1297
+ backendId: r.backend_id,
1298
+ inputTokens: Number(r.input_tokens),
1299
+ outputTokens: Number(r.output_tokens),
1300
+ cachedTokens: Number(r.cached_tokens),
1301
+ totalTokens: Number(r.total_tokens),
1302
+ turnsCount: Number(r.turns_count),
1303
+ updatedAt: Number(r.updated_at),
1304
+ }));
1305
+ }
1074
1306
  ensureColumn(table, column, definition) {
1075
1307
  const columns = this.db.prepare(`PRAGMA table_info(${table})`).all();
1076
1308
  if (columns.some(entry => entry.name === column)) {
@@ -0,0 +1,23 @@
1
+ import type { AppLocale } from '../types.js';
2
+ import type { BridgeStore } from './database.js';
3
+ export interface CumulativeTokenTotals {
4
+ inputTokens: number;
5
+ outputTokens: number;
6
+ cachedTokens: number;
7
+ totalTokens: number;
8
+ turnsCount: number;
9
+ }
10
+ export interface CumulativeTokenUsageRecord extends CumulativeTokenTotals {
11
+ backendId: string;
12
+ updatedAt: number;
13
+ }
14
+ export interface TokenUsageDelta {
15
+ inputTokens?: number | undefined;
16
+ outputTokens?: number | undefined;
17
+ cachedTokens?: number | undefined;
18
+ totalTokens?: number | undefined;
19
+ }
20
+ export declare function formatMetricTokenCount(value: number): string;
21
+ export declare function formatTokenUsageSummary(totals: CumulativeTokenTotals, locale?: AppLocale): string;
22
+ export declare function formatBackendTokenUsageBreakdown(usages: CumulativeTokenUsageRecord[]): string;
23
+ export declare function syncCodexLocalUsageToStore(store: BridgeStore, codexHome?: string): Promise<void>;
@@ -0,0 +1,61 @@
1
+ import { readCodexLocalUsageStats } from '../codex_app/local_usage.js';
2
+ export function formatMetricTokenCount(value) {
3
+ if (!Number.isFinite(value) || value <= 0) {
4
+ return '0';
5
+ }
6
+ if (value >= 1_000_000_000) {
7
+ const g = value / 1_000_000_000;
8
+ return `${Number.isInteger(g) ? g : Number(g.toFixed(2))}G`;
9
+ }
10
+ if (value >= 1_000_000) {
11
+ const m = value / 1_000_000;
12
+ return `${Number.isInteger(m) ? m : Number(m.toFixed(2))}M`;
13
+ }
14
+ if (value >= 1_000) {
15
+ const k = value / 1_000;
16
+ return `${Number.isInteger(k) ? k : Number(k.toFixed(1))}K`;
17
+ }
18
+ return String(Math.round(value));
19
+ }
20
+ export function formatTokenUsageSummary(totals, locale = 'zh') {
21
+ const total = formatMetricTokenCount(totals.totalTokens);
22
+ const input = formatMetricTokenCount(totals.inputTokens);
23
+ const output = formatMetricTokenCount(totals.outputTokens);
24
+ const cached = formatMetricTokenCount(totals.cachedTokens);
25
+ if (locale === 'zh') {
26
+ return `• **Token 用量**: 总计 ${total} (输入 ${input}, 输出 ${output}, 缓存 ${cached})`;
27
+ }
28
+ return `• **Token Usage**: Total ${total} (Input ${input}, Output ${output}, Cached ${cached})`;
29
+ }
30
+ export function formatBackendTokenUsageBreakdown(usages) {
31
+ if (usages.length <= 1)
32
+ return '';
33
+ const labels = {
34
+ antigravity: 'AGY',
35
+ codex: 'Codex',
36
+ opencode: 'OpenCode',
37
+ };
38
+ const parts = usages
39
+ .filter((u) => u.totalTokens > 0)
40
+ .map((u) => `${labels[u.backendId] || u.backendId}: ${formatMetricTokenCount(u.totalTokens)}`);
41
+ if (parts.length <= 1)
42
+ return '';
43
+ return `\n └ ${parts.join(' · ')}`;
44
+ }
45
+ export async function syncCodexLocalUsageToStore(store, codexHome) {
46
+ try {
47
+ const stats = await readCodexLocalUsageStats(codexHome);
48
+ if (stats.totals.totalTokens > 0 || stats.turns > 0) {
49
+ store.setBackendCumulativeTokenUsage({
50
+ inputTokens: stats.totals.inputTokens,
51
+ outputTokens: stats.totals.outputTokens,
52
+ cachedTokens: stats.totals.cachedInputTokens,
53
+ totalTokens: stats.totals.totalTokens,
54
+ turnsCount: stats.turns,
55
+ }, 'codex');
56
+ }
57
+ }
58
+ catch {
59
+ // Non-fatal if codex home is unavailable
60
+ }
61
+ }
@@ -9,6 +9,7 @@ export interface TelegramRemoteFile {
9
9
  file_size?: number;
10
10
  file_path?: string;
11
11
  }
12
+ export declare function isTransientNetworkError(err: unknown): boolean;
12
13
  export declare function callTelegramApi<T>(botToken: string, method: string, body: Record<string, unknown>): Promise<TelegramApiResult<T>>;
13
14
  export declare function callTelegramMultipartApi<T>(botToken: string, method: string, fields: Record<string, string>, files: Array<{
14
15
  fieldName: string;