@foxden-app/foxclaw 0.7.3 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.env.example +22 -8
  2. package/CHANGELOG.md +134 -0
  3. package/README.md +21 -1
  4. package/README_EN.md +21 -1
  5. package/dist/antigravity/adapter.d.ts +18 -0
  6. package/dist/antigravity/adapter.js +394 -0
  7. package/dist/antigravity/auth.d.ts +126 -0
  8. package/dist/antigravity/auth.js +933 -0
  9. package/dist/antigravity/client.d.ts +32 -0
  10. package/dist/antigravity/client.js +244 -0
  11. package/dist/antigravity/controller.d.ts +101 -0
  12. package/dist/antigravity/controller.js +2007 -0
  13. package/dist/antigravity/conversations.d.ts +29 -0
  14. package/dist/antigravity/conversations.js +289 -0
  15. package/dist/antigravity/events.d.ts +102 -0
  16. package/dist/antigravity/events.js +143 -0
  17. package/dist/antigravity/runtime.d.ts +49 -0
  18. package/dist/antigravity/runtime.js +82 -0
  19. package/dist/antigravity/subagents.d.ts +66 -0
  20. package/dist/antigravity/subagents.js +459 -0
  21. package/dist/channels/telegram/telegram_channel_adapter.d.ts +10 -3
  22. package/dist/channels/telegram/telegram_channel_adapter.js +2 -1
  23. package/dist/channels/weixin/weixin_channel_adapter.d.ts +5 -1
  24. package/dist/codex_app/adapter.d.ts +20 -0
  25. package/dist/codex_app/adapter.js +213 -0
  26. package/dist/codex_app/client.d.ts +1 -1
  27. package/dist/codex_app/client.js +12 -0
  28. package/dist/config.d.ts +14 -0
  29. package/dist/config.js +65 -16
  30. package/dist/controller/controller.d.ts +12 -5
  31. package/dist/controller/controller.js +109 -8
  32. package/dist/core/attachments.d.ts +11 -0
  33. package/dist/core/attachments.js +56 -0
  34. package/dist/core/engine_spi.d.ts +77 -0
  35. package/dist/core/engine_spi.js +1 -0
  36. package/dist/core/orchestrator.d.ts +106 -0
  37. package/dist/core/orchestrator.js +1556 -0
  38. package/dist/core/stream_preview.d.ts +29 -0
  39. package/dist/core/stream_preview.js +146 -0
  40. package/dist/core/turn_queue.d.ts +19 -0
  41. package/dist/core/turn_queue.js +41 -0
  42. package/dist/i18n.d.ts +6 -0
  43. package/dist/i18n.js +49 -0
  44. package/dist/main.js +375 -446
  45. package/dist/opencode/adapter.d.ts +11 -0
  46. package/dist/opencode/adapter.js +169 -0
  47. package/dist/opencode/controller.js +2 -2
  48. package/dist/store/database.d.ts +44 -0
  49. package/dist/store/database.js +275 -43
  50. package/dist/store/token_usage.d.ts +23 -0
  51. package/dist/store/token_usage.js +61 -0
  52. package/dist/telegram/api.d.ts +1 -0
  53. package/dist/telegram/api.js +80 -3
  54. package/dist/telegram/bot_home.js +5 -4
  55. package/dist/telegram/gateway.d.ts +4 -0
  56. package/dist/telegram/gateway.js +11 -0
  57. package/dist/types.d.ts +3 -0
  58. package/dist/update.d.ts +6 -0
  59. package/dist/update.js +95 -7
  60. package/dist/voice/tts.js +4 -2
  61. package/docs/devlogs//346/212/200/346/234/257/351/200/232/350/256/257/02./347/273/237/344/270/200/351/200/232/351/201/223/350/260/203/345/272/246/345/231/250/344/270/216/345/217/257/346/217/222/346/213/224/345/274/225/346/223/216SPI/357/274/232FoxClaw/344/273/216/345/215/225/344/275/223/350/265/260/345/220/221/345/244/232Agent/345/272/225/345/272/247.md +192 -0
  62. package/docs/devlogs//347/254/254/344/270/200/344/272/272/347/247/260/347/211/210/02./347/257/207/344/272/214_/345/275/223/344/270/200/344/270/252/346/241/245/346/216/245/345/231/250/345/274/200/345/247/213/345/255/246/344/274/232/345/220/254/346/207/202/346/211/200/346/234/211Agent.md +45 -0
  63. package/package.json +2 -2
