@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.
@@ -131,6 +131,7 @@ export class BridgeSessionCore {
131
131
  pendingForceTakeovers = new Map();
132
132
  pendingLoginScopesById = new Map();
133
133
  pendingAuthAddsByLoginId = new Map();
134
+ authLoginRecoveryTimers = new Map();
134
135
  latestTurnDiffs = new Map();
135
136
  threadTokenUsageAlerts = new Map();
136
137
  pendingAuthChoiceLists = new Map();
@@ -298,6 +299,9 @@ export class BridgeSessionCore {
298
299
  this.pendingLoginsByScope.clear();
299
300
  this.pendingLoginScopesById.clear();
300
301
  this.pendingAuthAddsByLoginId.clear();
302
+ for (const timer of this.authLoginRecoveryTimers.values())
303
+ clearTimeout(timer);
304
+ this.authLoginRecoveryTimers.clear();
301
305
  this.latestTurnDiffs.clear();
302
306
  this.threadTokenUsageAlerts.clear();
303
307
  this.pendingAuthChoiceLists.clear();
@@ -1812,6 +1816,15 @@ export class BridgeSessionCore {
1812
1816
  return;
1813
1817
  }
1814
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);
1815
1828
  this.pendingLoginScopesById.delete(loginId);
1816
1829
  if (this.pendingLoginsByScope.get(scopeId) === loginId) {
1817
1830
  this.pendingLoginsByScope.delete(scopeId);
@@ -1886,6 +1899,62 @@ export class BridgeSessionCore {
1886
1899
  const state = await this.listCodexAuthState();
1887
1900
  await this.restoreAuthAfterAddFailure(state.authDir, state.authPath, record.previousTargetPath);
1888
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
+ }
1889
1958
  async restoreAuthAfterAddFailure(authDir, authPath, previousTargetPath) {
1890
1959
  if (previousTargetPath) {
1891
1960
  await pointCodexAuthAtTarget(authDir, authPath, previousTargetPath);
@@ -5548,6 +5617,7 @@ export class BridgeSessionCore {
5548
5617
  mode: 'add',
5549
5618
  createdAt: Date.now(),
5550
5619
  });
5620
+ this.scheduleAuthLoginRecovery(login.loginId);
5551
5621
  await this.sendMessage(scopeId, [
5552
5622
  t(locale, 'auth_add_started', { value: candidateName }),
5553
5623
  t(locale, 'login_device_prereq'),
@@ -5591,6 +5661,7 @@ export class BridgeSessionCore {
5591
5661
  mode: 'repair',
5592
5662
  createdAt: Date.now(),
5593
5663
  });
5664
+ this.scheduleAuthLoginRecovery(login.loginId);
5594
5665
  await this.sendMessage(scopeId, [
5595
5666
  t(locale, 'auth_repair_started', { value: target.name }),
5596
5667
  t(locale, 'login_device_prereq'),
@@ -47,6 +47,7 @@ export interface EngineTurnExecution {
47
47
  on(event: 'result', listener: (result: EngineTurnResult) => void): void;
48
48
  on(event: 'error', listener: (error: Error) => void): void;
49
49
  on(event: 'exit', listener: (code: number | null) => void): void;
50
+ on(event: 'conversation', listener: (conversationId: string) => void): void;
50
51
  }
51
52
  export interface EngineTurnErrorContext {
52
53
  error: string;
@@ -79,9 +79,14 @@ export declare class UnifiedChannelOrchestrator {
79
79
  hasActiveTurn(scopeId: string): boolean;
80
80
  sendMessage(scopeId: string, text: string, keyboard?: InlineKeyboard): Promise<number>;
81
81
  editMessage(scopeId: string, messageId: number, text: string, keyboard?: InlineKeyboard): Promise<void>;
82
+ private safeDeliverChunks;
83
+ private safeDeliverMessage;
82
84
  handleText(event: TelegramTextEvent): Promise<void>;
83
85
  handleCallback(event: TelegramCallbackEvent): Promise<void>;
84
- startPromptTurn(event: TelegramTextEvent, prompt: string, locale: AppLocale): Promise<void>;
86
+ startPromptTurn(event: TelegramTextEvent, prompt: string, locale: AppLocale, options?: {
87
+ reuseMessageId?: number | undefined;
88
+ forcedThreadId?: string | undefined;
89
+ }): Promise<void>;
85
90
  private enqueuePromptTurn;
86
91
  private drainNextQueuedTurn;
87
92
  private executeTurn;