@foxden-app/foxclaw 0.10.0 → 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.
@@ -10,6 +10,7 @@ import { formatQuotaResetTime, AntigravityAuthManager } from './auth.js';
10
10
  import { AntigravityConversationManager, formatAge } from './conversations.js';
11
11
  import { UnifiedChannelOrchestrator } from '../core/orchestrator.js';
12
12
  import { AntigravityEngineAdapter } from './adapter.js';
13
+ import { AntigravitySubagentTracker } from './subagents.js';
13
14
  import { CodexEngineAdapter } from '../codex_app/adapter.js';
14
15
  import { OpencodeEngineAdapter } from '../opencode/adapter.js';
15
16
  import { BridgeSessionCore } from '../controller/controller.js';
@@ -64,6 +65,7 @@ export class UnifiedBridgeCore {
64
65
  codexCore;
65
66
  opencodeAdapter;
66
67
  defaultBackendId;
68
+ selfUpdater;
67
69
  orchestrator;
68
70
  watchers = new Map();
69
71
  pendingAgyRenames = new Map();
@@ -80,6 +82,7 @@ export class UnifiedBridgeCore {
80
82
  this.messaging = messaging ?? new TelegramMessagingPort(bot);
81
83
  this.codexApp = options?.codexApp;
82
84
  this.defaultBackendId = options?.defaultBackendId ?? 'antigravity';
85
+ this.selfUpdater = options?.selfUpdater;
83
86
  this.codexCore = options?.codexCore;
84
87
  if (!this.codexCore && options?.codexApp) {
85
88
  try {
@@ -612,6 +615,25 @@ export class UnifiedBridgeCore {
612
615
  const activeBackend = this.orchestrator.getBackendDescriptorForScope(scopeId);
613
616
  const isCodex = activeBackend.engineType === 'codex';
614
617
  if (cmd.toLowerCase() === 'update') {
618
+ if (this.selfUpdater) {
619
+ const status = await this.selfUpdater.readStatus();
620
+ if (status?.state === 'pending') {
621
+ await this.sendMessage(scopeId, locale === 'zh'
622
+ ? '⏳ 升级任务已在后台执行中,请稍候…'
623
+ : '⏳ Self-update is already running, please wait…');
624
+ return true;
625
+ }
626
+ await this.sendMessage(scopeId, locale === 'zh'
627
+ ? '🚀 正在启动全链路自升级(FoxClaw + Codex + Antigravity CLI),服务将自动更新并重载…'
628
+ : '🚀 Starting full-stack self-update (FoxClaw + Codex + Antigravity CLI). Service will restart automatically…');
629
+ try {
630
+ await this.selfUpdater.launch(scopeId, locale);
631
+ }
632
+ catch (err) {
633
+ await this.sendMessage(scopeId, `❌ 自升级启动失败: ${String(err)}`);
634
+ }
635
+ return true;
636
+ }
615
637
  if (this.codexCore) {
616
638
  if (_event) {
617
639
  this.codexCore.dispatchInboundLikeTelegramText(_event);
@@ -858,7 +880,7 @@ export class UnifiedBridgeCore {
858
880
  }
859
881
  if (data.startsWith(AGY_MODEL_CALLBACK_PREFIX)) {
860
882
  const model = data.slice(AGY_MODEL_CALLBACK_PREFIX.length);
861
- this.store.setChatSettings(scopeId, model, null);
883
+ this.store.setChatModel(scopeId, model === 'default' ? null : model);
862
884
  this.orchestrator.syncCurrentBackendSettings(scopeId);
863
885
  await this.messaging.answerCallback(data, `Model: ${model}`);
864
886
  await this.orchestrator.sendModelsMenu(scopeId, locale, messageId);
@@ -870,7 +892,7 @@ export class UnifiedBridgeCore {
870
892
  if (effort !== 'high' && settings?.serviceTier === 'boost') {
871
893
  this.store.setChatServiceTier(scopeId, null);
872
894
  }
873
- this.store.setChatSettings(scopeId, null, effort);
895
+ this.store.setChatEffort(scopeId, effort);
874
896
  this.orchestrator.syncCurrentBackendSettings(scopeId);
875
897
  await this.messaging.answerCallback(data, `Effort: ${effort}`);
876
898
  await this.sendEffortMenu(scopeId, '', locale, messageId);
@@ -1401,6 +1423,7 @@ export class UnifiedBridgeCore {
1401
1423
  messageId: null,
1402
1424
  currentToolLines: [],
1403
1425
  lastContentPreview: '',
1426
+ subagentTracker: new AntigravitySubagentTracker(conv.conversationId, undefined, this.logger),
1404
1427
  };
1405
1428
  this.watchers.set(scopeId, watcher);
1406
1429
  this.store.setWatchedThread(scopeId, conv.conversationId);
@@ -1472,6 +1495,37 @@ export class UnifiedBridgeCore {
1472
1495
  }, 1500);
1473
1496
  watcher.timer?.unref?.();
1474
1497
  }
1498
+ getScopeLocale(scopeId) {
1499
+ return this.store.getChatSettings(scopeId)?.locale || 'zh';
1500
+ }
1501
+ async renderWatcherProgressUpdate(watcher) {
1502
+ const locale = this.getScopeLocale(watcher.scopeId);
1503
+ let messageText = `👁 <b>[Antigravity 观察中]</b>\n\n`;
1504
+ if (watcher.currentToolLines.length > 0) {
1505
+ const recent = watcher.currentToolLines.slice(-8);
1506
+ messageText += `<blockquote expandable>🛠️ <b>主会话工具 (${watcher.currentToolLines.length} 项)</b>\n${recent.join('\n')}</blockquote>\n\n`;
1507
+ }
1508
+ const subBlock = watcher.subagentTracker.renderTelegramBlock(locale);
1509
+ if (subBlock) {
1510
+ messageText += `${subBlock}\n\n`;
1511
+ }
1512
+ const activeSubSummary = watcher.subagentTracker.getActiveSummaryLine(locale);
1513
+ if (activeSubSummary) {
1514
+ messageText += `⏳ <b>${escapeTelegramHtml(activeSubSummary)}</b>`;
1515
+ }
1516
+ else if (watcher.lastContentPreview) {
1517
+ messageText += watcher.lastContentPreview.slice(-3000);
1518
+ }
1519
+ else {
1520
+ messageText += `⏳ 正在执行中…`;
1521
+ }
1522
+ if (watcher.messageId) {
1523
+ await this.editMessage(watcher.scopeId, watcher.messageId, messageText).catch(() => { });
1524
+ }
1525
+ else {
1526
+ watcher.messageId = await this.sendMessage(watcher.scopeId, messageText).catch(() => null);
1527
+ }
1528
+ }
1475
1529
  async pollWatcher(watcher) {
1476
1530
  if (watcher.stopped)
1477
1531
  return;
@@ -1487,31 +1541,32 @@ export class UnifiedBridgeCore {
1487
1541
  return;
1488
1542
  }
1489
1543
  try {
1490
- if (!fs.existsSync(watcher.transcriptPath)) {
1491
- return;
1492
- }
1493
- const stats = fs.statSync(watcher.transcriptPath);
1494
- if (stats.size < watcher.fileOffset) {
1495
- watcher.fileOffset = 0;
1496
- watcher.remainder = '';
1497
- return;
1498
- }
1499
- if (stats.size === watcher.fileOffset) {
1500
- return;
1501
- }
1502
- const bytesToRead = stats.size - watcher.fileOffset;
1503
- const buffer = Buffer.alloc(bytesToRead);
1504
- const fd = fs.openSync(watcher.transcriptPath, 'r');
1505
- try {
1506
- fs.readSync(fd, buffer, 0, bytesToRead, watcher.fileOffset);
1507
- }
1508
- finally {
1509
- fs.closeSync(fd);
1544
+ let hasParentLines = false;
1545
+ const lines = [];
1546
+ if (fs.existsSync(watcher.transcriptPath)) {
1547
+ const stats = fs.statSync(watcher.transcriptPath);
1548
+ if (stats.size < watcher.fileOffset) {
1549
+ watcher.fileOffset = 0;
1550
+ watcher.remainder = '';
1551
+ }
1552
+ else if (stats.size > watcher.fileOffset) {
1553
+ const bytesToRead = stats.size - watcher.fileOffset;
1554
+ const buffer = Buffer.alloc(bytesToRead);
1555
+ const fd = fs.openSync(watcher.transcriptPath, 'r');
1556
+ try {
1557
+ fs.readSync(fd, buffer, 0, bytesToRead, watcher.fileOffset);
1558
+ }
1559
+ finally {
1560
+ fs.closeSync(fd);
1561
+ }
1562
+ watcher.fileOffset = stats.size;
1563
+ const chunk = watcher.remainder + buffer.toString('utf8');
1564
+ const splitLines = chunk.split('\n');
1565
+ watcher.remainder = splitLines.pop() ?? '';
1566
+ lines.push(...splitLines);
1567
+ hasParentLines = splitLines.length > 0;
1568
+ }
1510
1569
  }
1511
- watcher.fileOffset = stats.size;
1512
- const chunk = watcher.remainder + buffer.toString('utf8');
1513
- const lines = chunk.split('\n');
1514
- watcher.remainder = lines.pop() ?? '';
1515
1570
  for (const line of lines) {
1516
1571
  const trimmed = line.trim();
1517
1572
  if (!trimmed)
@@ -1525,12 +1580,19 @@ export class UnifiedBridgeCore {
1525
1580
  }
1526
1581
  await this.handleWatcherTranscriptEntry(watcher, entry);
1527
1582
  }
1583
+ // Poll subagents
1584
+ const subResult = await watcher.subagentTracker.poll();
1585
+ // If subagents had new tool events or status updates while the parent has no new lines
1586
+ if (subResult.hasUpdates && !hasParentLines) {
1587
+ await this.renderWatcherProgressUpdate(watcher);
1588
+ }
1528
1589
  }
1529
1590
  catch (err) {
1530
1591
  this.logger.debug('antigravity.poll_watcher_error', { error: String(err) });
1531
1592
  }
1532
1593
  }
1533
1594
  async handleWatcherTranscriptEntry(watcher, entry) {
1595
+ const locale = this.getScopeLocale(watcher.scopeId);
1534
1596
  if (entry.type === 'USER_INPUT') {
1535
1597
  const text = typeof entry.content === 'string' ? entry.content : '';
1536
1598
  const preview = text.length > 300 ? text.slice(0, 300) + '…' : text;
@@ -1540,6 +1602,16 @@ export class UnifiedBridgeCore {
1540
1602
  watcher.lastContentPreview = '';
1541
1603
  return;
1542
1604
  }
1605
+ if (entry.type === 'GENERIC') {
1606
+ const content = typeof entry.content === 'string' ? entry.content : '';
1607
+ if (content.includes('Created the following subagents') || content.includes('conversationId')) {
1608
+ const subResult = await watcher.subagentTracker.poll();
1609
+ if (subResult.hasUpdates) {
1610
+ await this.renderWatcherProgressUpdate(watcher);
1611
+ }
1612
+ }
1613
+ return;
1614
+ }
1543
1615
  if (entry.type === 'PLANNER_RESPONSE') {
1544
1616
  let hasUpdate = false;
1545
1617
  if (Array.isArray(entry.tool_calls) && entry.tool_calls.length > 0) {
@@ -1573,6 +1645,10 @@ export class UnifiedBridgeCore {
1573
1645
  : '';
1574
1646
  progressFinal += `\n\n<blockquote expandable>🛠️ <b>已调用工具 (${watcher.currentToolLines.length} 项)</b>\n${recent.join('\n')}${hiddenNote}</blockquote>`;
1575
1647
  }
1648
+ const subBlock = watcher.subagentTracker.renderTelegramBlock(locale);
1649
+ if (subBlock) {
1650
+ progressFinal += `\n\n<blockquote expandable>${subBlock}</blockquote>`;
1651
+ }
1576
1652
  await this.editMessage(watcher.scopeId, watcher.messageId, progressFinal).catch(() => { });
1577
1653
  }
1578
1654
  const chunks = chunkTelegramMessage(entry.content.trim(), 4000);
@@ -1582,6 +1658,7 @@ export class UnifiedBridgeCore {
1582
1658
  watcher.messageId = null;
1583
1659
  watcher.currentToolLines = [];
1584
1660
  watcher.lastContentPreview = '';
1661
+ watcher.subagentTracker = new AntigravitySubagentTracker(watcher.conversationId, undefined, this.logger);
1585
1662
  return;
1586
1663
  }
1587
1664
  if (entry.content) {
@@ -1594,7 +1671,15 @@ export class UnifiedBridgeCore {
1594
1671
  const recent = watcher.currentToolLines.slice(-8);
1595
1672
  messageText += `<blockquote expandable>🛠️ <b>已调用工具 (${watcher.currentToolLines.length} 项)</b>\n${recent.join('\n')}</blockquote>\n\n`;
1596
1673
  }
1597
- if (watcher.lastContentPreview) {
1674
+ const subBlock = watcher.subagentTracker.renderTelegramBlock(locale);
1675
+ if (subBlock) {
1676
+ messageText += `${subBlock}\n\n`;
1677
+ }
1678
+ const activeSubSummary = watcher.subagentTracker.getActiveSummaryLine(locale);
1679
+ if (activeSubSummary) {
1680
+ messageText += `⏳ <b>${escapeTelegramHtml(activeSubSummary)}</b>`;
1681
+ }
1682
+ else if (watcher.lastContentPreview) {
1598
1683
  messageText += watcher.lastContentPreview.slice(-3000);
1599
1684
  }
1600
1685
  else {
@@ -1877,7 +1962,7 @@ export class UnifiedBridgeCore {
1877
1962
  if (target !== 'high' && isBoost) {
1878
1963
  this.store.setChatServiceTier(scopeId, null);
1879
1964
  }
1880
- this.store.setChatSettings(scopeId, null, target);
1965
+ this.store.setChatEffort(scopeId, target);
1881
1966
  this.orchestrator.syncCurrentBackendSettings(scopeId);
1882
1967
  await this.sendMessage(scopeId, locale === 'zh' ? `✅ 思考深度已切换为: \`${target}\`` : `✅ Effort switched to: \`${target}\``);
1883
1968
  return;
@@ -90,6 +90,7 @@ export type AntigravityBridgeEvent = AgInitBridgeEvent | AgTextEvent | AgToolEve
90
90
  export declare function isVerificationError(errorMessage: string): boolean;
91
91
  export declare function isQuotaOrAuthError(errorMessage: string): boolean;
92
92
  export declare function isCapacityOrUnavailableError(errorMessage: string): boolean;
93
+ export declare function isTransientNetworkError(errorMessage: string): boolean;
93
94
  /**
94
95
  * Normalizes raw NDJSON stream events from `agy` into clean bridge events.
95
96
  */
@@ -28,6 +28,35 @@ export function isCapacityOrUnavailableError(errorMessage) {
28
28
  lower.includes('overloaded') ||
29
29
  lower.includes('high load'));
30
30
  }
31
+ export function isTransientNetworkError(errorMessage) {
32
+ if (!errorMessage)
33
+ return false;
34
+ const lower = errorMessage.toLowerCase();
35
+ return (lower.includes('client network socket disconnected') ||
36
+ lower.includes('connection timed out') ||
37
+ lower.includes('timed out') ||
38
+ lower.includes('timeout') ||
39
+ lower.includes('econnreset') ||
40
+ lower.includes('etimedout') ||
41
+ lower.includes('enotfound') ||
42
+ lower.includes('eai_again') ||
43
+ lower.includes('econnrefused') ||
44
+ lower.includes('socket hang up') ||
45
+ lower.includes('network socket disconnected') ||
46
+ lower.includes('retryable') ||
47
+ lower.includes('grpc') ||
48
+ lower.includes('network error') ||
49
+ lower.includes('fetch failed') ||
50
+ lower.includes('502 bad gateway') ||
51
+ lower.includes('504 gateway timeout') ||
52
+ lower.includes('proxy error') ||
53
+ lower.includes('read tcp') ||
54
+ lower.includes('und_err_') ||
55
+ lower.includes('broken pipe') ||
56
+ lower.includes('ehostunreach') ||
57
+ lower.includes('enetunreach') ||
58
+ lower.includes('exited unexpectedly'));
59
+ }
31
60
  /**
32
61
  * Normalizes raw NDJSON stream events from `agy` into clean bridge events.
33
62
  */
@@ -6,6 +6,7 @@ import { AntigravityAppClient } from './client.js';
6
6
  import { AntigravityAuthManager } from './auth.js';
7
7
  import { AntigravityBridgeCore } from './controller.js';
8
8
  import type { CodexAppClient } from '../codex_app/client.js';
9
+ import type { SelfUpdateRuntime } from '../update.js';
9
10
  export interface AntigravityRuntimeOptions {
10
11
  botToken?: string | undefined;
11
12
  botId?: string | undefined;
@@ -16,6 +17,7 @@ export interface AntigravityRuntimeOptions {
16
17
  codexApp?: CodexAppClient | undefined;
17
18
  app?: AntigravityAppClient | undefined;
18
19
  auth?: AntigravityAuthManager | undefined;
20
+ selfUpdater?: SelfUpdateRuntime | undefined;
19
21
  }
20
22
  /** Keeps the optional Antigravity Telegram bot lifecycle decoupled from Codex and OpenCode runtimes. */
21
23
  export declare class AntigravityTelegramRuntime {
@@ -0,0 +1,66 @@
1
+ import type { Logger } from '../logger.js';
2
+ import type { AppLocale } from '../types.js';
3
+ export interface SubagentProgressInfo {
4
+ conversationId: string;
5
+ parentConversationId: string;
6
+ agentName: string;
7
+ role?: string | undefined;
8
+ typeName?: string | undefined;
9
+ status: 'running' | 'idle' | 'done';
10
+ stepIndex: number;
11
+ toolCount: number;
12
+ currentTool: string | null;
13
+ recentTools: string[];
14
+ lastAction: string;
15
+ lastContentPreview: string;
16
+ updatedAt: number;
17
+ completedAt?: number | undefined;
18
+ }
19
+ export interface SubagentToolEvent {
20
+ subagentName: string;
21
+ conversationId: string;
22
+ toolName: string;
23
+ args?: Record<string, unknown> | string | undefined;
24
+ summary?: string | undefined;
25
+ status: 'running' | 'completed' | 'failed';
26
+ stepIndex?: number | undefined;
27
+ }
28
+ export interface SubagentPollResult {
29
+ subagents: SubagentProgressInfo[];
30
+ activeSubagents: SubagentProgressInfo[];
31
+ newToolEvents: SubagentToolEvent[];
32
+ hasUpdates: boolean;
33
+ }
34
+ export declare function cleanParameterValue(val: unknown): string;
35
+ export declare function extractSubagentToolSummary(name: string, parameters?: Record<string, unknown> | string | undefined): string | undefined;
36
+ export declare function escapeTelegramHtml(value: string): string;
37
+ export declare class AntigravitySubagentTracker {
38
+ private readonly baseDir;
39
+ private readonly dbPath;
40
+ private readonly logger;
41
+ private readonly parentConversationId;
42
+ private readonly subagents;
43
+ private parentTranscriptOffset;
44
+ private parentTranscriptRemainder;
45
+ constructor(parentConversationId: string, baseDir?: string, logger?: Logger);
46
+ get trackedCount(): number;
47
+ getSubagents(): SubagentProgressInfo[];
48
+ getActiveSubagents(): SubagentProgressInfo[];
49
+ getTranscriptPath(conversationId: string): string;
50
+ /**
51
+ * Discovers and polls active subagents for new steps, tool calls, and completion status.
52
+ */
53
+ poll(): Promise<SubagentPollResult>;
54
+ private discoverSubagentsFromDb;
55
+ private discoverSubagentsFromParentTranscript;
56
+ private extractSubagentsFromTranscriptEntry;
57
+ private pollSubagentTranscript;
58
+ /**
59
+ * Renders subagent progress for Telegram messages (watch mode or stream preview).
60
+ */
61
+ renderTelegramBlock(locale?: AppLocale): string;
62
+ /**
63
+ * One-line summary of what active subagents are currently doing.
64
+ */
65
+ getActiveSummaryLine(locale?: AppLocale): string | null;
66
+ }