@@ -26,6 +26,7 @@ import { normalizeAccessPreset, resolveAccessMode } from './access.js';
26
26
  import { diffObservedTurn, findLatestTurn, findLiveTurn } from './observer.js';
27
27
  import { applySessionLog, bootstrapSessionLog, splitJsonlChunk, } from './session_observer.js';
28
28
  import { renderActiveTurnStatus } from './status.js';
29
+ import { formatMetricTokenCount } from '../store/token_usage.js';
29
30
  import { writeRuntimeStatus } from '../runtime.js';
30
31
  const AUTH_DELETE_REASON_NEEDS_REPAIR = 'needs_repair';
31
32
  const RESTART_PREVIEW_RECOVERY_RETRY_DELAYS_MS = [3000, 7000, 15_000, 30_000, 45_000, 60_000, 60_000, 60_000];
@@ -130,6 +131,7 @@ export class BridgeSessionCore {
130
131
  pendingForceTakeovers = new Map();
131
132
  pendingLoginScopesById = new Map();
132
133
  pendingAuthAddsByLoginId = new Map();
134
+ authLoginRecoveryTimers = new Map();
133
135
  latestTurnDiffs = new Map();
134
136
  threadTokenUsageAlerts = new Map();
135
137
  pendingAuthChoiceLists = new Map();
@@ -212,6 +214,12 @@ export class BridgeSessionCore {
212
214
  void this.handleAsyncError('channel.text', error, event.scopeId);
213
215
  });
214
216
  }
217
+ hasPendingInteraction(scopeId) {
218
+ return this.pendingThreadRenames.has(scopeId)
219
+ || this.pendingThreadNewCwds.has(scopeId)
220
+ || this.pendingUserInputs.has(scopeId)
221
+ || this.pendingForceTakeovers.has(scopeId);
222
+ }
215
223
  /** Start Codex app-server transport and attach RPC listeners. */
