@foxden-app/foxclaw 0.7.2 → 0.10.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 (71) hide show
  1. package/.env.example +14 -0
  2. package/CHANGELOG.md +129 -0
  3. package/README.md +22 -2
  4. package/README_EN.md +22 -2
  5. package/dist/antigravity/adapter.d.ts +17 -0
  6. package/dist/antigravity/adapter.js +256 -0
  7. package/dist/antigravity/auth.d.ts +121 -0
  8. package/dist/antigravity/auth.js +896 -0
  9. package/dist/antigravity/client.d.ts +32 -0
  10. package/dist/antigravity/client.js +238 -0
  11. package/dist/antigravity/controller.d.ts +96 -0
  12. package/dist/antigravity/controller.js +1922 -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 +101 -0
  16. package/dist/antigravity/events.js +114 -0
  17. package/dist/antigravity/runtime.d.ts +47 -0
  18. package/dist/antigravity/runtime.js +82 -0
  19. package/dist/auth/cross_node_sync.js +2 -2
  20. package/dist/channels/telegram/telegram_channel_adapter.d.ts +10 -3
  21. package/dist/channels/telegram/telegram_channel_adapter.js +2 -1
  22. package/dist/channels/weixin/weixin_channel_adapter.d.ts +5 -1
  23. package/dist/codex_app/adapter.d.ts +20 -0
  24. package/dist/codex_app/adapter.js +213 -0
  25. package/dist/codex_app/client.d.ts +2 -1
  26. package/dist/codex_app/client.js +47 -4
  27. package/dist/codex_app/force_takeover.d.ts +12 -0
  28. package/dist/codex_app/force_takeover.js +27 -0
  29. package/dist/config.d.ts +12 -0
  30. package/dist/config.js +42 -0
  31. package/dist/controller/controller.d.ts +15 -5
  32. package/dist/controller/controller.js +252 -20
  33. package/dist/core/attachments.d.ts +11 -0
  34. package/dist/core/attachments.js +56 -0
  35. package/dist/core/engine_spi.d.ts +76 -0
  36. package/dist/core/engine_spi.js +1 -0
  37. package/dist/core/orchestrator.d.ts +101 -0
  38. package/dist/core/orchestrator.js +1404 -0
  39. package/dist/core/stream_preview.d.ts +29 -0
  40. package/dist/core/stream_preview.js +146 -0
  41. package/dist/core/turn_queue.d.ts +19 -0
  42. package/dist/core/turn_queue.js +41 -0
  43. package/dist/i18n.d.ts +36 -6
  44. package/dist/i18n.js +81 -6
  45. package/dist/main.js +190 -20
  46. package/dist/opencode/adapter.d.ts +11 -0
  47. package/dist/opencode/adapter.js +168 -0
  48. package/dist/store/database.d.ts +41 -0
  49. package/dist/store/database.js +242 -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.js +34 -1
  53. package/dist/telegram/bot_home.d.ts +3 -0
  54. package/dist/telegram/bot_home.js +107 -0
  55. package/dist/telegram/gateway.d.ts +5 -0
  56. package/dist/telegram/gateway.js +21 -0
  57. package/dist/types.d.ts +1 -0
  58. package/dist/update.d.ts +6 -0
  59. package/dist/update.js +78 -6
  60. package/dist/voice/target.js +2 -1
  61. package/dist/voice/tts.js +4 -2
  62. 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
  63. 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
  64. package/docs/user-manual.md +11 -1
  65. package/docs/zh/2026-09-05-bot-home-migration.md +26 -0
  66. package/docs/zh/2026-09-05-reliability-review.md +53 -0
  67. package/docs/zh/troubleshooting.md +12 -0
  68. package/docs/zh/user-manual.md +13 -1
  69. package/package.json +2 -2
  70. package/scripts/force-takeover.py +118 -0
  71. package/scripts/force-takeover.test.py +122 -0
