@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.
package/.env.example CHANGED
@@ -1,11 +1,12 @@
1
- # Required for new installs: one or more bot tokens for this host.
2
- # Comma-separated bot tokens. By default, each bot gets an independent Codex runtime/auth selection.
3
- # Homes use ~/.foxclaw/codex/telegram/@TelegramUsername/home (old numeric paths remain as links).
4
- TG_BOT_TOKENS=<telegram_bot_token>
5
- # Backward-compatible single-runtime setup.
6
- # In TG_BOT_TOKENS mode, if this exact token is also present in TG_BOT_TOKENS,
7
- # that bot uses the default terminal CODEX_HOME/auth instead of an isolated home.
8
- # TG_BOT_TOKEN=<telegram_bot_token>
1
+ # Required: one or more Telegram bot tokens for Codex execution.
2
+ # Comma-separated bot tokens. The 1st bot shares the default terminal CODEX_HOME (~/.codex).
3
+ # Additional bots receive independent isolated homes (~/.foxclaw/codex/telegram/@TelegramUsername/home).
4
+ CODEX_BOT_TOKENS=<codex_telegram_bot_token>
5
+
6
+ # Optional: one or more Telegram bot tokens for Google Antigravity execution.
7
+ # Comma-separated bot tokens. The 1st bot shares the default Antigravity home (~/.gemini/antigravity-cli).
8
+ # Additional bots receive independent isolated homes (~/.foxclaw/antigravity/telegram/@TelegramUsername/home).
9
+ # ANTIGRAVITY_BOT_TOKENS=<antigravity_telegram_bot_token>
9
10
 
10
11
  # Required: only this Telegram user can control the bridge
11
12
  TG_ALLOWED_USER_ID=<telegram_user_id>
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  All notable FoxClaw changes are listed here. Each release note is bilingual so GitHub Releases and the npm package are useful to both Chinese and English readers.
4
4
 
5
+ ## 0.11.0 - 2026-09-30
6
+
7
+ ### 中文
8
+ - **多后端 Telegram Bot 变量重构与后端原生隔离**:
9
+ - 弃用原有的 `TG_BOT_TOKEN`/`TG_BOT_TOKENS`,引入面向具体执行后端的 `CODEX_BOT_TOKENS` 与 `ANTIGRAVITY_BOT_TOKENS`。
10
+ - 每类后端的首个 Bot 默认与本地终端 CLI 共享主目录(Codex 共享 `~/.codex`,Antigravity 共享 `~/.gemini/antigravity-cli`),后续实例自动创建隔离主目录。
11
+ - 隔离目录路径统一升级为人类可读格式(如 `@WallianBot`、`@waxiaosheBot`),彻底告别原有的裸数字 ID 目录。
12
+ - **作用域会话继承与迁移容错增强**:
13
+ - 修复多 Bot 模式下旧未命名空间会话因历史脏数据导致无法自动迁移的问题;加入 `updated_at` 时间戳比对,自动无缝承接最新正在执行的任务线程。
14
+ - 增强 `scope_backend_bindings` 的自动迁移与跨引擎会话保持能力。
15
+ - **网络鲁棒性与服务重启自愈**:
16
+ - 强化 Telegram 长轮询与分片投递在网络断连、代理抖动时的自动恢复机制。
17
+ - 修复服务重启后活跃任务的断点自动续跑逻辑。
18
+
19
+ ### English
20
+ - **Backend-Centric Bot Variables & Native Home Isolation**:
21
+ - Deprecated legacy `TG_BOT_TOKEN`/`TG_BOT_TOKENS` in favor of backend-specific `CODEX_BOT_TOKENS` and `ANTIGRAVITY_BOT_TOKENS`.
22
+ - The first bot of each backend type shares the default terminal CLI home (`~/.codex` for Codex, `~/.gemini/antigravity-cli` for Antigravity), while additional bots receive fully isolated homes.
23
+ - Isolated directories are now standardized to human-readable Telegram usernames (e.g. `@WallianBot`, `@waxiaosheBot`).
24
+ - **Resilient Scope Migration & Session Inheritance**:
25
+ - Resolved scope migration conflicts where stale historical records blocked recent unnamespaced turns from being adopted under multi-bot mode.
26
+ - Added timestamp-based migration for `chat_bindings` and `scope_backend_bindings` to ensure seamless continuation of active sessions across restarts.
27
+ - **Network Resilience & Service Auto-Recovery**:
28
+ - Enhanced polling and chunk delivery retry mechanisms during transient network resets and proxy timeouts.
29
+
5
30
  ## 0.10.0 - 2026-09-27