216
224
  async startCodexApp() {
217
225
  this.stopping = false;
@@ -291,6 +299,9 @@ export class BridgeSessionCore {
291
299
  this.pendingLoginsByScope.clear();
292
300
  this.pendingLoginScopesById.clear();
293
301
  this.pendingAuthAddsByLoginId.clear();
302
+ for (const timer of this.authLoginRecoveryTimers.values())
303
+ clearTimeout(timer);
304
+ this.authLoginRecoveryTimers.clear();
294
305
  this.latestTurnDiffs.clear();
295
306
  this.threadTokenUsageAlerts.clear();
296
307
  this.pendingAuthChoiceLists.clear();
@@ -324,12 +335,13 @@ export class BridgeSessionCore {
324
335
  this.updateStatus();
325
336
  }
326
337
  getRuntimeStatus() {
338
+ const appServer = typeof this.app.getServerStatus === 'function' ? this.app.getServerStatus() : null;
327
339
  return {
328
340
  running: true,
329
- connected: this.app.isConnected(),
330
- userAgent: this.app.getUserAgent(),
341
+ connected: typeof this.app.isConnected === 'function' ? this.app.isConnected() : false,
342
+ userAgent: typeof this.app.getUserAgent === 'function' ? this.app.getUserAgent() : null,
331
343
  codexHome: this.config.codexHome ?? path.join(os.homedir(), '.codex'),
332
- codexAppServer: this.app.getServerStatus(),
344
+ ...(appServer ? { codexAppServer: appServer } : {}),
333
345
  botUsername: this.botUsername,
334
346
  currentBindings: this.store.countBindings(),
335
347
  pendingApprovals: this.store.countPendingApprovals(),
@@ -559,6 +571,9 @@ export class BridgeSessionCore {
559
571
  if (codexUpdateLine) {
560
572
  lines.push(t(locale, 'status_last_codex_update', { value: codexUpdateLine }));
561
573
  }
574
+ if (lastUpdate.agyUpdate) {
575
+ lines.push(locale === 'zh' ? `Antigravity CLI:${lastUpdate.agyUpdate}` : `Antigravity CLI: ${lastUpdate.agyUpdate}`);
576
+ }
562
577
  }
563
578
  else {
564
579
  lines.push(t(locale, 'status_last_update_none'));
@@ -1801,6 +1816,15 @@ export class BridgeSessionCore {
1801
1816
  return;
1802
1817
  }
1803
1818
  const pendingAuthAdd = loginId ? this.pendingAuthAddsByLoginId.get(loginId) ?? null : null;
1819
+ if (params?.success && pendingAuthAdd)
1820
+ await this.reconcileAuthLoginCredential(pendingAuthAdd);
1821
+ // A recovery check and the server notification may complete concurrently.
1822
+ if (this.pendingLoginScopesById.get(loginId) !== scopeId)
1823
+ return;
1824
+ const timer = this.authLoginRecoveryTimers.get(loginId);
1825
+ if (timer)
1826
+ clearTimeout(timer);
1827
+ this.authLoginRecoveryTimers.delete(loginId);
1804
1828
  this.pendingLoginScopesById.delete(loginId);
1805
1829
  if (this.pendingLoginsByScope.get(scopeId) === loginId) {
1806
1830
  this.pendingLoginsByScope.delete(scopeId);
@@ -1875,6 +1899,62 @@ export class BridgeSessionCore {
1875
1899
  const state = await this.listCodexAuthState();
1876
1900
  await this.restoreAuthAfterAddFailure(state.authDir, state.authPath, record.previousTargetPath);
1877
1901
  }
1902
+ scheduleAuthLoginRecovery(loginId) {
1903
+ const timer = setTimeout(() => {
1904
+ this.authLoginRecoveryTimers.delete(loginId);
1905
+ void this.recoverAuthLogin(loginId).catch(error => {
1906
+ this.logger.warn('codex.auth_login_recovery_failed', { error: toErrorMeta(error) });
1907
+ }).finally(() => {
1908
+ if (!this.stopping && this.pendingAuthAddsByLoginId.has(loginId)) {
1909
+ this.scheduleAuthLoginRecovery(loginId);
1910
+ }
1911
+ });
1912
+ }, 5_000);
1913
+ timer.unref();
1914
+ this.authLoginRecoveryTimers.set(loginId, timer);
1915
+ }
1916
+ async recoverAuthLogin(loginId) {
1917
+ const record = this.pendingAuthAddsByLoginId.get(loginId);
1918
+ if (!record)
1919
+ return;
1920
+ if (Date.now() - record.createdAt >= 15 * 60_000) {
1921
+ await this.handleAccountLoginCompleted({ loginId, success: false, error: 'Device login timed out; completion was not confirmed.' });
1922
+ return;
1923
+ }
1924
+ await this.reconcileAuthLoginCredential(record);
1925
+ const metadata = await readChatGptAuthMetadata(record.path);
1926
+ if (!metadata || metadata.lastRefreshMs < record.createdAt
1927
+ || !chatGptAuthMetadataMatchesCandidateName(record.name, metadata))
1928
+ return;
1929
+ const account = await this.app.readAccount(true);
1930
+ const limits = await this.app.readAccountRateLimits();
1931
+ if (!account || !limits || !selectCodexRateLimitSnapshot(limits)
1932
+ || (metadata.email && account.email !== metadata.email))
1933
+ return;
1934
+ if (!this.pendingAuthAddsByLoginId.has(loginId))
1935
+ return;
1936
+ this.logger.info('codex.auth_login_recovered', { candidate: record.name });
1937
+ await this.handleAccountLoginCompleted({ loginId, success: true });
1938
+ }
1939
+ async reconcileAuthLoginCredential(record) {
1940
+ const authPath = path.join(this.resolveAuthDir(), 'auth.json');
1941
+ const [target, current] = await Promise.all([
1942
+ readChatGptAuthRecord(record.path), readChatGptAuthRecord(authPath),
1943
+ ]);
1944
+ // Codex can replace auth.json atomically, breaking our candidate symlink.
1945
+ // Only adopt a newly written credential for the same account identity.
1946
+ if (target && current && current.lastRefreshMs >= record.createdAt
1947
+ && current.quotaIdentityId === target.quotaIdentityId
1948
+ && current.lastRefreshMs > target.lastRefreshMs
1949
+ && chatGptAuthMetadataMatchesCandidateName(record.name, current)) {
1950
+ if (!this.pendingAuthAddsByLoginId.has(record.loginId))
1951
+ return;
1952
+ const temporary = `${record.path}.${process.pid}.tmp`;
1953
+ await fs.writeFile(temporary, current.raw, { mode: 0o600 });
1954
+ await fs.rename(temporary, record.path);
1955
+ await pointCodexAuthAtTarget(this.resolveAuthDir(), authPath, record.path);
1956
+ }
1957
+ }
1878
1958
  async restoreAuthAfterAddFailure(authDir, authPath, previousTargetPath) {
1879
1959
  if (previousTargetPath) {
1880
1960
  await pointCodexAuthAtTarget(authDir, authPath, previousTargetPath);
@@ -3481,7 +3561,9 @@ export class BridgeSessionCore {
3481
3561
  this.coordinator.statusUpdated(status);
3482
3562
  return;
3483
3563
  }
3484
- writeRuntimeStatus(this.config.statusPath, status);
3564
+ if (this.config.statusPath) {
3565
+ writeRuntimeStatus(this.config.statusPath, status);
3566
+ }
3485
3567
  }
3486
3568
  async sendMessage(scopeId, text, inlineKeyboard) {
3487
3569
  return this.sendRichMarkdownMessage(scopeId, text, inlineKeyboard);
@@ -5044,6 +5126,9 @@ export class BridgeSessionCore {
5044
5126
  const rows = [
5045
5127
  ['FoxClaw', `${status.fromVersion} -> ${status.toVersion ?? t(status.locale, 'unknown')}`, status.locale === 'zh' ? '升级完成,服务已重启' : 'Updated; service restarted'],
5046
5128
  ['Codex CLI', formatSelfUpdateVersionTransition(status.codexFromVersion, status.codexToVersion, status.locale), formatCodexUpdateState(status)],
5129
+ ...(status.agyFromVersion || status.agyUpdate ? [
5130
+ ['Antigravity (AGY)', formatSelfUpdateVersionTransition(status.agyFromVersion ?? null, status.agyToVersion ?? null, status.locale), status.agyUpdate ?? (status.locale === 'zh' ? '已检查更新' : 'Checked')]
5131
+ ] : []),
5047
5132
  [status.locale === 'zh' ? '集群广播' : 'Cluster broadcast', status.toVersion ?? t(status.locale, 'unknown'), broadcastLine],
5048
5133
  ];
5049
5134
  const notes = status.releaseNotes?.filter(note => note.trim()) ?? [];
@@ -5061,6 +5146,7 @@ export class BridgeSessionCore {
5061
5146
  telegramBold(title),
5062
5147
  escapeTelegramHtml(foxclawResult),
5063
5148
  escapeTelegramHtml(codexUpdateLine ?? (status.locale === 'zh' ? 'Codex CLI:未执行升级。' : 'Codex CLI: not updated.')),
5149
+ ...(status.agyUpdate ? [escapeTelegramHtml(`Antigravity CLI:${status.agyUpdate}`)] : []),
5064
5150
  escapeTelegramHtml(broadcastLine),
5065
5151
  releaseNotes ? escapeTelegramHtml(releaseNotes) : '',
5066
5152
  ].filter(Boolean).join('\n');
@@ -5531,6 +5617,7 @@ export class BridgeSessionCore {
5531
5617
  mode: 'add',
5532
5618
  createdAt: Date.now(),
5533
5619
  });
5620
+ this.scheduleAuthLoginRecovery(login.loginId);
5534
5621
  await this.sendMessage(scopeId, [
5535
5622
  t(locale, 'auth_add_started', { value: candidateName }),
5536
5623
  t(locale, 'login_device_prereq'),
@@ -5574,6 +5661,7 @@ export class BridgeSessionCore {
5574
5661
  mode: 'repair',
5575
5662
  createdAt: Date.now(),
5576
5663
  });
5664
+ this.scheduleAuthLoginRecovery(login.loginId);
5577
5665
  await this.sendMessage(scopeId, [
5578
5666
  t(locale, 'auth_repair_started', { value: target.name }),
5579
5667
  t(locale, 'login_device_prereq'),
@@ -7049,11 +7137,11 @@ export class BridgeSessionCore {
7049
7137
  events: formatTokenCount(stats.usageEvents),
7050
7138
  }),
7051
7139
  t(locale, 'status_codex_local_tokens', {
7052
- total: formatTokenCount(stats.totals.totalTokens),
7053
- input: formatTokenCount(stats.totals.inputTokens),
7140
+ total: formatCodexTokenCountWithMetric(stats.totals.totalTokens),
7141
+ input: formatCodexTokenCountWithMetric(stats.totals.inputTokens),
7054
7142
  visible: formatTokenCount(Math.max(0, stats.totals.outputTokens - stats.totals.reasoningOutputTokens)),
7055
- output: formatTokenCount(stats.totals.outputTokens),
7056
- cached: formatTokenCount(stats.totals.cachedInputTokens),
7143
+ output: formatCodexTokenCountWithMetric(stats.totals.outputTokens),
7144
+ cached: formatCodexTokenCountWithMetric(stats.totals.cachedInputTokens),
7057
7145
  reasoning: formatTokenCount(stats.totals.reasoningOutputTokens),
7058
7146
  }),
7059
7147
  ...this.formatCodexLocalResponseThroughputStatusLines(locale, stats),
@@ -7122,6 +7210,13 @@ export class BridgeSessionCore {
7122
7210
  this.localUsageCache = snapshot;
7123
7211
  this.localUsageCacheLoaded = true;
7124
7212
  await writeCodexLocalUsageSnapshot(this.codexLocalUsageSnapshotPath(), snapshot);
7213
+ this.store.setBackendCumulativeTokenUsage({
7214
+ inputTokens: stats.totals.inputTokens,
7215
+ outputTokens: stats.totals.outputTokens,
7216
+ cachedTokens: stats.totals.cachedInputTokens,
7217
+ totalTokens: stats.totals.totalTokens,
7218
+ turnsCount: stats.turns,
7219
+ }, 'codex');
7125
7220
  }
7126
7221
  codexLocalUsageSnapshotPath() {
7127
7222
  return path.join(path.dirname(this.config.statusPath), this.runtimeSnapshotFilename(CODEX_LOCAL_USAGE_SNAPSHOT_FILENAME));
@@ -12031,6 +12126,12 @@ function formatTokenCount(value) {
12031
12126
  }
12032
12127
  return Math.round(value).toLocaleString('en-US');
12033
12128
  }
12129
+ function formatCodexTokenCountWithMetric(value) {
12130
+ if (value >= 1_000_000) {
12131
+ return `${formatMetricTokenCount(value)} (${formatTokenCount(value)})`;
12132
+ }
12133
+ return formatTokenCount(value);
12134
+ }
12034
12135
  function formatLocalTimestamp(seconds) {
12035
12136
  const date = new Date(seconds * 1000);
12036
12137
  const pad = (value) => String(value).padStart(2, '0');
@@ -0,0 +1,11 @@
1
+ import type { Logger } from '../logger.js';
2
+ import { type StagedTelegramAttachment, type TelegramInboundAttachment } from '../telegram/media.js';
3
+ export interface TelegramFileDownloader {
4
+ getFile(fileId: string): Promise<{
5
+ file_id: string;
6
+ file_path?: string;
7
+ file_size?: number;
8
+ }>;
9
+ downloadResolvedFile(filePath: string, destination: string): Promise<unknown>;
10
+ }
11
+ export declare function stageInboundAttachments(downloader: TelegramFileDownloader, cwd: string, threadId: string, attachments: readonly TelegramInboundAttachment[], logger?: Logger): Promise<StagedTelegramAttachment[]>;
@@ -0,0 +1,56 @@
1
+ import fsPromises from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { planAttachmentStoragePath, isNativeImageAttachment, TELEGRAM_BOT_API_DOWNLOAD_LIMIT_BYTES, } from '../telegram/media.js';
4
+ export async function stageInboundAttachments(downloader, cwd, threadId, attachments, logger) {
5
+ const staged = [];
6
+ for (const attachment of attachments) {
7
+ try {
8
+ if (attachment.localPath) {
9
+ const planned = planAttachmentStoragePath(cwd, threadId, attachment, path.basename(attachment.localPath));
10
+ await fsPromises.mkdir(path.dirname(planned.localPath), { recursive: true });
11
+ await fsPromises.copyFile(attachment.localPath, planned.localPath);
12
+ const stat = await fsPromises.stat(planned.localPath);
13
+ const resolvedSize = stat.size;
14
+ staged.push({
15
+ ...attachment,
16
+ fileName: planned.fileName,
17
+ fileSize: resolvedSize,
18
+ localPath: planned.localPath,
19
+ relativePath: planned.relativePath,
20
+ nativeImage: isNativeImageAttachment(attachment),
21
+ });
22
+ continue;
23
+ }
24
+ const remoteFile = await downloader.getFile(attachment.fileId);
25
+ const resolvedSize = attachment.fileSize ?? remoteFile.file_size ?? null;
26
+ if (resolvedSize !== null && resolvedSize > TELEGRAM_BOT_API_DOWNLOAD_LIMIT_BYTES) {
27
+ logger?.warn('attachments.too_large', {
28
+ fileId: attachment.fileId,
29
+ size: resolvedSize,
30
+ });
31
+ continue;
32
+ }
33
+ if (!remoteFile.file_path) {
34
+ continue;
35
+ }
36
+ const planned = planAttachmentStoragePath(cwd, threadId, attachment, remoteFile.file_path);
37
+ await fsPromises.mkdir(path.dirname(planned.localPath), { recursive: true });
38
+ await downloader.downloadResolvedFile(remoteFile.file_path, planned.localPath);
39
+ staged.push({
40
+ ...attachment,
41
+ fileName: planned.fileName,
42
+ fileSize: resolvedSize,
43
+ localPath: planned.localPath,
44
+ relativePath: planned.relativePath,
45
+ nativeImage: isNativeImageAttachment(attachment),
46
+ });
47
+ }
48
+ catch (error) {
49
+ logger?.warn('attachments.download_failed', {
50
+ fileId: attachment.fileId,
51
+ error: error instanceof Error ? error.message : String(error),
52
+ });
53
+ }
54
+ }
55
+ return staged;
56
+ }
@@ -0,0 +1,77 @@
1
+ import type { StagedTelegramAttachment } from '../telegram/media.js';
2
+ import type { AppLocale } from '../types.js';
3
+ export interface EngineModel {
4
+ id: string;
5
+ name: string;
6
+ description?: string | undefined;
7
+ isDefault?: boolean | undefined;
8
+ }
9
+ export interface EngineTurnRequest {
10
+ scopeId: string;
11
+ prompt: string;
12
+ stagedAttachments?: StagedTelegramAttachment[] | undefined;
13
+ threadId: string | null;
14
+ cwd: string;
15
+ model: string;
16
+ effort?: string | null | undefined;
17
+ serviceTier?: string | null | undefined;
18
+ locale: AppLocale;
19
+ }
20
+ export interface EngineToolEvent {
21
+ name: string;
22
+ args?: Record<string, unknown> | string | undefined;
23
+ output?: string | undefined;
24
+ status?: 'running' | 'completed' | 'failed' | undefined;
25
+ stepIndex?: number | undefined;
26
+ summary?: string | undefined;
27
+ }
28
+ export interface EngineTurnResult {
29
+ kind: 'result';
30
+ status: 'SUCCESS' | 'ERROR' | 'INTERRUPTED';
31
+ response: string;
32
+ error?: string | undefined;
33
+ conversationId: string | null;
34
+ usage?: {
35
+ inputTokens?: number | undefined;
36
+ outputTokens?: number | undefined;
37
+ cachedTokens?: number | undefined;
38
+ totalTokens?: number | undefined;
39
+ } | undefined;
40
+ }
41
+ export interface EngineTurnExecution {
42
+ turnId?: string | undefined;
43
+ cancel(): void;
44
+ waitForResult(): Promise<EngineTurnResult | null>;
45
+ on(event: 'delta', listener: (text: string) => void): void;
46
+ on(event: 'tool', listener: (tool: EngineToolEvent) => void): void;
47
+ on(event: 'result', listener: (result: EngineTurnResult) => void): void;
48
+ on(event: 'error', listener: (error: Error) => void): void;
49
+ on(event: 'exit', listener: (code: number | null) => void): void;
50
+ on(event: 'conversation', listener: (conversationId: string) => void): void;
51
+ }
52
+ export interface EngineTurnErrorContext {
53
+ error: string;
54
+ request: EngineTurnRequest;
55
+ retryCount: number;
56
+ retryTurn: (retryCount: number) => Promise<void>;
57
+ sendMessage: (text: string) => Promise<number>;
58
+ editMessage: (messageId: number, text: string) => Promise<void>;
59
+ }
60
+ export interface IEngineAdapter {
61
+ readonly id: string;
62
+ readonly name: string;
63
+ listModels(): Promise<EngineModel[]>;
64
+ executeTurn(request: EngineTurnRequest): EngineTurnExecution;
65
+ preflightTurn?(request: EngineTurnRequest): Promise<void>;
66
+ handleTurnError?(context: EngineTurnErrorContext): Promise<boolean>;
67
+ }
68
+ export interface BackendDescriptor {
69
+ id: string;
70
+ name: string;
71
+ engineType: string;
72
+ adapter: IEngineAdapter;
73
+ account?: string | undefined;
74
+ details?: string | undefined;
75
+ isDefault?: boolean | undefined;
76
+ onSelect?: (scopeId: string) => Promise<void>;
77
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,106 @@
1
+ import type { AppConfig } from '../config.js';
2
+ import type { BridgeStore } from '../store/database.js';
3
+ import type { Logger } from '../logger.js';
4
+ import type { AppLocale } from '../types.js';
5
+ import type { TelegramGateway, TelegramTextEvent, TelegramCallbackEvent } from '../telegram/gateway.js';
6
+ import type { TelegramMessagingPort, InlineKeyboard } from '../channels/telegram/telegram_messaging_port.js';
7
+ import { TurnQueueManager } from './turn_queue.js';
8
+ import type { IEngineAdapter, EngineTurnExecution, BackendDescriptor } from './engine_spi.js';
9
+ export declare const STREAM_THROTTLE_MS = 700;
10
+ export declare const TYPING_INTERVAL_MS = 4000;
11
+ export interface UnifiedActiveTurn {
12
+ scopeId: string;
13
+ messageId: number;
14
+ threadId: string | null;
15
+ execution: EngineTurnExecution;
16
+ accumulatedText: string;
17
+ toolLines: string[];
18
+ flushTimer: NodeJS.Timeout | null;
19
+ lastFlushTime: number;
20
+ typingTimer: NodeJS.Timeout | null;
21
+ stepIndex?: number;
22
+ toolCount: number;
23
+ currentTool?: string | null;
24
+ startTime: number;
25
+ }
26
+ export interface EngineCustomUiHook {
27
+ renderCustomStatus?(scopeId: string, locale: AppLocale): Promise<string | null>;
28
+ renderCustomSetupRows?(scopeId: string, locale: AppLocale): Promise<InlineKeyboard>;
29
+ handleCustomCallback?(scopeId: string, data: string, locale: AppLocale, messageId?: number, event?: TelegramCallbackEvent): Promise<boolean>;
30
+ handleCustomCommand?(scopeId: string, command: string, args: string, locale: AppLocale, event?: TelegramTextEvent): Promise<boolean>;
31
+ handleCustomInbound?(event: TelegramTextEvent, locale: AppLocale): Promise<boolean>;
32
+ }
33
+ export declare class UnifiedChannelOrchestrator {
34
+ readonly config: AppConfig;
35
+ readonly store: BridgeStore;
36
+ readonly logger: Logger;
37
+ readonly bot: TelegramGateway;
38
+ readonly messaging: TelegramMessagingPort;
39
+ readonly queueManager: TurnQueueManager;
40
+ readonly customUi?: EngineCustomUiHook | undefined;
41
+ private readonly backends;
42
+ private readonly defaultBackendId;
43
+ private readonly activeTurns;
44
+ private readonly stalePanelDeleteTimers;
45
+ private readonly backendProvider?;
46
+ get adapter(): IEngineAdapter;
47
+ constructor(options: {
48
+ config: AppConfig;
49
+ store: BridgeStore;
50
+ logger: Logger;
51
+ bot: TelegramGateway;
52
+ adapter?: IEngineAdapter;
53
+ adapters?: Map<string, IEngineAdapter> | IEngineAdapter[];
54
+ backends?: BackendDescriptor[];
55
+ backendProvider?: (() => Promise<BackendDescriptor[]> | BackendDescriptor[]) | undefined;
56
+ defaultBackendId?: string;
57
+ messaging: TelegramMessagingPort;
58
+ customUi?: EngineCustomUiHook | undefined;
59
+ });
60
+ getAdapterForScope(scopeId: string): IEngineAdapter;
61
+ getBackendDescriptorForScope(scopeId: string): BackendDescriptor;
62
+ getAdapterForBackend(backendId: string): IEngineAdapter;
63
+ registerBackend(desc: BackendDescriptor): void;
64
+ listBackends(): Promise<BackendDescriptor[]>;
65
+ resolveBackendDescriptor(backendId: string): Promise<BackendDescriptor | null>;
66
+ syncCurrentBackendSettings(scopeId: string): void;
67
+ scheduleStalePanelDeletion(scopeId: string, messageId: number): void;
68
+ switchBackend(scopeId: string, targetBackendId: string, locale: AppLocale): Promise<{
69
+ previousBackendId: string;
70
+ newBackendId: string;
71
+ restoredThreadId: string | null;
72
+ cwd: string | null;
73
+ }>;
74
+ registerInboundHandlers(): void;
75
+ dispatchInboundLikeTelegramText(event: TelegramTextEvent): void;
76
+ start(): Promise<void>;
77
+ stop(): Promise<void>;
78
+ getActiveTurnsCount(): number;
79
+ hasActiveTurn(scopeId: string): boolean;
80
+ sendMessage(scopeId: string, text: string, keyboard?: InlineKeyboard): Promise<number>;
81
+ editMessage(scopeId: string, messageId: number, text: string, keyboard?: InlineKeyboard): Promise<void>;
82
+ private safeDeliverChunks;
83
+ private safeDeliverMessage;
84
+ handleText(event: TelegramTextEvent): Promise<void>;
85
+ handleCallback(event: TelegramCallbackEvent): Promise<void>;
86
+ startPromptTurn(event: TelegramTextEvent, prompt: string, locale: AppLocale, options?: {
87
+ reuseMessageId?: number | undefined;
88
+ forcedThreadId?: string | undefined;
89
+ }): Promise<void>;
90
+ private enqueuePromptTurn;
91
+ private drainNextQueuedTurn;
92
+ private executeTurn;
93
+ private handleEffortCommand;
94
+ private handleBoostCommand;
95
+ private handleActiveCommand;
96
+ private handleQueueCommand;
97
+ private handleSteerCommand;
98
+ handleInterrupt(scopeId: string, locale: AppLocale): Promise<void>;
99
+ handleNewSession(scopeId: string, locale: AppLocale, targetCwdInput?: string): Promise<void>;
100
+ sendStatus(scopeId: string, locale: AppLocale): Promise<void>;
101
+ sendSetupMenu(scopeId: string, locale: AppLocale, editMessageId?: number): Promise<void>;
102
+ sendModelsMenu(scopeId: string, locale: AppLocale, editMessageId?: number): Promise<void>;
103
+ sendHelp(scopeId: string, locale: AppLocale): Promise<void>;
104
+ sendBackendMenu(scopeId: string, locale: AppLocale, editMessageId?: number): Promise<void>;
105
+ handleBackendCommand(scopeId: string, argsString: string, locale: AppLocale): Promise<void>;
106
+ }