@@ -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) {
@@ -299,8 +328,28 @@ export class BridgeStore {
299
328
  clearBinding(chatId) {
300
329
  this.db.prepare('DELETE FROM chat_bindings WHERE chat_id = ?').run(chatId);
301
330
  }
331
+ getWatchedThread(chatId) {
332
+ const row = this.db.prepare('SELECT thread_id FROM watched_threads WHERE scope_id = ?').get(chatId);
333
+ return row?.thread_id ? String(row.thread_id) : null;
334
+ }
335
+ setWatchedThread(chatId, threadId) {
336
+ if (threadId) {
337
+ this.db.prepare(`
338
+ INSERT INTO watched_threads (scope_id, thread_id, updated_at)
339
+ VALUES (?, ?, ?)
340
+ ON CONFLICT(scope_id) DO UPDATE SET thread_id = excluded.thread_id, updated_at = excluded.updated_at
341
+ `).run(chatId, threadId, Date.now());
342
+ }
343
+ else {
344
+ this.db.prepare('DELETE FROM watched_threads WHERE scope_id = ?').run(chatId);
345
+ }
346
+ }
347
+ listWatchedThreads() {
348
+ const rows = this.db.prepare('SELECT scope_id, thread_id FROM watched_threads').all();
349
+ return rows.map((r) => ({ scopeId: String(r.scope_id), threadId: String(r.thread_id) }));
350
+ }
302
351
  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);
352
+ 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
353
  if (!row)
305
354
  return null;
306
355
  return {
@@ -312,33 +361,97 @@ export class BridgeStore {
312
361
  collaborationMode: normalizeCollaborationMode(row.collaboration_mode),
313
362
  serviceTier: row.service_tier === null ? null : String(row.service_tier),
314
363
  activeTurnMessageMode: normalizeActiveTurnMessageMode(row.active_turn_message_mode),
364
+ activeBackendId: row.active_backend_id === null || row.active_backend_id === undefined ? null : String(row.active_backend_id),
315
365
  updatedAt: Number(row.updated_at),
316
366
  };
317
367
  }
318
368
  setChatSettings(chatId, model, reasoningEffort, locale) {
319
369
  const current = this.getChatSettings(chatId);
320
370
  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);
371
+ this.writeChatSettings(chatId, model, reasoningEffort, nextLocale, current?.accessPreset ?? null, current?.collaborationMode ?? null, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null, current?.activeBackendId ?? null);
322
372
  }
323
373
  setChatLocale(chatId, locale) {
324
374
  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);
375
+ 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
376
  }
327
377
  setChatAccessPreset(chatId, accessPreset) {
328
378
  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);
379
+ 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
380
  }
331
381
  setChatCollaborationMode(chatId, collaborationMode) {
332
382
  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);
383
+ 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
384
  }
335
385
  setChatServiceTier(chatId, serviceTier) {
336
386
  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);
387
+ 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
388
  }
339
389
  setChatActiveTurnMessageMode(chatId, activeTurnMessageMode) {
340
390
  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);
391
+ 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);
392
+ }
393
+ getActiveBackend(chatId) {
394
+ return this.getChatSettings(chatId)?.activeBackendId ?? null;
395
+ }
396
+ setActiveBackend(chatId, backendId) {
397
+ const current = this.getChatSettings(chatId);
398
+ 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);
399
+ }
400
+ getScopeBackendBinding(scopeId, backendId) {
401
+ const row = this.db.prepare(`
402
+ SELECT thread_id, cwd, model, reasoning_effort, active_turn_message_mode, service_tier, access_preset
403
+ FROM scope_backend_bindings
404
+ WHERE scope_id = ? AND backend_id = ?
405
+ `).get(scopeId, backendId);
406
+ if (!row)
407
+ return null;
408
+ return {
409
+ threadId: String(row.thread_id),
410
+ cwd: row.cwd ? String(row.cwd) : null,
411
+ model: row.model === null || row.model === undefined ? null : String(row.model),
412
+ reasoningEffort: row.reasoning_effort === null || row.reasoning_effort === undefined ? null : String(row.reasoning_effort),
413
+ activeTurnMessageMode: row.active_turn_message_mode === null || row.active_turn_message_mode === undefined ? null : String(row.active_turn_message_mode),
414
+ serviceTier: row.service_tier === null || row.service_tier === undefined ? null : String(row.service_tier),
415
+ accessPreset: row.access_preset === null || row.access_preset === undefined ? null : String(row.access_preset),
416
+ };
417
+ }
418
+ setScopeBackendBinding(scopeId, backendId, threadId, cwd, settings) {
419
+ const current = this.getScopeBackendBinding(scopeId, backendId);
420
+ const model = settings && settings.model !== undefined ? settings.model : current?.model ?? null;
421
+ const reasoningEffort = settings && settings.reasoningEffort !== undefined ? settings.reasoningEffort : current?.reasoningEffort ?? null;
422
+ const activeTurnMessageMode = settings && settings.activeTurnMessageMode !== undefined ? settings.activeTurnMessageMode : current?.activeTurnMessageMode ?? null;
423
+ const serviceTier = settings && settings.serviceTier !== undefined ? settings.serviceTier : current?.serviceTier ?? null;
424
+ const accessPreset = settings && settings.accessPreset !== undefined ? settings.accessPreset : current?.accessPreset ?? null;
425
+ this.db.prepare(`
426
+ INSERT INTO scope_backend_bindings (
427
+ scope_id, backend_id, thread_id, cwd, model, reasoning_effort,
428
+ active_turn_message_mode, service_tier, access_preset, updated_at
429
+ )
430
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
431
+ ON CONFLICT(scope_id, backend_id) DO UPDATE SET
432
+ thread_id = excluded.thread_id,
433
+ cwd = excluded.cwd,
434
+ model = excluded.model,
435
+ reasoning_effort = excluded.reasoning_effort,
436
+ active_turn_message_mode = excluded.active_turn_message_mode,
437
+ service_tier = excluded.service_tier,
438
+ access_preset = excluded.access_preset,
439
+ updated_at = excluded.updated_at
440
+ `).run(scopeId, backendId, threadId, cwd, model, reasoningEffort, activeTurnMessageMode, serviceTier, accessPreset, Date.now());
441
+ }
442
+ listScopeBackendBindings(scopeId) {
443
+ const rows = this.db.prepare(`
444
+ SELECT backend_id, thread_id, cwd, updated_at
445
+ FROM scope_backend_bindings
446
+ WHERE scope_id = ?
447
+ ORDER BY updated_at DESC
448
+ `).all(scopeId);
449
+ return rows.map((r) => ({
450
+ backendId: String(r.backend_id),
451
+ threadId: String(r.thread_id),
452
+ cwd: r.cwd ? String(r.cwd) : null,
453
+ updatedAt: Number(r.updated_at),
454
+ }));
342
455
  }