6
31
 
7
32
  ### 中文
@@ -9,7 +9,8 @@ export declare class AntigravityEngineAdapter implements IEngineAdapter {
9
9
  private readonly defaultModel;
10
10
  private readonly auth;
11
11
  private readonly logger;
12
- constructor(client: AntigravityAppClient, defaultModel?: string, auth?: AntigravityAuthManager, logger?: Logger);
12
+ private readonly baseDir;
13
+ constructor(client: AntigravityAppClient, defaultModel?: string, auth?: AntigravityAuthManager, logger?: Logger, baseDir?: string);
13
14
  listModels(): Promise<EngineModel[]>;
14
15
  executeTurn(request: EngineTurnRequest): EngineTurnExecution;
15
16
  preflightTurn(_request: EngineTurnRequest): Promise<void>;
@@ -1,8 +1,9 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import path from 'node:path';
3
3
  import { normalizeAgyModelId } from './client.js';
4
- import { isCapacityOrUnavailableError, isVerificationError, isQuotaOrAuthError, } from './events.js';
4
+ import { isCapacityOrUnavailableError, isVerificationError, isQuotaOrAuthError, isTransientNetworkError, } from './events.js';
5
5
  import { buildAttachmentPrompt } from '../telegram/media.js';
6
+ import { AntigravitySubagentTracker } from './subagents.js';
6
7
  function extractToolSummary(name, parameters) {
7
8
  if (!parameters)
8
9
  return undefined;
@@ -39,11 +40,13 @@ export class AntigravityEngineAdapter {
39
40
  defaultModel;
40
41
  auth;
41
42
  logger;
42
- constructor(client, defaultModel = 'gemini-3.8-flash', auth, logger) {
43
+ baseDir;
44
+ constructor(client, defaultModel = 'gemini-3.8-flash', auth, logger, baseDir) {
43
45
  this.client = client;
44
46
  this.defaultModel = defaultModel;
45
47
  this.auth = auth;
46
48
  this.logger = logger;
49
+ this.baseDir = baseDir ?? auth?.authDir;
47
50
  }
48
51
  async listModels() {
49
52
  const rawModels = await this.client.listModels();
@@ -98,7 +101,67 @@ export class AntigravityEngineAdapter {
98
101
  model: targetModel,
99
102
  effort: request.effort ?? null,
100
103
  });
104
+ let subagentTracker = null;
105
+ let pollTimer = null;
106
+ let turnFinished = false;
107
+ const ensureTracker = (convId) => {
108
+ if (!convId || subagentTracker)
109
+ return;
110
+ subagentTracker = new AntigravitySubagentTracker(convId, this.baseDir, this.logger);
111
+ };
112
+ if (request.threadId) {
113
+ ensureTracker(request.threadId);
114
+ }
115
+ const stopSubagentPolling = () => {
116
+ turnFinished = true;
117
+ if (pollTimer) {
118
+ clearInterval(pollTimer);
119
+ pollTimer = null;
120
+ }
121
+ };
122
+ const startSubagentPolling = () => {
123
+ if (pollTimer || turnFinished)
124
+ return;
125
+ pollTimer = setInterval(async () => {
126
+ if (turnFinished) {
127
+ stopSubagentPolling();
128
+ return;
129
+ }
130
+ if (!subagentTracker && agTurn.conversationId) {
131
+ ensureTracker(agTurn.conversationId);
132
+ }
133
+ if (subagentTracker) {
134
+ try {
135
+ const res = await subagentTracker.poll();
136
+ if (res.newToolEvents && res.newToolEvents.length > 0) {
137
+ for (const ev of res.newToolEvents) {
138
+ emitter.emit('tool', {
139
+ name: `[${ev.subagentName}] ${ev.toolName}`,
140
+ args: ev.args,
141
+ status: ev.status,
142
+ stepIndex: ev.stepIndex,
143
+ summary: ev.summary,
144
+ });
145
+ }
146
+ }
147
+ }
148
+ catch {
149
+ // ignore polling errors
150
+ }
151
+ }
152
+ }, 1000);
153
+ pollTimer.unref?.();
154
+ };
155
+ startSubagentPolling();
156
+ let emittedConvId = null;
101
157
  agTurn.on('event', (ev) => {
158
+ if (ev.conversationId) {
159
+ ensureTracker(ev.conversationId);
160
+ if (emittedConvId !== ev.conversationId) {
161
+ emittedConvId = ev.conversationId;
162
+ emitter.emit('conversation', ev.conversationId);
163
+ }
164
+ }
102
165
  switch (ev.kind) {
103
166
  case 'text':
104
167
  emitter.emit('delta', ev.delta);
@@ -114,6 +177,7 @@ export class AntigravityEngineAdapter {
114
177
  });
115
178
  break;
116
179
  case 'result': {
180
+ stopSubagentPolling();
117
181
  const res = {
118
182
  kind: 'result',
119
183
  status: ev.status === 'SUCCESS' ? 'SUCCESS' : 'ERROR',
@@ -135,33 +199,45 @@ export class AntigravityEngineAdapter {
135
199
  }
136
200
  });
137
201
  agTurn.on('error', (err) => {
202
+ stopSubagentPolling();
138
203
  emitter.emit('error', err);
139
204
  });
140
205
  agTurn.on('exit', (code) => {
206
+ stopSubagentPolling();
141
207
  emitter.emit('exit', code);
142
208
  });
143
209
  return {
144
- turnId: agTurn.conversationId ?? undefined,
145
- cancel: () => agTurn.cancel(),
210
+ get turnId() {
211
+ return agTurn.conversationId ?? undefined;
212
+ },
213
+ cancel: () => {
214
+ stopSubagentPolling();
215
+ agTurn.cancel();
216
+ },
146
217
  waitForResult: async () => {
147
- const ev = await agTurn.waitForResult();
148
- if (!ev || ev.kind !== 'result')
149
- return null;
150
- return {
151
- kind: 'result',
152
- status: ev.status === 'SUCCESS' ? 'SUCCESS' : 'ERROR',
153
- response: ev.response || ev.error || (ev.status === 'ERROR' ? 'Antigravity execution failed' : ''),
154
- error: ev.error || (ev.status === 'ERROR' ? 'Antigravity execution failed' : undefined),
155
- conversationId: ev.conversationId,
156
- usage: ev.usage
157
- ? {
158
- inputTokens: ev.usage.input_tokens,
159
- outputTokens: ev.usage.output_tokens,
160
- cachedTokens: ev.usage.cache_read_tokens,
161
- totalTokens: ev.usage.total_tokens,
162
- }
163
- : undefined,
164
- };
218
+ try {
219
+ const ev = await agTurn.waitForResult();
220
+ if (!ev || ev.kind !== 'result')
221
+ return null;
222
+ return {
223
+ kind: 'result',
224
+ status: ev.status === 'SUCCESS' ? 'SUCCESS' : 'ERROR',
225
+ response: ev.response || ev.error || (ev.status === 'ERROR' ? 'Antigravity execution failed' : ''),
226
+ error: ev.error || (ev.status === 'ERROR' ? 'Antigravity execution failed' : undefined),
227
+ conversationId: ev.conversationId,
228
+ usage: ev.usage
229
+ ? {
230
+ inputTokens: ev.usage.input_tokens,
231
+ outputTokens: ev.usage.output_tokens,
232
+ cachedTokens: ev.usage.cache_read_tokens,
233
+ totalTokens: ev.usage.total_tokens,
234
+ }
235
+ : undefined,
236
+ };
237
+ }
238
+ finally {
239
+ stopSubagentPolling();
240
+ }
165
241
  },
166
242
  on: (event, listener) => {
167
243
  emitter.on(event, listener);
@@ -195,17 +271,22 @@ export class AntigravityEngineAdapter {
195
271
  }
196
272
  const rotated = await this.auth.rotateNextCandidate(failingName);
197
273
  const nextAccountName = rotated.account.email || rotated.account.name;
198
- await ctx.sendMessage(ctx.request.locale === 'zh'
199
- ? `🛡️ **检测到 Google 安全验证风控 (Verification Required)**\n\n` +
200
- `• **受限账号**: \`${failingEmail}\` (已自动暂停并冷却)\n` +
201
- `• **故障恢复**: 已无感切换至备用账号 \`${nextAccountName}\`,正在继续执行您的任务…\n\n` +
202
- `💡 **如何人工解除该账号风控**:\n` +
203
- `1. 可在终端/CLI 执行 \`agy -p "hi"\`,在弹出的浏览器完成人机验证;\n` +
204
- `2. 或在 Telegram 发送 \`/login\` 重新授权该账号后即可恢复。`
205
- : `🛡️ **Google Security Verification Triggered (Verification Required)**\n\n` +
206
- `• **Affected**: \`${failingEmail}\` (paused & cooling)\n` +
207
- `• **Failover**: Switched to \`${nextAccountName}\`, continuing your task…\n\n` +
208
- `💡 **How to resolve**: Complete verification via \`agy -p "hi"\` in browser or run \`/login\` in Telegram.`);
274
+ try {
275
+ await ctx.sendMessage(ctx.request.locale === 'zh'
276
+ ? `🛡️ **检测到 Google 安全验证风控 (Verification Required)**\n\n` +
277
+ `• **受限账号**: \`${failingEmail}\` (已自动暂停并冷却)\n` +
278
+ `• **故障恢复**: 已无感切换至备用账号 \`${nextAccountName}\`,正在继续执行您的任务…\n\n` +
279
+ `💡 **如何人工解除该账号风控**:\n` +
280
+ `1. 可在终端/CLI 执行 \`agy -p "hi"\`,在弹出的浏览器完成人机验证;\n` +
281
+ `2. 或在 Telegram 发送 \`/login\` 重新授权该账号后即可恢复。`
282
+ : `🛡️ **Google Security Verification Triggered (Verification Required)**\n\n` +
283
+ `• **Affected**: \`${failingEmail}\` (paused & cooling)\n` +
284
+ `• **Failover**: Switched to \`${nextAccountName}\`, continuing your task…\n\n` +
285
+ `💡 **How to resolve**: Complete verification via \`agy -p "hi"\` in browser or run \`/login\` in Telegram.`);
286
+ }
287
+ catch {
288
+ /* messaging may fail during network glitch */
289
+ }
209
290
  await ctx.retryTurn(ctx.retryCount + 1);
210
291
  return true;
211
292
  }
@@ -224,9 +305,14 @@ export class AntigravityEngineAdapter {
224
305
  this.auth.markCooldown(active.email, 30 * 60 * 1000);
225
306
  }
226
307
  const rotated = await this.auth.rotateNextCandidate(active?.name);
227
- await ctx.sendMessage(ctx.request.locale === 'zh'
228
- ? `⚠️ 检测到当前账号配额超限或凭据失效,已标记进入 30 分钟冷却,自动切换到账号 \`${rotated.account.email || rotated.account.name}\` 并重试…`
229
- : `⚠️ Quota or auth issue detected. Marked account into 30m cooldown, switched to \`${rotated.account.email || rotated.account.name}\` and retrying…`);
308
+ try {
309
+ await ctx.sendMessage(ctx.request.locale === 'zh'
310
+ ? `⚠️ 检测到当前账号配额超限或凭据失效,已标记进入 30 分钟冷却,自动切换到账号 \`${rotated.account.email || rotated.account.name}\` 并重试…`
311
+ : `⚠️ Quota or auth issue detected. Marked account into 30m cooldown, switched to \`${rotated.account.email || rotated.account.name}\` and retrying…`);
312
+ }
313
+ catch {
314
+ /* messaging may fail during network glitch */
315
+ }
230
316
  await ctx.retryTurn(ctx.retryCount + 1);
231
317
  return true;
232
318
  }
@@ -238,17 +324,69 @@ export class AntigravityEngineAdapter {
238
324
  }
239
325
  const isCapacity = isCapacityOrUnavailableError(ctx.error);
240
326
  if (isCapacity && ctx.retryCount < 2) {
241
- await ctx.sendMessage(ctx.request.locale === 'zh'
242
- ? `⏳ Google 模型服务临时繁忙 (503 Unavailable),等待 2 秒后自动重试 (第 ${ctx.retryCount + 1}/2 次)…`
243
- : `⏳ Google model temporarily unavailable (503), retrying in 2s (attempt ${ctx.retryCount + 1}/2)…`);
327
+ try {
328
+ await ctx.sendMessage(ctx.request.locale === 'zh'
329
+ ? `⏳ Google 模型服务临时繁忙 (503 Unavailable),等待 2 秒后自动重试 (第 ${ctx.retryCount + 1}/2 次)…`
330
+ : `⏳ Google model temporarily unavailable (503), retrying in 2s (attempt ${ctx.retryCount + 1}/2)…`);
331
+ }
332
+ catch {
333
+ /* messaging may fail during network glitch */
334
+ }
244
335
  await new Promise((r) => setTimeout(r, 2000));
245
336
  await ctx.retryTurn(ctx.retryCount + 1);
246
337
  return true;
247
338
  }
248
339
  if (isCapacity) {
249
- await ctx.sendMessage(ctx.request.locale === 'zh'
250
- ? `⚠️ **Google 模型容量繁忙 (503 UNAVAILABLE)**\n\nGoogle 服务端当前模型 (\`${ctx.request.model}\`) 暂时无可用容量。\n\n💡 **建议方案**:\n• 稍等 1 分钟后重试\n• 发送 /setup 或 /models 切换为 \`gemini-3.1-pro-high\` 继续`
251
- : `⚠️ **Model Capacity Unavailable (503)**\n\nGoogle servers have no capacity for \`${ctx.request.model}\` right now.\n\n💡 Tip: Wait 1 minute or switch model via /setup or /models.`);
340
+ try {
341
+ await ctx.sendMessage(ctx.request.locale === 'zh'
342
+ ? `⚠️ **Google 模型容量繁忙 (503 UNAVAILABLE)**\n\nGoogle 服务端当前模型 (\`${ctx.request.model}\`) 暂时无可用容量。\n\n💡 **建议方案**:\n• 稍等 1 分钟后重试\n• 发送 /setup 或 /models 切换为 \`gemini-3.1-pro-high\` 继续`
343
+ : `⚠️ **Model Capacity Unavailable (503)**\n\nGoogle servers have no capacity for \`${ctx.request.model}\` right now.\n\n💡 Tip: Wait 1 minute or switch model via /setup or /models.`);
344
+ }
345
+ catch {
346
+ /* messaging may fail during network glitch */
347
+ }
348
+ return true;
349
+ }
350
+ const isNetwork = isTransientNetworkError(ctx.error);
351
+ if (isNetwork && ctx.retryCount < 3) {
352
+ const backoffMs = Math.min(15000, 3000 * Math.pow(2, ctx.retryCount));
353
+ const delaySec = Math.round(backoffMs / 1000);
354
+ try {
355
+ await ctx.sendMessage(ctx.request.locale === 'zh'
356
+ ? `🌐 **检测到网络波动或连接中断**\n\n` +
357
+ `• **错误提示**: \`${ctx.error.slice(0, 100)}\`\n` +
358
+ `• **安全重试**: 将在 ${delaySec} 秒后自动重试并在网络恢复后继续执行 (第 ${ctx.retryCount + 1}/3 次)…`
359
+ : `🌐 **Network Fluctuation Detected**\n\n` +
360
+ `• **Error**: \`${ctx.error.slice(0, 100)}\`\n` +
361
+ `• **Auto-Retry**: Retrying in ${delaySec}s to resume execution after network recovery (attempt ${ctx.retryCount + 1}/3)…`);
362
+ }
363
+ catch {
364
+ /* messaging may fail during network glitch */
365
+ }
366
+ await new Promise((r) => setTimeout(r, backoffMs));
367
+ try {
368
+ await ctx.retryTurn(ctx.retryCount + 1);
369
+ }
370
+ catch (err) {
371
+ this.logger?.error('antigravity.retry_turn_failed', { error: String(err) });
372
+ }
373
+ return true;
374
+ }
375
+ if (isNetwork) {
376
+ try {
377
+ await ctx.sendMessage(ctx.request.locale === 'zh'
378
+ ? `⚠️ **网络连接连续中断 (已重试 3 次)**\n\n` +
379
+ `• **错误详情**: \`${ctx.error.slice(0, 150)}\`\n` +
380
+ `• **说明**: 代理或网络暂时无法稳定连接 Google 服务端。\n\n` +
381
+ `💡 **建议方案**:\n• 请检查本机网络与代理设置(如 Clash / v2ray / proxy)\n• 网络恢复后直接再次发送您的指令即可继续`
382
+ : `⚠️ **Network Connection Failed (Retried 3 times)**\n\n` +
383
+ `• **Error**: \`${ctx.error.slice(0, 150)}\`\n` +
384
+ `• **Note**: Unable to reach Google servers stably.\n\n` +
385
+ `💡 Tip: Please check your network/proxy connection, and send your message again once restored.`);
386
+ }
387
+ catch {
388
+ /* messaging may fail during network glitch */
389
+ }
252
390
  return true;
253
391
  }
254
392
  return false;
@@ -119,3 +119,8 @@ export declare class AntigravityAuthManager {
119
119
  private atomicCopy;
120
120
  private atomicWriteJson;
121
121
  }
122
+ /**
123
+ * Links Antigravity OAuth tokens and paused account state from sourceAuthDir into targetAuthDir.
124
+ * Does NOT link conversation_summaries.db or brain/ to keep conversation history isolated per bot.
125
+ */
126
+ export declare function linkAntigravityAuthTokens(sourceAuthDir: string, targetAuthDir: string): void;
@@ -894,3 +894,40 @@ export class AntigravityAuthManager {
894
894
  await fs.rename(tempPath, destPath);
895
895
  }
896
896
  }
897
+ /**
898
+ * Links Antigravity OAuth tokens and paused account state from sourceAuthDir into targetAuthDir.
899
+ * Does NOT link conversation_summaries.db or brain/ to keep conversation history isolated per bot.
900
+ */
901
+ export function linkAntigravityAuthTokens(sourceAuthDir, targetAuthDir) {
902
+ const resolvedSource = path.resolve(sourceAuthDir);
903
+ const resolvedTarget = path.resolve(targetAuthDir);
904
+ if (resolvedSource === resolvedTarget)
905
+ return;
906
+ fsSync.mkdirSync(resolvedTarget, { recursive: true, mode: 0o700 });
907
+ if (!fsSync.existsSync(resolvedSource))
908
+ return;
909
+ const entries = fsSync.readdirSync(resolvedSource, { withFileTypes: true });
910
+ for (const entry of entries) {
911
+ if (entry.isFile() &&
912
+ (entry.name === 'antigravity-oauth-token' ||
913
+ entry.name.startsWith('antigravity-oauth-token_') ||
914
+ entry.name === 'paused-accounts.json')) {
915
+ const sourceFile = path.join(resolvedSource, entry.name);
916
+ const targetFile = path.join(resolvedTarget, entry.name);
917
+ try {
918
+ if (fsSync.existsSync(targetFile) || fsSync.lstatSync(targetFile, { throwIfNoEntry: false })) {
919
+ fsSync.unlinkSync(targetFile);
920
+ }
921
+ fsSync.symlinkSync(sourceFile, targetFile);
922
+ }
923
+ catch {
924
+ try {
925
+ fsSync.copyFileSync(sourceFile, targetFile);
926
+ }
927
+ catch {
928
+ // best-effort fallback
929
+ }
930
+ }
931
+ }
932
+ }
933
+ }
@@ -183,8 +183,14 @@ export class AntigravityAppClient {
183
183
  conversationId: activeConversationId,
184
184
  stderr: stderrBuffer ? stderrBuffer.slice(-200) : undefined,
185
185
  });
186
- if (!finalResultEvent && code !== 0) {
187
- const errorMsg = stderrBuffer.trim() || `Process exited with code ${code}${signal ? ` (${signal})` : ''}`;
186
+ if (!finalResultEvent) {
187
+ const isCanceled = signal === 'SIGINT' || signal === 'SIGTERM';
188
+ const errorMsg = isCanceled
189
+ ? 'Execution canceled'
190
+ : (stderrBuffer.trim() ||
191
+ (code !== 0
192
+ ? `Process exited with code ${code}${signal ? ` (${signal})` : ''}`
193
+ : 'Process exited unexpectedly before producing a result'));
188
194
  const errResult = {
189
195
  kind: 'result',
190
196
  conversationId: activeConversationId || '',
@@ -12,6 +12,7 @@ import type { CodexAppClient, CodexAppServerRuntimeStatus } from '../codex_app/c
12
12
  import { OpencodeEngineAdapter } from '../opencode/adapter.js';
13
13
  import type { OpencodeAppClient } from '../opencode/client.js';
14
14
  import { BridgeSessionCore } from '../controller/controller.js';
15
+ import type { SelfUpdateRuntime } from '../update.js';
15
16
  export interface UnifiedBridgeRuntimeStatus {
16
17
  running: boolean;
17
18
  connected: boolean;
@@ -33,6 +34,7 @@ export interface UnifiedBridgeCoreOptions {
33
34
  opencodeApp?: OpencodeAppClient | undefined;
34
35
  opencodeAdapter?: OpencodeEngineAdapter | undefined;
35
36
  defaultBackendId?: 'antigravity' | 'codex' | 'opencode' | string | undefined;
37
+ selfUpdater?: SelfUpdateRuntime | undefined;
36
38
  }
37
39
  export declare class UnifiedBridgeCore {
38
40
  private readonly config;
@@ -49,6 +51,7 @@ export declare class UnifiedBridgeCore {
49
51
  readonly codexCore?: BridgeSessionCore | undefined;
50
52
  private readonly opencodeAdapter?;
51
53
  private readonly defaultBackendId;
54
+ private readonly selfUpdater?;
52
55
  private readonly orchestrator;
53
56
  private readonly watchers;
54
57
  private readonly pendingAgyRenames;
@@ -87,6 +90,8 @@ export declare class UnifiedBridgeCore {
87
90
  private watchConversation;
88
91
  private unwatchConversation;
89
92
  private scheduleWatcherPoll;
93
+ private getScopeLocale;
94
+ private renderWatcherProgressUpdate;
90
95
  private pollWatcher;
91
96
  private handleWatcherTranscriptEntry;
92
97
  private sendAuthMenu;