@foxden-app/foxclaw 0.10.0 → 0.11.1

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,459 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { DatabaseSync } from 'node:sqlite';
5
+ export function cleanParameterValue(val) {
6
+ if (typeof val !== 'string')
7
+ return '';
8
+ let str = val.trim();
9
+ if (str.startsWith('"') && str.endsWith('"') && str.length >= 2) {
10
+ str = str.slice(1, -1).trim();
11
+ }
12
+ return str;
13
+ }
14
+ export function extractSubagentToolSummary(name, parameters) {
15
+ if (!parameters)
16
+ return undefined;
17
+ let params = {};
18
+ if (typeof parameters === 'string') {
19
+ try {
20
+ params = JSON.parse(parameters);
21
+ }
22
+ catch {
23
+ return parameters.slice(0, 60);
24
+ }
25
+ }
26
+ else if (typeof parameters === 'object') {
27
+ params = parameters;
28
+ }
29
+ const summary = cleanParameterValue(params.toolSummary);
30
+ if (summary)
31
+ return summary.slice(0, 60);
32
+ const action = cleanParameterValue(params.toolAction);
33
+ if (action)
34
+ return action.slice(0, 60);
35
+ const cmd = cleanParameterValue(params.CommandLine);
36
+ if (cmd)
37
+ return `$ ${cmd.slice(0, 70)}`;
38
+ const targetFile = cleanParameterValue(params.TargetFile);
39
+ if (targetFile)
40
+ return path.basename(targetFile);
41
+ const absPath = cleanParameterValue(params.AbsolutePath);
42
+ if (absPath)
43
+ return path.basename(absPath);
44
+ const p = cleanParameterValue(params.path);
45
+ if (p)
46
+ return path.basename(p);
47
+ const q = cleanParameterValue(params.query);
48
+ if (q)
49
+ return q.slice(0, 60);
50
+ const url = cleanParameterValue(params.Url);
51
+ if (url)
52
+ return url.slice(0, 60);
53
+ const prompt = cleanParameterValue(params.Prompt);
54
+ if (prompt)
55
+ return prompt.slice(0, 50);
56
+ return undefined;
57
+ }
58
+ export function escapeTelegramHtml(value) {
59
+ return value
60
+ .replace(/&/g, '&')
61
+ .replace(/</g, '&lt;')
62
+ .replace(/>/g, '&gt;');
63
+ }
64
+ export class AntigravitySubagentTracker {
65
+ baseDir;
66
+ dbPath;
67
+ logger;
68
+ parentConversationId;
69
+ subagents = new Map();
70
+ parentTranscriptOffset = 0;
71
+ parentTranscriptRemainder = '';
72
+ constructor(parentConversationId, baseDir, logger) {
73
+ this.parentConversationId = parentConversationId;
74
+ this.baseDir = baseDir || path.join(os.homedir(), '.gemini', 'antigravity-cli');
75
+ this.dbPath = path.join(this.baseDir, 'conversation_summaries.db');
76
+ this.logger = logger;
77
+ }
78
+ get trackedCount() {
79
+ return this.subagents.size;
80
+ }
81
+ getSubagents() {
82
+ return Array.from(this.subagents.values()).map((s) => ({ ...s.info }));
83
+ }
84
+ getActiveSubagents() {
85
+ return Array.from(this.subagents.values())
86
+ .filter((s) => s.info.status === 'running')
87
+ .map((s) => ({ ...s.info }));
88
+ }
89
+ getTranscriptPath(conversationId) {
90
+ return path.join(this.baseDir, 'brain', conversationId, '.system_generated', 'logs', 'transcript.jsonl');
91
+ }
92
+ /**
93
+ * Discovers and polls active subagents for new steps, tool calls, and completion status.
94
+ */
95
+ async poll() {
96
+ let hasUpdates = false;
97
+ const newToolEvents = [];
98
+ // 1. Discover subagents from SQLite database
99
+ this.discoverSubagentsFromDb();
100
+ // 2. Discover subagents from parent transcript.jsonl
101
+ this.discoverSubagentsFromParentTranscript();
102
+ // 3. Poll each tracked subagent's transcript
103
+ for (const state of this.subagents.values()) {
104
+ const updated = this.pollSubagentTranscript(state, newToolEvents);
105
+ if (updated) {
106
+ hasUpdates = true;
107
+ }
108
+ }
109
+ const all = this.getSubagents();
110
+ const active = all.filter((s) => s.status === 'running');
111
+ return {
112
+ subagents: all,
113
+ activeSubagents: active,
114
+ newToolEvents,
115
+ hasUpdates,
116
+ };
117
+ }
118
+ discoverSubagentsFromDb() {
119
+ if (!fs.existsSync(this.dbPath))
120
+ return;
121
+ try {
122
+ const db = new DatabaseSync(this.dbPath, { readOnly: true });
123
+ try {
124
+ db.exec('PRAGMA busy_timeout = 2000;');
125
+ // Discover direct subagents of parent, as well as nested subagents
126
+ const parentIdsToQuery = [this.parentConversationId, ...Array.from(this.subagents.keys())];
127
+ const placeholders = parentIdsToQuery.map(() => '?').join(',');
128
+ const rows = db.prepare(`
129
+ SELECT conversation_id, agent_name, status, not_fully_idle, killed, last_modified_time, parent_conversation_id
130
+ FROM conversation_summaries
131
+ WHERE parent_conversation_id IN (${placeholders}) AND killed = 0
132
+ ORDER BY last_modified_time ASC
133
+ `).all(...parentIdsToQuery);
134
+ for (const r of rows) {
135
+ const id = String(r.conversation_id);
136
+ const parentId = String(r.parent_conversation_id || this.parentConversationId);
137
+ const agentName = String(r.agent_name || 'Subagent');
138
+ const isIdle = !r.not_fully_idle || r.status === 'CASCADE_RUN_STATUS_IDLE';
139
+ if (!this.subagents.has(id)) {
140
+ const transcriptPath = this.getTranscriptPath(id);
141
+ this.subagents.set(id, {
142
+ info: {
143
+ conversationId: id,
144
+ parentConversationId: parentId,
145
+ agentName,
146
+ status: isIdle ? 'done' : 'running',
147
+ stepIndex: 0,
148
+ toolCount: 0,
149
+ currentTool: null,
150
+ recentTools: [],
151
+ lastAction: '',
152
+ lastContentPreview: '',
153
+ updatedAt: Date.now(),
154
+ },
155
+ transcriptPath,
156
+ fileOffset: 0,
157
+ remainder: '',
158
+ seenToolKeys: new Set(),
159
+ });
160
+ }
161
+ }
162
+ }
163
+ finally {
164
+ db.close();
165
+ }
166
+ }
167
+ catch (err) {
168
+ this.logger?.debug('antigravity.subagents.db_discovery_error', { error: String(err) });
169
+ }
170
+ }
171
+ discoverSubagentsFromParentTranscript() {
172
+ const parentTranscript = this.getTranscriptPath(this.parentConversationId);
173
+ if (!fs.existsSync(parentTranscript))
174
+ return;
175
+ try {
176
+ const stats = fs.statSync(parentTranscript);
177
+ if (stats.size < this.parentTranscriptOffset) {
178
+ this.parentTranscriptOffset = 0;
179
+ this.parentTranscriptRemainder = '';
180
+ }
181
+ if (stats.size === this.parentTranscriptOffset)
182
+ return;
183
+ const bytesToRead = stats.size - this.parentTranscriptOffset;
184
+ const buffer = Buffer.alloc(bytesToRead);
185
+ const fd = fs.openSync(parentTranscript, 'r');
186
+ try {
187
+ fs.readSync(fd, buffer, 0, bytesToRead, this.parentTranscriptOffset);
188
+ }
189
+ finally {
190
+ fs.closeSync(fd);
191
+ }
192
+ this.parentTranscriptOffset = stats.size;
193
+ const chunk = this.parentTranscriptRemainder + buffer.toString('utf8');
194
+ const lines = chunk.split('\n');
195
+ this.parentTranscriptRemainder = lines.pop() ?? '';
196
+ for (const line of lines) {
197
+ const trimmed = line.trim();
198
+ if (!trimmed)
199
+ continue;
200
+ try {
201
+ const entry = JSON.parse(trimmed);
202
+ this.extractSubagentsFromTranscriptEntry(entry, this.parentConversationId);
203
+ }
204
+ catch {
205
+ // ignore malformed lines
206
+ }
207
+ }
208
+ }
209
+ catch (err) {
210
+ this.logger?.debug('antigravity.subagents.parent_transcript_error', { error: String(err) });
211
+ }
212
+ }
213
+ extractSubagentsFromTranscriptEntry(entry, parentId) {
214
+ // 1. Check tool_calls for invoke_subagent
215
+ if (Array.isArray(entry.tool_calls)) {
216
+ for (const tc of entry.tool_calls) {
217
+ if (tc.name === 'invoke_subagent' && tc.args) {
218
+ let subagentsArg = tc.args.Subagents;
219
+ if (typeof subagentsArg === 'string') {
220
+ try {
221
+ subagentsArg = JSON.parse(subagentsArg);
222
+ }
223
+ catch {
224
+ // ignore
225
+ }
226
+ }
227
+ if (Array.isArray(subagentsArg)) {
228
+ for (const sub of subagentsArg) {
229
+ const name = sub.Role || sub.TypeName || 'Subagent';
230
+ void name;
231
+ }
232
+ }
233
+ }
234
+ }
235
+ }
236
+ // 2. Check generic content or output for Created the following subagents
237
+ const content = typeof entry.content === 'string' ? entry.content : '';
238
+ if (content.includes('Created the following subagents') || content.includes('conversationId')) {
239
+ const convMatches = content.matchAll(/"conversationId":\s*"([0-9a-fA-F-]{36})"/g);
240
+ for (const match of convMatches) {
241
+ const subId = match[1];
242
+ if (subId && subId !== this.parentConversationId && !this.subagents.has(subId)) {
243
+ const transcriptPath = this.getTranscriptPath(subId);
244
+ this.subagents.set(subId, {
245
+ info: {
246
+ conversationId: subId,
247
+ parentConversationId: parentId,
248
+ agentName: 'Subagent',
249
+ status: 'running',
250
+ stepIndex: 0,
251
+ toolCount: 0,
252
+ currentTool: null,
253
+ recentTools: [],
254
+ lastAction: '',
255
+ lastContentPreview: '',
256
+ updatedAt: Date.now(),
257
+ },
258
+ transcriptPath,
259
+ fileOffset: 0,
260
+ remainder: '',
261
+ seenToolKeys: new Set(),
262
+ });
263
+ }
264
+ }
265
+ }
266
+ }
267
+ pollSubagentTranscript(state, newToolEvents) {
268
+ if (!fs.existsSync(state.transcriptPath)) {
269
+ return false;
270
+ }
271
+ try {
272
+ const stats = fs.statSync(state.transcriptPath);
273
+ if (stats.size < state.fileOffset) {
274
+ state.fileOffset = 0;
275
+ state.remainder = '';
276
+ }
277
+ if (stats.size === state.fileOffset) {
278
+ return false;
279
+ }
280
+ const bytesToRead = stats.size - state.fileOffset;
281
+ const buffer = Buffer.alloc(bytesToRead);
282
+ const fd = fs.openSync(state.transcriptPath, 'r');
283
+ try {
284
+ fs.readSync(fd, buffer, 0, bytesToRead, state.fileOffset);
285
+ }
286
+ finally {
287
+ fs.closeSync(fd);
288
+ }
289
+ state.fileOffset = stats.size;
290
+ const chunk = state.remainder + buffer.toString('utf8');
291
+ const lines = chunk.split('\n');
292
+ state.remainder = lines.pop() ?? '';
293
+ let hasUpdates = false;
294
+ for (const line of lines) {
295
+ const trimmed = line.trim();
296
+ if (!trimmed)
297
+ continue;
298
+ let entry;
299
+ try {
300
+ entry = JSON.parse(trimmed);
301
+ }
302
+ catch {
303
+ continue;
304
+ }
305
+ // Check if subagent spawned a nested subagent
306
+ this.extractSubagentsFromTranscriptEntry(entry, state.info.conversationId);
307
+ if (typeof entry.step_index === 'number') {
308
+ state.info.stepIndex = entry.step_index;
309
+ }
310
+ if (entry.type === 'USER_INPUT' && typeof entry.content === 'string') {
311
+ // Task description received
312
+ hasUpdates = true;
313
+ state.info.updatedAt = Date.now();
314
+ }
315
+ if (entry.type === 'GENERIC') {
316
+ hasUpdates = true;
317
+ state.info.updatedAt = Date.now();
318
+ if (state.lastRunningTool) {
319
+ newToolEvents.push({
320
+ subagentName: state.info.agentName,
321
+ conversationId: state.info.conversationId,
322
+ toolName: state.lastRunningTool.toolName,
323
+ summary: state.lastRunningTool.summary,
324
+ status: entry.status === 'ERROR' ? 'failed' : 'completed',
325
+ stepIndex: state.lastRunningTool.stepIndex,
326
+ });
327
+ state.lastRunningTool = null;
328
+ state.info.currentTool = null;
329
+ }
330
+ }
331
+ if (entry.type === 'PLANNER_RESPONSE') {
332
+ hasUpdates = true;
333
+ state.info.updatedAt = Date.now();
334
+ if (typeof entry.content === 'string' && entry.content.trim()) {
335
+ state.info.lastContentPreview = entry.content.trim().slice(-300);
336
+ }
337
+ if (Array.isArray(entry.tool_calls) && entry.tool_calls.length > 0) {
338
+ for (const tc of entry.tool_calls) {
339
+ const toolName = tc.name || 'tool';
340
+ const toolKey = `${state.info.stepIndex}:${toolName}:${JSON.stringify(tc.args || {})}`;
341
+ if (!state.seenToolKeys.has(toolKey)) {
342
+ state.seenToolKeys.add(toolKey);
343
+ state.info.toolCount += 1;
344
+ if (state.lastRunningTool) {
345
+ newToolEvents.push({
346
+ subagentName: state.info.agentName,
347
+ conversationId: state.info.conversationId,
348
+ toolName: state.lastRunningTool.toolName,
349
+ summary: state.lastRunningTool.summary,
350
+ status: 'completed',
351
+ stepIndex: state.lastRunningTool.stepIndex,
352
+ });
353
+ state.lastRunningTool = null;
354
+ }
355
+ if (toolName === 'send_message') {
356
+ state.info.status = 'done';
357
+ state.info.completedAt = Date.now();
358
+ state.info.lastAction = '✅ 完成任务并回传结果给主 Agent';
359
+ state.info.currentTool = null;
360
+ }
361
+ else {
362
+ const summary = extractSubagentToolSummary(toolName, tc.args);
363
+ const displayDesc = summary ? ` · <i>${escapeTelegramHtml(summary)}</i>` : '';
364
+ const toolLine = `⚙️ <code>${escapeTelegramHtml(toolName)}</code>${displayDesc}`;
365
+ state.info.recentTools.push(toolLine);
366
+ state.info.currentTool = summary ? `${toolName} (${summary})` : toolName;
367
+ state.info.lastAction = summary ? `${toolName}: ${summary}` : toolName;
368
+ state.lastRunningTool = {
369
+ toolName,
370
+ summary,
371
+ stepIndex: state.info.stepIndex,
372
+ };
373
+ newToolEvents.push({
374
+ subagentName: state.info.agentName,
375
+ conversationId: state.info.conversationId,
376
+ toolName,
377
+ args: tc.args,
378
+ summary,
379
+ status: 'running',
380
+ stepIndex: state.info.stepIndex,
381
+ });
382
+ }
383
+ }
384
+ }
385
+ }
386
+ // If entry is DONE and was send_message, subagent is finished
387
+ if (entry.status === 'DONE' && state.info.status === 'done') {
388
+ newToolEvents.push({
389
+ subagentName: state.info.agentName,
390
+ conversationId: state.info.conversationId,
391
+ toolName: 'completed',
392
+ summary: 'Subagent finished',
393
+ status: 'completed',
394
+ stepIndex: state.info.stepIndex,
395
+ });
396
+ }
397
+ }
398
+ }
399
+ return hasUpdates;
400
+ }
401
+ catch (err) {
402
+ this.logger?.debug('antigravity.subagent.poll_error', {
403
+ conversationId: state.info.conversationId,
404
+ error: String(err),
405
+ });
406
+ return false;
407
+ }
408
+ }
409
+ /**
410
+ * Renders subagent progress for Telegram messages (watch mode or stream preview).
411
+ */
412
+ renderTelegramBlock(locale = 'zh') {
413
+ const all = this.getSubagents();
414
+ if (all.length === 0)
415
+ return '';
416
+ const lines = [];
417
+ const activeCount = all.filter((s) => s.status === 'running').length;
418
+ const headerTitle = locale === 'zh'
419
+ ? `🤖 <b>子 Agent 实时动态 (${activeCount > 0 ? `${activeCount} 个执行中` : `共 ${all.length} 个已完成`})</b>`
420
+ : `🤖 <b>Subagent Activity (${activeCount > 0 ? `${activeCount} running` : `${all.length} completed`})</b>`;
421
+ lines.push(headerTitle);
422
+ for (const sub of all) {
423
+ const isDone = sub.status === 'done';
424
+ const icon = isDone ? '✅' : '🔄';
425
+ const statusText = isDone
426
+ ? (locale === 'zh' ? '已完成' : 'Completed')
427
+ : (locale === 'zh' ? `步骤 #${sub.stepIndex} · 已执行 ${sub.toolCount} 项工具` : `Step #${sub.stepIndex} · ${sub.toolCount} tools`);
428
+ const name = escapeTelegramHtml(sub.agentName || 'Subagent');
429
+ lines.push(`${icon} <b>[${name}]</b> <i>(${statusText})</i>`);
430
+ if (!isDone && sub.currentTool) {
431
+ lines.push(` ⚙️ <b>正在执行</b>: <code>${escapeTelegramHtml(sub.currentTool)}</code>`);
432
+ }
433
+ else if (isDone && sub.lastAction) {
434
+ lines.push(` ↳ <i>${escapeTelegramHtml(sub.lastAction)}</i>`);
435
+ }
436
+ if (sub.recentTools.length > 0) {
437
+ const recent = sub.recentTools.slice(-3);
438
+ for (const t of recent) {
439
+ lines.push(` ↳ ${t}`);
440
+ }
441
+ }
442
+ }
443
+ return lines.join('\n');
444
+ }
445
+ /**
446
+ * One-line summary of what active subagents are currently doing.
447
+ */
448
+ getActiveSummaryLine(locale = 'zh') {
449
+ const active = this.getActiveSubagents();
450
+ if (active.length === 0)
451
+ return null;
452
+ const first = active[0];
453
+ const name = first.agentName || 'Subagent';
454
+ const action = first.currentTool || first.lastAction || (locale === 'zh' ? '正在执行' : 'Working');
455
+ return locale === 'zh'
456
+ ? `🤖 子 Agent [${name}] 正在执行: ${action}…`
457
+ : `🤖 Subagent [${name}] running: ${action}…`;
458
+ }
459
+ }
package/dist/config.d.ts CHANGED
@@ -19,6 +19,8 @@ export declare function resolveEnvPath(): string;
19
19
  export declare function getLoadedEnvPath(): string | null;