343
456
  findChatIdByThreadId(threadId) {
344
457
  const row = this.db.prepare('SELECT chat_id FROM chat_bindings WHERE thread_id = ?').get(threadId);
@@ -365,7 +478,7 @@ export class BridgeStore {
365
478
  });
366
479
  }
367
480
  getCachedThread(chatId, index) {
368
- const row = this.db.prepare(`
481
+ const row = this.db.prepare(`
369
482
  SELECT idx, thread_id, name, preview, cwd, model_provider, status, archived, updated_at
370
483
  FROM thread_cache
371
484
  WHERE chat_id = ? AND idx = ?
@@ -385,11 +498,11 @@ export class BridgeStore {
385
498
  };
386
499
  }
387
500
  listCachedThreads(chatId) {
388
- const rows = this.db.prepare(`
501
+ const rows = this.db.prepare(`
389
502
  SELECT idx, thread_id, name, preview, cwd, model_provider, status, archived, updated_at
390
503
  FROM thread_cache
391
504
  WHERE chat_id = ?
392
- ORDER BY idx ASC
505
+ ORDER BY idx ASC
393
506
  `).all(chatId);
394
507
  return rows.map((row) => ({
395
508
  index: Number(row.idx),
@@ -404,7 +517,7 @@ export class BridgeStore {
404
517
  }));
405
518
  }
406
519
  savePendingApproval(record) {
407
- this.db.prepare(`
520
+ this.db.prepare(`
408
521
  INSERT INTO pending_approvals (
409
522
  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
523
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
@@ -833,10 +946,10 @@ export class BridgeStore {
833
946
  resolvedAt: row.resolved_at === null ? null : Number(row.resolved_at),
834
947
  };
835
948
  }
836
- writeChatSettings(chatId, model, reasoningEffort, locale, accessPreset, collaborationMode, serviceTier, activeTurnMessageMode) {
949
+ writeChatSettings(chatId, model, reasoningEffort, locale, accessPreset, collaborationMode, serviceTier, activeTurnMessageMode, activeBackendId = null) {
837
950
  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 (?, ?, ?, ?, ?, ?, ?, ?, ?)
951
+ 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)
952
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
840
953
  ON CONFLICT(chat_id) DO UPDATE SET
841
954
  model = excluded.model,
842
955
  reasoning_effort = excluded.reasoning_effort,
@@ -845,8 +958,9 @@ export class BridgeStore {
845
958
  collaboration_mode = excluded.collaboration_mode,
846
959
  service_tier = excluded.service_tier,
847
960
  active_turn_message_mode = excluded.active_turn_message_mode,
961
+ active_backend_id = excluded.active_backend_id,
848
962
  updated_at = excluded.updated_at
849
- `).run(chatId, model, reasoningEffort, locale, accessPreset, collaborationMode, serviceTier, activeTurnMessageMode, Date.now());
963
+ `).run(chatId, model, reasoningEffort, locale, accessPreset, collaborationMode, serviceTier, activeTurnMessageMode, activeBackendId, Date.now());
850
964
  }
851
965
  getWeixinContextToken(scopeId) {
852
966
  const row = this.db.prepare('SELECT context_token FROM weixin_context_tokens WHERE scope_id = ?').get(scopeId);
@@ -1071,6 +1185,91 @@ export class BridgeStore {
1071
1185
  updatedAt: Number(row.updated_at),
1072
1186
  }));
1073
1187
  }
1188
+ recordTokenUsage(delta, backendId = 'default') {
1189
+ const input = Math.max(0, Math.round(delta.inputTokens ?? 0));
1190
+ const output = Math.max(0, Math.round(delta.outputTokens ?? 0));
1191
+ const cached = Math.max(0, Math.round(delta.cachedTokens ?? 0));
1192
+ const total = delta.totalTokens != null ? Math.max(0, Math.round(delta.totalTokens)) : input + output;
1193
+ const now = Date.now();
1194
+ this.db.prepare(`
1195
+ INSERT INTO cumulative_token_usage (backend_id, input_tokens, output_tokens, cached_tokens, total_tokens, turns_count, updated_at)
1196
+ VALUES (?, ?, ?, ?, ?, 1, ?)
1197
+ ON CONFLICT(backend_id) DO UPDATE SET
1198
+ input_tokens = cumulative_token_usage.input_tokens + excluded.input_tokens,
1199
+ output_tokens = cumulative_token_usage.output_tokens + excluded.output_tokens,
1200
+ cached_tokens = cumulative_token_usage.cached_tokens + excluded.cached_tokens,
1201
+ total_tokens = cumulative_token_usage.total_tokens + excluded.total_tokens,
1202
+ turns_count = cumulative_token_usage.turns_count + 1,
1203
+ updated_at = excluded.updated_at
1204
+ `).run(backendId, input, output, cached, total, now);
1205
+ }
1206
+ setBackendCumulativeTokenUsage(usage, backendId) {
1207
+ const input = Math.max(0, Math.round(usage.inputTokens));
1208
+ const output = Math.max(0, Math.round(usage.outputTokens));
1209
+ const cached = Math.max(0, Math.round(usage.cachedTokens ?? 0));
1210
+ const total = usage.totalTokens != null ? Math.max(0, Math.round(usage.totalTokens)) : input + output;
1211
+ const turns = Math.max(0, Math.round(usage.turnsCount ?? 0));
1212
+ const now = Date.now();
1213
+ this.db.prepare(`
1214
+ INSERT INTO cumulative_token_usage (backend_id, input_tokens, output_tokens, cached_tokens, total_tokens, turns_count, updated_at)
1215
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1216
+ ON CONFLICT(backend_id) DO UPDATE SET
1217
+ input_tokens = excluded.input_tokens,
1218
+ output_tokens = excluded.output_tokens,
1219
+ cached_tokens = excluded.cached_tokens,
1220
+ total_tokens = excluded.total_tokens,
1221
+ turns_count = excluded.turns_count,
1222
+ updated_at = excluded.updated_at
1223
+ `).run(backendId, input, output, cached, total, turns, now);
1224
+ }
1225
+ getCumulativeTokenUsage(backendId) {
1226
+ if (backendId && backendId !== 'all') {
1227
+ const row = this.db.prepare(`
1228
+ SELECT input_tokens, output_tokens, cached_tokens, total_tokens, turns_count
1229
+ FROM cumulative_token_usage
1230
+ WHERE backend_id = ?
1231
+ `).get(backendId);
1232
+ return {
1233
+ inputTokens: Number(row?.input_tokens ?? 0),
1234
+ outputTokens: Number(row?.output_tokens ?? 0),
1235
+ cachedTokens: Number(row?.cached_tokens ?? 0),
1236
+ totalTokens: Number(row?.total_tokens ?? 0),
1237
+ turnsCount: Number(row?.turns_count ?? 0),
1238
+ };
1239
+ }
1240
+ const row = this.db.prepare(`
1241
+ SELECT
1242
+ COALESCE(SUM(input_tokens), 0) AS input_tokens,
1243
+ COALESCE(SUM(output_tokens), 0) AS output_tokens,
1244
+ COALESCE(SUM(cached_tokens), 0) AS cached_tokens,
1245
+ COALESCE(SUM(total_tokens), 0) AS total_tokens,
1246
+ COALESCE(SUM(turns_count), 0) AS turns_count
1247
+ FROM cumulative_token_usage
1248
+ `).get();
1249
+ return {
1250
+ inputTokens: Number(row?.input_tokens ?? 0),
1251
+ outputTokens: Number(row?.output_tokens ?? 0),
1252
+ cachedTokens: Number(row?.cached_tokens ?? 0),
1253
+ totalTokens: Number(row?.total_tokens ?? 0),
1254
+ turnsCount: Number(row?.turns_count ?? 0),
1255
+ };
1256
+ }
1257
+ getAllBackendTokenUsages() {
1258
+ const rows = this.db.prepare(`
1259
+ SELECT backend_id, input_tokens, output_tokens, cached_tokens, total_tokens, turns_count, updated_at
1260
+ FROM cumulative_token_usage
1261
+ ORDER BY total_tokens DESC
1262
+ `).all();
1263
+ return rows.map((r) => ({
1264
+ backendId: r.backend_id,
1265
+ inputTokens: Number(r.input_tokens),
1266
+ outputTokens: Number(r.output_tokens),
1267
+ cachedTokens: Number(r.cached_tokens),
1268
+ totalTokens: Number(r.total_tokens),
1269
+ turnsCount: Number(r.turns_count),
1270
+ updatedAt: Number(r.updated_at),
1271
+ }));
1272
+ }
1074
1273
  ensureColumn(table, column, definition) {
1075
1274
  const columns = this.db.prepare(`PRAGMA table_info(${table})`).all();
1076
1275
  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
+ }
@@ -4,7 +4,18 @@ import https from 'node:https';
4
4
  import path from 'node:path';
5
5
  import { pipeline } from 'node:stream/promises';
6
6
  const DEFAULT_API_BASE_URL = 'https://api.telegram.org';
7
- export async function callTelegramApi(botToken, method, body) {
7
+ function isTransientNetworkError(err) {
8
+ if (!err)
9
+ return false;
10
+ const msg = String(err.message || err);
11
+ return (msg.includes('Client network socket disconnected') ||
12
+ msg.includes('ECONNRESET') ||
13
+ msg.includes('ETIMEDOUT') ||
14
+ msg.includes('EAI_AGAIN') ||
15
+ msg.includes('ENOTFOUND') ||
16
+ msg.includes('socket hang up'));
17
+ }
18
+ function callTelegramApiOnce(botToken, method, body) {
8
19
  const payload = JSON.stringify(body);
9
20
  const endpoint = telegramApiUrl(botToken, method);
10
21
  return new Promise((resolve, reject) => {
@@ -14,7 +25,9 @@ export async function callTelegramApi(botToken, method, body) {
14
25
  'content-type': 'application/json',
15
26
  'content-length': Buffer.byteLength(payload),
16
27
  },
28
+ signal: AbortSignal.timeout(telegramApiTimeoutMs(false)),
17
29
  }, (response) => {
30
+ response.on('error', reject);
18
31
  const chunks = [];
19
32
  response.on('data', (chunk) => {
20
33
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
@@ -37,6 +50,24 @@ export async function callTelegramApi(botToken, method, body) {
37
50
  request.end();
38
51
  });
39
52
  }
53
+ export async function callTelegramApi(botToken, method, body) {
54
+ const maxAttempts = method === 'getUpdates' ? 1 : 3;
55
+ let lastError;
56
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
57
+ try {
58
+ return await callTelegramApiOnce(botToken, method, body);
59
+ }
60
+ catch (err) {
61
+ lastError = err;
62
+ if (attempt < maxAttempts && isTransientNetworkError(err)) {
63
+ await new Promise((r) => setTimeout(r, 400 * attempt));
64
+ continue;
65
+ }
66
+ throw err;
67
+ }
68
+ }
69
+ throw lastError;
70
+ }
40
71
  export async function callTelegramMultipartApi(botToken, method, fields, files) {
41
72
  const boundary = `foxclaw-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
42
73
  const parts = [];
@@ -58,7 +89,9 @@ export async function callTelegramMultipartApi(botToken, method, fields, files)
58
89
  'content-type': `multipart/form-data; boundary=${boundary}`,
59
90
  'content-length': payload.length,
60
91
  },
92
+ signal: AbortSignal.timeout(telegramApiTimeoutMs(true)),
61
93
  }, (response) => {
94
+ response.on('error', reject);
62
95
  const chunks = [];
63
96
  response.on('data', (chunk) => {
64
97
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
@@ -0,0 +1,3 @@
1
+ /** Keep storage names readable while bot IDs remain the stable routing identity. */
2
+ export declare function prepareTelegramBotHome(baseDir: string, botId: string, username: string | null, sharedHome?: string | null): string;
3
+ export declare function readTelegramBotHomeIdentity(codexHome: string): string | null;