20
20
  export declare function loadEnv(): void;
21
21
  export interface AppConfig {
22
+ codexBotTokens: string[];
23
+ antigravityBotTokens: string[];
22
24
  tgBotToken: string;
23
25
  tgBotTokens: string[];
24
26
  tgMultiBotMode: boolean;
@@ -45,7 +47,6 @@ export interface AppConfig {
45
47
  opencodeServerStatePath: string;
46
48
  opencodeServerLogPath: string;
47
49
  antigravityBotToken: string | null;
48
- antigravityBotTokens: string[];
49
50
  antigravityMultiBotMode: boolean;
50
51
  antigravityDefaultRuntimeBotToken: string | null;
51
52
  antigravityHome: string | null;
@@ -110,6 +111,7 @@ export interface CodexApiProviderConfig {
110
111
  }
111
112
  export declare function loadConfig(): AppConfig;
112
113
  export declare function buildCodexApiProviderOverrides(providers: readonly CodexApiProviderConfig[], defaultProviderId?: string | null): string[];
114
+ export declare function validateCodexBotTokens(tokens: readonly string[]): void;
113
115
  export declare function validateOpencodeBotToken(token: string | null, codexTokens: readonly string[]): void;
114
116
  export declare function validateAntigravityBotTokens(tokens: readonly string[], codexTokens: readonly string[], opencodeToken: string | null): void;
115
117
  export declare function validateAntigravityBotToken(token: string | null, codexTokens: readonly string[], opencodeToken: string | null): void;
package/dist/config.js CHANGED
@@ -44,36 +44,35 @@ export function loadEnv() {
44
44
  }
45
45
  export function loadConfig() {
46
46
  loadEnv();
47
- const configuredTokens = parseCommaSeparatedIds(process.env.TG_BOT_TOKENS);
48
- const legacyToken = optional('TG_BOT_TOKEN');
49
- const tgDefaultRuntimeBotToken = selectDefaultRuntimeBotToken(configuredTokens, legacyToken);
50
- const tgBotTokens = configuredTokens.length > 0
51
- ? configuredTokens
52
- : legacyToken
53
- ? [legacyToken]
54
- : [];
55
- if (tgBotTokens.length === 0) {
56
- throw new Error('TG_BOT_TOKENS or TG_BOT_TOKEN is required');
47
+ if (process.env.TG_BOT_TOKEN?.trim() || process.env.TG_BOT_TOKENS?.trim()) {
48
+ throw new Error('Configuration error: TG_BOT_TOKEN and TG_BOT_TOKENS have been removed. Please update .env to use CODEX_BOT_TOKENS and/or ANTIGRAVITY_BOT_TOKENS.');
57
49
  }
50
+ const codexBotTokens = parseCommaSeparatedIds(process.env.CODEX_BOT_TOKENS || process.env.CODEX_BOT_TOKEN);
51
+ const antigravityBotTokens = parseCommaSeparatedIds(process.env.ANTIGRAVITY_BOT_TOKENS || process.env.ANTIGRAVITY_BOT_TOKEN);
58
52
  const opencodeBotToken = optional('OPENCODE_BOT_TOKEN');
59
- validateOpencodeBotToken(opencodeBotToken, tgBotTokens);
60
- const configuredAntigravityTokens = parseCommaSeparatedIds(process.env.ANTIGRAVITY_BOT_TOKENS);
61
- const legacyAntigravityToken = optional('ANTIGRAVITY_BOT_TOKEN');
62
- const antigravityDefaultRuntimeBotToken = selectDefaultRuntimeBotToken(configuredAntigravityTokens, legacyAntigravityToken);
63
- const antigravityBotTokens = configuredAntigravityTokens.length > 0
64
- ? configuredAntigravityTokens
65
- : legacyAntigravityToken
66
- ? [legacyAntigravityToken]
67
- : [];
53
+ if (codexBotTokens.length === 0 && antigravityBotTokens.length === 0) {
54
+ throw new Error('At least one Telegram bot token must be configured via CODEX_BOT_TOKENS or ANTIGRAVITY_BOT_TOKENS');
55
+ }
56
+ validateCodexBotTokens(codexBotTokens);
57
+ validateAntigravityBotTokens(antigravityBotTokens, codexBotTokens, opencodeBotToken);
58
+ validateOpencodeBotToken(opencodeBotToken, [...codexBotTokens, ...antigravityBotTokens]);
59
+ const codexDefaultRuntimeBotToken = selectDefaultRuntimeBotToken(codexBotTokens, optional('CODEX_DEFAULT_RUNTIME_BOT_TOKEN')) || codexBotTokens[0] || null;
60
+ const antigravityDefaultRuntimeBotToken = selectDefaultRuntimeBotToken(antigravityBotTokens, optional('ANTIGRAVITY_DEFAULT_RUNTIME_BOT_TOKEN')) ||
61
+ antigravityBotTokens[0] ||
62
+ null;
63
+ const totalBots = codexBotTokens.length + antigravityBotTokens.length;
64
+ const tgBotTokens = codexBotTokens.length > 0 ? codexBotTokens : antigravityBotTokens;
65
+ const tgBotToken = (codexBotTokens[0] || antigravityBotTokens[0]);
68
66
  const antigravityBotToken = antigravityBotTokens[0] || null;
69
- validateAntigravityBotTokens(antigravityBotTokens, tgBotTokens, opencodeBotToken);
70
67
  const config = {
71
- tgBotToken: tgBotTokens[0],
68
+ codexBotTokens,
69
+ antigravityBotTokens,
70
+ tgBotToken,
72
71
  tgBotTokens,
73
- tgMultiBotMode: configuredTokens.length > 0,
74
- tgDefaultRuntimeBotToken,
72
+ tgMultiBotMode: totalBots > 1,
73
+ tgDefaultRuntimeBotToken: codexDefaultRuntimeBotToken,
75
74
  tgScopeBotId: null,
76
- tgRequireExplicitGroupAddressing: configuredTokens.length > 1,
75
+ tgRequireExplicitGroupAddressing: totalBots > 1,
77
76
  tgAllowedUserId: required('TG_ALLOWED_USER_ID'),
78
77
  tgAllowedChatId: optional('TG_ALLOWED_CHAT_ID'),
79
78
  tgAllowedTopicId: nullableIntEnv('TG_ALLOWED_TOPIC_ID'),
@@ -94,8 +93,7 @@ export function loadConfig() {
94
93
  opencodeServerStatePath: process.env.OPENCODE_SERVER_STATE_PATH || DEFAULT_OPENCODE_SERVER_STATE_PATH,
95
94
  opencodeServerLogPath: process.env.OPENCODE_SERVER_LOG_PATH || DEFAULT_OPENCODE_SERVER_LOG_PATH,
96
95
  antigravityBotToken,
97
- antigravityBotTokens,
98
- antigravityMultiBotMode: configuredAntigravityTokens.length > 0,
96
+ antigravityMultiBotMode: antigravityBotTokens.length > 0,
99
97
  antigravityDefaultRuntimeBotToken,
100
98
  antigravityHome: process.env.ANTIGRAVITY_HOME?.trim() || null,
101
99
  antigravityCliBin: process.env.ANTIGRAVITY_CLI_BIN || resolveCommand('agy') || '/home/wuya/.local/bin/agy',
@@ -169,11 +167,20 @@ export function buildCodexApiProviderOverrides(providers, defaultProviderId = nu
169
167
  }
170
168
  return overrides;
171
169
  }
170
+ export function validateCodexBotTokens(tokens) {
171
+ const seen = new Set();
172
+ for (const token of tokens) {
173
+ if (seen.has(token)) {
174
+ throw new Error(`CODEX_BOT_TOKENS contains duplicate Telegram bot token: ${token}`);
175
+ }
176
+ seen.add(token);
177
+ }
178
+ }
172
179
  export function validateOpencodeBotToken(token, codexTokens) {
173
180
  if (!token)
174
181
  return;
175
182
  if (codexTokens.includes(token)) {
176
- throw new Error('OPENCODE_BOT_TOKEN must use a different bot from TG_BOT_TOKENS/TG_BOT_TOKEN');
183
+ throw new Error('OPENCODE_BOT_TOKEN must use a different bot from CODEX_BOT_TOKENS');
177
184
  }
178
185
  }
179
186
  export function validateAntigravityBotTokens(tokens, codexTokens, opencodeToken) {
@@ -186,10 +193,10 @@ export function validateAntigravityBotTokens(tokens, codexTokens, opencodeToken)
186
193
  }
187
194
  seen.add(token);
188
195
  if (codexTokens.includes(token)) {
189
- throw new Error('ANTIGRAVITY_BOT_TOKENS/ANTIGRAVITY_BOT_TOKEN must use a different bot from TG_BOT_TOKENS/TG_BOT_TOKEN');
196
+ throw new Error('ANTIGRAVITY_BOT_TOKENS must use a different bot from CODEX_BOT_TOKENS');
190
197
  }
191
198
  if (opencodeToken && token === opencodeToken) {
192
- throw new Error('ANTIGRAVITY_BOT_TOKENS/ANTIGRAVITY_BOT_TOKEN must use a different bot from OPENCODE_BOT_TOKEN');
199
+ throw new Error('ANTIGRAVITY_BOT_TOKENS must use a different bot from OPENCODE_BOT_TOKEN');
193
200
  }
194
201
  }
195
202
  }
@@ -71,6 +71,7 @@ export declare class BridgeSessionCore {
71
71
  private pendingForceTakeovers;
72
72
  private pendingLoginScopesById;
73
73
  private pendingAuthAddsByLoginId;
74
+ private authLoginRecoveryTimers;
74
75
  private latestTurnDiffs;
75
76
  private threadTokenUsageAlerts;
76
77
  private pendingAuthChoiceLists;
@@ -156,6 +157,9 @@ export declare class BridgeSessionCore {
156
157
  private handleServerRequestResolved;
157
158
  private handleAccountLoginCompleted;
158
159
  private restorePendingAuthAdd;
160
+ private scheduleAuthLoginRecovery;
161
+ private recoverAuthLogin;
162
+ private reconcileAuthLoginCredential;
159
163
  private restoreAuthAfterAddFailure;
160
164
  private handleAccountUpdated;
161
165
  private handleRateLimitsUpdated;