@foxden-app/foxclaw 0.7.2 → 0.10.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 +14 -0
- package/CHANGELOG.md +129 -0
- package/README.md +22 -2
- package/README_EN.md +22 -2
- package/dist/antigravity/adapter.d.ts +17 -0
- package/dist/antigravity/adapter.js +256 -0
- package/dist/antigravity/auth.d.ts +121 -0
- package/dist/antigravity/auth.js +896 -0
- package/dist/antigravity/client.d.ts +32 -0
- package/dist/antigravity/client.js +238 -0
- package/dist/antigravity/controller.d.ts +96 -0
- package/dist/antigravity/controller.js +1922 -0
- package/dist/antigravity/conversations.d.ts +29 -0
- package/dist/antigravity/conversations.js +289 -0
- package/dist/antigravity/events.d.ts +101 -0
- package/dist/antigravity/events.js +114 -0
- package/dist/antigravity/runtime.d.ts +47 -0
- package/dist/antigravity/runtime.js +82 -0
- package/dist/auth/cross_node_sync.js +2 -2
- package/dist/channels/telegram/telegram_channel_adapter.d.ts +10 -3
- package/dist/channels/telegram/telegram_channel_adapter.js +2 -1
- package/dist/channels/weixin/weixin_channel_adapter.d.ts +5 -1
- package/dist/codex_app/adapter.d.ts +20 -0
- package/dist/codex_app/adapter.js +213 -0
- package/dist/codex_app/client.d.ts +2 -1
- package/dist/codex_app/client.js +47 -4
- package/dist/codex_app/force_takeover.d.ts +12 -0
- package/dist/codex_app/force_takeover.js +27 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +42 -0
- package/dist/controller/controller.d.ts +15 -5
- package/dist/controller/controller.js +252 -20
- package/dist/core/attachments.d.ts +11 -0
- package/dist/core/attachments.js +56 -0
- package/dist/core/engine_spi.d.ts +76 -0
- package/dist/core/engine_spi.js +1 -0
- package/dist/core/orchestrator.d.ts +101 -0
- package/dist/core/orchestrator.js +1404 -0
- package/dist/core/stream_preview.d.ts +29 -0
- package/dist/core/stream_preview.js +146 -0
- package/dist/core/turn_queue.d.ts +19 -0
- package/dist/core/turn_queue.js +41 -0
- package/dist/i18n.d.ts +36 -6
- package/dist/i18n.js +81 -6
- package/dist/main.js +190 -20
- package/dist/opencode/adapter.d.ts +11 -0
- package/dist/opencode/adapter.js +168 -0
- package/dist/store/database.d.ts +41 -0
- package/dist/store/database.js +242 -43
- package/dist/store/token_usage.d.ts +23 -0
- package/dist/store/token_usage.js +61 -0
- package/dist/telegram/api.js +34 -1
- package/dist/telegram/bot_home.d.ts +3 -0
- package/dist/telegram/bot_home.js +107 -0
- package/dist/telegram/gateway.d.ts +5 -0
- package/dist/telegram/gateway.js +21 -0
- package/dist/types.d.ts +1 -0
- package/dist/update.d.ts +6 -0
- package/dist/update.js +78 -6
- package/dist/voice/target.js +2 -1
- package/dist/voice/tts.js +4 -2
- 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
- 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
- package/docs/user-manual.md +11 -1
- package/docs/zh/2026-09-05-bot-home-migration.md +26 -0
- package/docs/zh/2026-09-05-reliability-review.md +53 -0
- package/docs/zh/troubleshooting.md +12 -0
- package/docs/zh/user-manual.md +13 -1
- package/package.json +2 -2
- package/scripts/force-takeover.py +118 -0
- package/scripts/force-takeover.test.py +122 -0
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import type { BridgeSessionCore } from '../../controller/controller.js';
|
|
2
|
+
export interface ITelegramBridgeCore {
|
|
3
|
+
registerTelegramInboundHandlers(): void;
|
|
4
|
+
startCodexApp(): Promise<void>;
|
|
5
|
+
startTelegramPolling(): Promise<void>;
|
|
6
|
+
stop(): Promise<void>;
|
|
7
|
+
}
|
|
2
8
|
/**
|
|
3
|
-
* Telegram channel: inbound subscription + transport startup ordering for {@link BridgeSessionCore}
|
|
9
|
+
* Telegram channel: inbound subscription + transport startup ordering for {@link BridgeSessionCore}
|
|
10
|
+
* or {@link UnifiedBridgeCore}.
|
|
4
11
|
* Additional channels (e.g. Weixin) can compose the same core with their own adapters.
|
|
5
12
|
*/
|
|
6
13
|
export declare class TelegramChannelAdapter {
|
|
7
14
|
private readonly core;
|
|
8
|
-
constructor(core: BridgeSessionCore);
|
|
15
|
+
constructor(core: BridgeSessionCore | ITelegramBridgeCore);
|
|
9
16
|
start(): Promise<void>;
|
|
10
17
|
stop(): Promise<void>;
|
|
11
|
-
get sessionCore(): BridgeSessionCore;
|
|
18
|
+
get sessionCore(): BridgeSessionCore | ITelegramBridgeCore;
|
|
12
19
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram channel: inbound subscription + transport startup ordering for {@link BridgeSessionCore}
|
|
2
|
+
* Telegram channel: inbound subscription + transport startup ordering for {@link BridgeSessionCore}
|
|
3
|
+
* or {@link UnifiedBridgeCore}.
|
|
3
4
|
* Additional channels (e.g. Weixin) can compose the same core with their own adapters.
|
|
4
5
|
*/
|
|
5
6
|
export class TelegramChannelAdapter {
|
|
@@ -2,6 +2,10 @@ import type { AppConfig } from '../../config.js';
|
|
|
2
2
|
import type { BridgeSessionCore } from '../../controller/controller.js';
|
|
3
3
|
import type { Logger } from '../../logger.js';
|
|
4
4
|
import type { BridgeStore } from '../../store/database.js';
|
|
5
|
+
import type { TelegramTextEvent } from '../../telegram/gateway.js';
|
|
6
|
+
export interface IWeixinBridgeCore {
|
|
7
|
+
dispatchInboundLikeTelegramText(event: TelegramTextEvent): void;
|
|
8
|
+
}
|
|
5
9
|
export declare class WeixinChannelAdapter {
|
|
6
10
|
private readonly core;
|
|
7
11
|
private readonly store;
|
|
@@ -9,7 +13,7 @@ export declare class WeixinChannelAdapter {
|
|
|
9
13
|
private readonly logger;
|
|
10
14
|
private readonly abort;
|
|
11
15
|
private loops;
|
|
12
|
-
constructor(core: BridgeSessionCore, store: BridgeStore, config: AppConfig, logger: Logger);
|
|
16
|
+
constructor(core: BridgeSessionCore | IWeixinBridgeCore, store: BridgeStore, config: AppConfig, logger: Logger);
|
|
13
17
|
start(): Promise<void>;
|
|
14
18
|
stop(): Promise<void>;
|
|
15
19
|
private listAccounts;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IEngineAdapter, EngineModel, EngineTurnRequest, EngineTurnExecution } from '../core/engine_spi.js';
|
|
2
|
+
import type { CodexAppClient } from './client.js';
|
|
3
|
+
import type { SandboxModeValue } from '../types.js';
|
|
4
|
+
export declare class CodexEngineAdapter implements IEngineAdapter {
|
|
5
|
+
private readonly client;
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly name: string;
|
|
8
|
+
private readonly defaultModel;
|
|
9
|
+
private readonly defaultApprovalPolicy;
|
|
10
|
+
private readonly defaultSandboxMode;
|
|
11
|
+
constructor(client: CodexAppClient, options?: {
|
|
12
|
+
id?: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
defaultModel?: string | undefined;
|
|
15
|
+
defaultApprovalPolicy?: string;
|
|
16
|
+
defaultSandboxMode?: SandboxModeValue;
|
|
17
|
+
});
|
|
18
|
+
listModels(): Promise<EngineModel[]>;
|
|
19
|
+
executeTurn(request: EngineTurnRequest): EngineTurnExecution;
|
|
20
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { normalizeTurnActivityEvent } from '../controller/activity.js';
|
|
3
|
+
import { buildAttachmentPrompt } from '../telegram/media.js';
|
|
4
|
+
export class CodexEngineAdapter {
|
|
5
|
+
client;
|
|
6
|
+
id;
|
|
7
|
+
name;
|
|
8
|
+
defaultModel;
|
|
9
|
+
defaultApprovalPolicy;
|
|
10
|
+
defaultSandboxMode;
|
|
11
|
+
constructor(client, options = {}) {
|
|
12
|
+
this.client = client;
|
|
13
|
+
this.id = options.id ?? 'codex';
|
|
14
|
+
this.name = options.name ?? 'OpenAI Codex (App Server)';
|
|
15
|
+
this.defaultModel = options.defaultModel;
|
|
16
|
+
this.defaultApprovalPolicy = options.defaultApprovalPolicy ?? 'on-request';
|
|
17
|
+
this.defaultSandboxMode = options.defaultSandboxMode ?? 'workspace-write';
|
|
18
|
+
}
|
|
19
|
+
async listModels() {
|
|
20
|
+
try {
|
|
21
|
+
const models = await this.client.listModels();
|
|
22
|
+
const list = models.map((m) => ({
|
|
23
|
+
id: m.id,
|
|
24
|
+
name: m.displayName || m.id,
|
|
25
|
+
description: m.description,
|
|
26
|
+
isDefault: m.isDefault || m.id === this.defaultModel,
|
|
27
|
+
}));
|
|
28
|
+
if (!list.some((m) => m.id === 'gpt-6-sol')) {
|
|
29
|
+
list.push({
|
|
30
|
+
id: 'gpt-6-sol',
|
|
31
|
+
name: 'GPT-6-Sol',
|
|
32
|
+
description: 'Next-generation flagship model with deep reasoning',
|
|
33
|
+
isDefault: false,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return list;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
executeTurn(request) {
|
|
43
|
+
const emitter = new EventEmitter();
|
|
44
|
+
let threadId = request.threadId;
|
|
45
|
+
let turnId = null;
|
|
46
|
+
let cancelled = false;
|
|
47
|
+
let promptText = request.prompt;
|
|
48
|
+
if (request.stagedAttachments && request.stagedAttachments.length > 0 && !request.prompt.includes('Telegram attachments:')) {
|
|
49
|
+
promptText = buildAttachmentPrompt(request.prompt, request.stagedAttachments);
|
|
50
|
+
}
|
|
51
|
+
let accumulatedResponse = '';
|
|
52
|
+
let finalResult = null;
|
|
53
|
+
let turnError = null;
|
|
54
|
+
const resultWaiters = [];
|
|
55
|
+
const resolveWaiters = (res) => {
|
|
56
|
+
while (resultWaiters.length > 0) {
|
|
57
|
+
resultWaiters.shift()(res);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const onNotification = (msg) => {
|
|
61
|
+
try {
|
|
62
|
+
const ev = normalizeTurnActivityEvent(msg);
|
|
63
|
+
if (!ev)
|
|
64
|
+
return;
|
|
65
|
+
if (ev.turnId && turnId && ev.turnId !== turnId)
|
|
66
|
+
return;
|
|
67
|
+
if (ev.kind === 'agent_message_delta') {
|
|
68
|
+
accumulatedResponse += ev.delta;
|
|
69
|
+
emitter.emit('delta', ev.delta);
|
|
70
|
+
}
|
|
71
|
+
else if (ev.kind === 'tool_started') {
|
|
72
|
+
const cmdName = Array.isArray(ev.exec?.command) ? ev.exec.command.join(' ') : 'command';
|
|
73
|
+
emitter.emit('tool', {
|
|
74
|
+
name: cmdName,
|
|
75
|
+
status: 'running',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else if (ev.kind === 'tool_completed') {
|
|
79
|
+
const cmdName = Array.isArray(ev.exec?.command) ? ev.exec.command.join(' ') : 'command';
|
|
80
|
+
emitter.emit('tool', {
|
|
81
|
+
name: cmdName,
|
|
82
|
+
status: 'completed',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
else if (ev.kind === 'turn_completed') {
|
|
86
|
+
cleanup();
|
|
87
|
+
finalResult = {
|
|
88
|
+
kind: 'result',
|
|
89
|
+
status: ev.state === 'interrupted' ? 'INTERRUPTED' : 'SUCCESS',
|
|
90
|
+
response: accumulatedResponse,
|
|
91
|
+
conversationId: threadId,
|
|
92
|
+
};
|
|
93
|
+
emitter.emit('result', finalResult);
|
|
94
|
+
resolveWaiters(finalResult);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Ignore parsing errors for unrelated notifications
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const cleanup = () => {
|
|
102
|
+
this.client.off('notification', onNotification);
|
|
103
|
+
};
|
|
104
|
+
this.client.on('notification', onNotification);
|
|
105
|
+
const effectiveModel = request.model && request.model !== 'default'
|
|
106
|
+
? request.model
|
|
107
|
+
: (this.defaultModel && this.defaultModel !== 'default' ? this.defaultModel : null);
|
|
108
|
+
const run = async () => {
|
|
109
|
+
try {
|
|
110
|
+
if (!threadId) {
|
|
111
|
+
const session = await this.client.startThread({
|
|
112
|
+
cwd: request.cwd,
|
|
113
|
+
model: effectiveModel,
|
|
114
|
+
approvalPolicy: this.defaultApprovalPolicy,
|
|
115
|
+
sandboxMode: this.defaultSandboxMode,
|
|
116
|
+
});
|
|
117
|
+
threadId = session.thread.threadId;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
try {
|
|
121
|
+
await this.client.resumeThread({
|
|
122
|
+
threadId,
|
|
123
|
+
cwd: request.cwd,
|
|
124
|
+
approvalPolicy: this.defaultApprovalPolicy,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// thread might already be active on server
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (cancelled) {
|
|
132
|
+
cleanup();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!threadId) {
|
|
136
|
+
throw new Error('Failed to resolve or create Codex thread for turn');
|
|
137
|
+
}
|
|
138
|
+
let turn;
|
|
139
|
+
try {
|
|
140
|
+
turn = await this.client.startTurn({
|
|
141
|
+
threadId,
|
|
142
|
+
input: [{ type: 'text', text: promptText, text_elements: [] }],
|
|
143
|
+
cwd: request.cwd,
|
|
144
|
+
model: effectiveModel,
|
|
145
|
+
effort: request.effort ?? null,
|
|
146
|
+
serviceTier: request.serviceTier ?? undefined,
|
|
147
|
+
collaborationMode: null,
|
|
148
|
+
approvalPolicy: this.defaultApprovalPolicy,
|
|
149
|
+
sandboxMode: this.defaultSandboxMode,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch (turnErr) {
|
|
153
|
+
const errMsg = String(turnErr);
|
|
154
|
+
if (errMsg.includes('not found') ||
|
|
155
|
+
errMsg.includes('No thread') ||
|
|
156
|
+
errMsg.includes('stale') ||
|
|
157
|
+
errMsg.includes('invalid thread')) {
|
|
158
|
+
const session = await this.client.startThread({
|
|
159
|
+
cwd: request.cwd,
|
|
160
|
+
model: effectiveModel,
|
|
161
|
+
approvalPolicy: this.defaultApprovalPolicy,
|
|
162
|
+
sandboxMode: this.defaultSandboxMode,
|
|
163
|
+
});
|
|
164
|
+
threadId = session.thread.threadId;
|
|
165
|
+
turn = await this.client.startTurn({
|
|
166
|
+
threadId,
|
|
167
|
+
input: [{ type: 'text', text: promptText, text_elements: [] }],
|
|
168
|
+
cwd: request.cwd,
|
|
169
|
+
model: effectiveModel,
|
|
170
|
+
effort: request.effort ?? null,
|
|
171
|
+
serviceTier: request.serviceTier ?? undefined,
|
|
172
|
+
collaborationMode: null,
|
|
173
|
+
approvalPolicy: this.defaultApprovalPolicy,
|
|
174
|
+
sandboxMode: this.defaultSandboxMode,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
throw turnErr;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
turnId = turn.id;
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
cleanup();
|
|
185
|
+
turnError = err instanceof Error ? err : new Error(String(err));
|
|
186
|
+
emitter.emit('error', turnError);
|
|
187
|
+
resolveWaiters(null);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
void run();
|
|
191
|
+
return {
|
|
192
|
+
turnId: turnId ?? undefined,
|
|
193
|
+
cancel: () => {
|
|
194
|
+
cancelled = true;
|
|
195
|
+
if (threadId && turnId) {
|
|
196
|
+
void this.client.interruptTurn(threadId, turnId).catch(() => { });
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
waitForResult: async () => {
|
|
200
|
+
if (finalResult)
|
|
201
|
+
return finalResult;
|
|
202
|
+
if (turnError)
|
|
203
|
+
return null;
|
|
204
|
+
return new Promise((resolve) => {
|
|
205
|
+
resultWaiters.push(resolve);
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
on: (event, listener) => {
|
|
209
|
+
emitter.on(event, listener);
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -49,7 +49,7 @@ interface StartTurnOptions {
|
|
|
49
49
|
serviceTier?: string | null | undefined;
|
|
50
50
|
collaborationMode: CodexCollaborationMode | null;
|
|
51
51
|
}
|
|
52
|
-
interface CodexAppServerRuntimeStatus {
|
|
52
|
+
export interface CodexAppServerRuntimeStatus {
|
|
53
53
|
pid: number | null;
|
|
54
54
|
port: number | null;
|
|
55
55
|
running: boolean;
|
|
@@ -78,6 +78,7 @@ export declare class CodexAppClient extends EventEmitter {
|
|
|
78
78
|
private port;
|
|
79
79
|
private connected;
|
|
80
80
|
private userAgent;
|
|
81
|
+
private readonly requestTimeoutMs;
|
|
81
82
|
constructor(codexCliBin: string, launchCommand: string, autolaunch: boolean, serverStatePath: string, serverLogPath: string, logger: Logger, childEnv?: NodeJS.ProcessEnv | null, appServerConfigOverrides?: readonly string[]);
|
|
82
83
|
isConnected(): boolean;
|
|
83
84
|
getUserAgent(): string | null;
|
package/dist/codex_app/client.js
CHANGED
|
@@ -33,6 +33,7 @@ export class CodexAppClient extends EventEmitter {
|
|
|
33
33
|
port = null;
|
|
34
34
|
connected = false;
|
|
35
35
|
userAgent = null;
|
|
36
|
+
requestTimeoutMs = 30_000;
|
|
36
37
|
constructor(codexCliBin, launchCommand, autolaunch, serverStatePath, serverLogPath, logger, childEnv = null, appServerConfigOverrides = []) {
|
|
37
38
|
super();
|
|
38
39
|
this.codexCliBin = codexCliBin;
|
|
@@ -283,6 +284,18 @@ export class CodexAppClient extends EventEmitter {
|
|
|
283
284
|
models.push(...rows.map(mapModel));
|
|
284
285
|
cursor = typeof result.nextCursor === 'string' ? result.nextCursor : null;
|
|
285
286
|
} while (cursor);
|
|
287
|
+
if (!models.some((m) => m.id === 'gpt-6-sol' || m.model === 'gpt-6-sol')) {
|
|
288
|
+
models.push({
|
|
289
|
+
id: 'gpt-6-sol',
|
|
290
|
+
model: 'gpt-6-sol',
|
|
291
|
+
displayName: 'GPT-6-Sol',
|
|
292
|
+
description: 'Next-generation flagship model with deep reasoning',
|
|
293
|
+
isDefault: false,
|
|
294
|
+
supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
295
|
+
defaultReasoningEffort: 'medium',
|
|
296
|
+
serviceTiers: [],
|
|
297
|
+
});
|
|
298
|
+
}
|
|
286
299
|
return models;
|
|
287
300
|
}
|
|
288
301
|
async readAccount(refreshToken = false) {
|
|
@@ -577,6 +590,9 @@ export class CodexAppClient extends EventEmitter {
|
|
|
577
590
|
port: state.port,
|
|
578
591
|
error: error instanceof Error ? error.message : String(error),
|
|
579
592
|
});
|
|
593
|
+
if (isProcessAlive(state.pid)) {
|
|
594
|
+
throw new Error(`Managed Codex app-server pid ${state.pid} is alive but unreachable; retry the connection or explicitly restart it.`, { cause: error });
|
|
595
|
+
}
|
|
580
596
|
this.clearServerStateForPid(state.pid);
|
|
581
597
|
return false;
|
|
582
598
|
}
|
|
@@ -588,11 +604,18 @@ export class CodexAppClient extends EventEmitter {
|
|
|
588
604
|
try {
|
|
589
605
|
await new Promise((resolve, reject) => {
|
|
590
606
|
const ws = new WebSocket(url);
|
|
607
|
+
const timer = setTimeout(() => {
|
|
608
|
+
ws.close();
|
|
609
|
+
reject(new Error('WebSocket handshake timed out'));
|
|
610
|
+
}, 2000);
|
|
591
611
|
const onError = (event) => {
|
|
612
|
+
clearTimeout(timer);
|
|
592
613
|
ws.close();
|
|
593
614
|
reject(new Error(`WebSocket connect failed: ${String(event.type)}`));
|
|
594
615
|
};
|
|
595
616
|
ws.addEventListener('open', () => {
|
|
617
|
+
clearTimeout(timer);
|
|
618
|
+
ws.removeEventListener('error', onError);
|
|
596
619
|
this.socket = ws;
|
|
597
620
|
this.connected = true;
|
|
598
621
|
ws.addEventListener('message', message => this.handleMessage(String(message.data)));
|
|
@@ -608,7 +631,12 @@ export class CodexAppClient extends EventEmitter {
|
|
|
608
631
|
});
|
|
609
632
|
});
|
|
610
633
|
ws.addEventListener('error', err => {
|
|
611
|
-
this.logger.warn('codex.ws.error',
|
|
634
|
+
this.logger.warn('codex.ws.error', { message: err.message || 'WebSocket transport error', port: this.port });
|
|
635
|
+
if (this.socket === ws) {
|
|
636
|
+
this.socket = null;
|
|
637
|
+
ws.close();
|
|
638
|
+
this.handleDisconnect({ source: 'websocket-error', port: this.port });
|
|
639
|
+
}
|
|
612
640
|
});
|
|
613
641
|
resolve();
|
|
614
642
|
}, { once: true });
|
|
@@ -649,8 +677,22 @@ export class CodexAppClient extends EventEmitter {
|
|
|
649
677
|
}
|
|
650
678
|
const id = String(++this.requestId);
|
|
651
679
|
return new Promise((resolve, reject) => {
|
|
652
|
-
|
|
653
|
-
|
|
680
|
+
const timer = setTimeout(() => {
|
|
681
|
+
this.pending.delete(id);
|
|
682
|
+
this.logger.warn('codex.request_timeout', { method, timeoutMs: this.requestTimeoutMs });
|
|
683
|
+
reject(new Error(`Codex request timed out: ${method}. The result is unknown; check /status or use foxclaw resume before retrying.`));
|
|
684
|
+
}, this.requestTimeoutMs);
|
|
685
|
+
this.pending.set(id, {
|
|
686
|
+
resolve: (value) => { clearTimeout(timer); resolve(value); },
|
|
687
|
+
reject: (error) => { clearTimeout(timer); reject(error); },
|
|
688
|
+
});
|
|
689
|
+
try {
|
|
690
|
+
this.send({ jsonrpc: '2.0', id, method, params });
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
this.pending.get(id)?.reject(error);
|
|
694
|
+
this.pending.delete(id);
|
|
695
|
+
}
|
|
654
696
|
});
|
|
655
697
|
}
|
|
656
698
|
send(payload) {
|
|
@@ -690,6 +732,7 @@ export class CodexAppClient extends EventEmitter {
|
|
|
690
732
|
}
|
|
691
733
|
}
|
|
692
734
|
handleDisconnect(meta) {
|
|
735
|
+
this.logger.warn('codex.disconnected', meta);
|
|
693
736
|
if (this.connected) {
|
|
694
737
|
this.connected = false;
|
|
695
738
|
}
|
|
@@ -711,7 +754,7 @@ export class CodexAppClient extends EventEmitter {
|
|
|
711
754
|
this.reconnectTimer = setTimeout(async () => {
|
|
712
755
|
this.reconnectTimer = null;
|
|
713
756
|
try {
|
|
714
|
-
await this.
|
|
757
|
+
await this.start();
|
|
715
758
|
}
|
|
716
759
|
catch (error) {
|
|
717
760
|
this.logger.error('codex.reconnect_failed', { error: String(error) });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ExternalWriterIdentity {
|
|
2
|
+
pid: number;
|
|
3
|
+
startTime: string;
|
|
4
|
+
exe: string;
|
|
5
|
+
lockDevice: string;
|
|
6
|
+
lockInode: string;
|
|
7
|
+
cwd: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const externalWriterControl: {
|
|
10
|
+
inspect(home: string, threadId: string): Promise<ExternalWriterIdentity>;
|
|
11
|
+
stop(home: string, threadId: string, expected: ExternalWriterIdentity): Promise<void>;
|
|
12
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
const helper = fileURLToPath(new URL('../../scripts/force-takeover.py', import.meta.url));
|
|
6
|
+
async function runHelper(home, threadId, expected) {
|
|
7
|
+
if (process.platform !== 'linux')
|
|
8
|
+
throw new Error('Force takeover requires Linux/WSL with pidfd support');
|
|
9
|
+
const args = [helper, home, threadId];
|
|
10
|
+
if (expected)
|
|
11
|
+
args.push(JSON.stringify(expected));
|
|
12
|
+
const { stdout } = await execFileAsync('python3', args, { timeout: 12_000, maxBuffer: 16_384 });
|
|
13
|
+
const response = JSON.parse(stdout);
|
|
14
|
+
if (!response.ok)
|
|
15
|
+
throw new Error(response.error ?? 'External writer inspection failed');
|
|
16
|
+
return response.result;
|
|
17
|
+
}
|
|
18
|
+
export const externalWriterControl = {
|
|
19
|
+
async inspect(home, threadId) {
|
|
20
|
+
return await runHelper(home, threadId);
|
|
21
|
+
},
|
|
22
|
+
async stop(home, threadId, expected) {
|
|
23
|
+
const result = await runHelper(home, threadId, expected);
|
|
24
|
+
if (result.stopped !== true)
|
|
25
|
+
throw new Error('CLI stop was not confirmed');
|
|
26
|
+
},
|
|
27
|
+
};
|
package/dist/config.d.ts
CHANGED
|
@@ -9,7 +9,9 @@ export declare const DEFAULT_CODEX_APP_SERVER_STATE_PATH: string;
|
|
|
9
9
|
export declare const DEFAULT_CODEX_APP_SERVER_LOG_PATH: string;
|
|
10
10
|
export declare const DEFAULT_OPENCODE_SERVER_STATE_PATH: string;
|
|
11
11
|
export declare const DEFAULT_OPENCODE_SERVER_LOG_PATH: string;
|
|
12
|
+
export declare const DEFAULT_ANTIGRAVITY_AUTH_DIR: string;
|
|
12
13
|
export declare const DEFAULT_CODEX_TELEGRAM_HOME: string;
|
|
14
|
+
export declare const DEFAULT_ANTIGRAVITY_TELEGRAM_HOME: string;
|
|
13
15
|
export declare const DEFAULT_AUTH_SYNC_STATE_PATH: string;
|
|
14
16
|
export declare const DEFAULT_AUTH_SYNC_TEMP_DIR: string;
|
|
15
17
|
export declare const DEFAULT_ENV_PATH: string;
|
|
@@ -42,6 +44,14 @@ export interface AppConfig {
|
|
|
42
44
|
opencodeServerPassword: string | null;
|
|
43
45
|
opencodeServerStatePath: string;
|
|
44
46
|
opencodeServerLogPath: string;
|
|
47
|
+
antigravityBotToken: string | null;
|
|
48
|
+
antigravityBotTokens: string[];
|
|
49
|
+
antigravityMultiBotMode: boolean;
|
|
50
|
+
antigravityDefaultRuntimeBotToken: string | null;
|
|
51
|
+
antigravityHome: string | null;
|
|
52
|
+
antigravityCliBin: string;
|
|
53
|
+
antigravityAuthDir: string;
|
|
54
|
+
antigravityDefaultModel: string;
|
|
45
55
|
storePath: string;
|
|
46
56
|
logLevel: LogLevel;
|
|
47
57
|
defaultCwd: string;
|
|
@@ -101,6 +111,8 @@ export interface CodexApiProviderConfig {
|
|
|
101
111
|
export declare function loadConfig(): AppConfig;
|
|
102
112
|
export declare function buildCodexApiProviderOverrides(providers: readonly CodexApiProviderConfig[], defaultProviderId?: string | null): string[];
|
|
103
113
|
export declare function validateOpencodeBotToken(token: string | null, codexTokens: readonly string[]): void;
|
|
114
|
+
export declare function validateAntigravityBotTokens(tokens: readonly string[], codexTokens: readonly string[], opencodeToken: string | null): void;
|
|
115
|
+
export declare function validateAntigravityBotToken(token: string | null, codexTokens: readonly string[], opencodeToken: string | null): void;
|
|
104
116
|
export declare function parseCodexApiProviders(raw: string | undefined): CodexApiProviderConfig[];
|
|
105
117
|
export declare function selectDefaultRuntimeBotToken(configuredTokens: string[], legacyToken: string | null): string | null;
|
|
106
118
|
export declare function ensureAppDirs(config: AppConfig): void;
|
package/dist/config.js
CHANGED
|
@@ -12,7 +12,9 @@ export const DEFAULT_CODEX_APP_SERVER_STATE_PATH = path.join(APP_HOME, 'runtime'
|
|
|
12
12
|
export const DEFAULT_CODEX_APP_SERVER_LOG_PATH = path.join(APP_HOME, 'logs', 'codex-app-server.log');
|
|
13
13
|
export const DEFAULT_OPENCODE_SERVER_STATE_PATH = path.join(APP_HOME, 'runtime', 'opencode-server.json');
|
|
14
14
|
export const DEFAULT_OPENCODE_SERVER_LOG_PATH = path.join(APP_HOME, 'logs', 'opencode-server.log');
|
|
15
|
+
export const DEFAULT_ANTIGRAVITY_AUTH_DIR = path.join(os.homedir(), '.gemini', 'antigravity-cli');
|
|
15
16
|
export const DEFAULT_CODEX_TELEGRAM_HOME = path.join(APP_HOME, 'codex', 'telegram');
|
|
17
|
+
export const DEFAULT_ANTIGRAVITY_TELEGRAM_HOME = path.join(APP_HOME, 'antigravity', 'telegram');
|
|
16
18
|
export const DEFAULT_AUTH_SYNC_STATE_PATH = path.join(APP_HOME, 'runtime', 'auth-sync.json');
|
|
17
19
|
export const DEFAULT_AUTH_SYNC_TEMP_DIR = path.join(APP_HOME, 'runtime', 'auth-sync');
|
|
18
20
|
export const DEFAULT_ENV_PATH = path.join(APP_HOME, '.env');
|
|
@@ -55,6 +57,16 @@ export function loadConfig() {
|
|
|
55
57
|
}
|
|
56
58
|
const opencodeBotToken = optional('OPENCODE_BOT_TOKEN');
|
|
57
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
|
+
: [];
|
|
68
|
+
const antigravityBotToken = antigravityBotTokens[0] || null;
|
|
69
|
+
validateAntigravityBotTokens(antigravityBotTokens, tgBotTokens, opencodeBotToken);
|
|
58
70
|
const config = {
|
|
59
71
|
tgBotToken: tgBotTokens[0],
|
|
60
72
|
tgBotTokens,
|
|
@@ -81,6 +93,14 @@ export function loadConfig() {
|
|
|
81
93
|
opencodeServerPassword: optional('OPENCODE_SERVER_PASSWORD'),
|
|
82
94
|
opencodeServerStatePath: process.env.OPENCODE_SERVER_STATE_PATH || DEFAULT_OPENCODE_SERVER_STATE_PATH,
|
|
83
95
|
opencodeServerLogPath: process.env.OPENCODE_SERVER_LOG_PATH || DEFAULT_OPENCODE_SERVER_LOG_PATH,
|
|
96
|
+
antigravityBotToken,
|
|
97
|
+
antigravityBotTokens,
|
|
98
|
+
antigravityMultiBotMode: configuredAntigravityTokens.length > 0,
|
|
99
|
+
antigravityDefaultRuntimeBotToken,
|
|
100
|
+
antigravityHome: process.env.ANTIGRAVITY_HOME?.trim() || null,
|
|
101
|
+
antigravityCliBin: process.env.ANTIGRAVITY_CLI_BIN || resolveCommand('agy') || '/home/wuya/.local/bin/agy',
|
|
102
|
+
antigravityAuthDir: process.env.ANTIGRAVITY_AUTH_DIR?.trim() || DEFAULT_ANTIGRAVITY_AUTH_DIR,
|
|
103
|
+
antigravityDefaultModel: process.env.ANTIGRAVITY_DEFAULT_MODEL?.trim() || 'gemini-3.8-flash-high',
|
|
84
104
|
storePath: process.env.STORE_PATH || DEFAULT_STORE_PATH,
|
|
85
105
|
logLevel: parseLogLevel(process.env.LOG_LEVEL || 'info'),
|
|
86
106
|
defaultCwd: process.env.DEFAULT_CWD || process.cwd(),
|
|
@@ -156,6 +176,28 @@ export function validateOpencodeBotToken(token, codexTokens) {
|
|
|
156
176
|
throw new Error('OPENCODE_BOT_TOKEN must use a different bot from TG_BOT_TOKENS/TG_BOT_TOKEN');
|
|
157
177
|
}
|
|
158
178
|
}
|
|
179
|
+
export function validateAntigravityBotTokens(tokens, codexTokens, opencodeToken) {
|
|
180
|
+
if (tokens.length === 0)
|
|
181
|
+
return;
|
|
182
|
+
const seen = new Set();
|
|
183
|
+
for (const token of tokens) {
|
|
184
|
+
if (seen.has(token)) {
|
|
185
|
+
throw new Error(`ANTIGRAVITY_BOT_TOKENS contains duplicate Telegram bot token: ${token}`);
|
|
186
|
+
}
|
|
187
|
+
seen.add(token);
|
|
188
|
+
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');
|
|
190
|
+
}
|
|
191
|
+
if (opencodeToken && token === opencodeToken) {
|
|
192
|
+
throw new Error('ANTIGRAVITY_BOT_TOKENS/ANTIGRAVITY_BOT_TOKEN must use a different bot from OPENCODE_BOT_TOKEN');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
export function validateAntigravityBotToken(token, codexTokens, opencodeToken) {
|
|
197
|
+
if (!token)
|
|
198
|
+
return;
|
|
199
|
+
validateAntigravityBotTokens([token], codexTokens, opencodeToken);
|
|
200
|
+
}
|
|
159
201
|
export function parseCodexApiProviders(raw) {
|
|
160
202
|
if (!raw?.trim())
|
|
161
203
|
return [];
|
|
@@ -2,8 +2,8 @@ import type { AppConfig } from '../config.js';
|
|
|
2
2
|
import type { Logger } from '../logger.js';
|
|
3
3
|
import type { BridgeStore } from '../store/database.js';
|
|
4
4
|
import { type AuthSyncClusterAuditResult } from '../auth/cross_node_sync.js';
|
|
5
|
-
import type { RuntimeStatus } from '../types.js';
|
|
6
|
-
import type { TelegramGateway, TelegramTextEvent } from '../telegram/gateway.js';
|
|
5
|
+
import type { AppLocale, RuntimeStatus } from '../types.js';
|
|
6
|
+
import type { TelegramGateway, TelegramTextEvent, TelegramCallbackEvent } from '../telegram/gateway.js';
|
|
7
7
|
import { BridgeMessagingRouter } from '../channels/bridge_messaging_router.js';
|
|
8
8
|
import type { CodexAppClient } from '../codex_app/client.js';
|
|
9
9
|
import type { SelfUpdateRuntime, SelfUpdateStatus } from '../update.js';
|
|
@@ -66,6 +66,9 @@ export declare class BridgeSessionCore {
|
|
|
66
66
|
private pendingUserInputs;
|
|
67
67
|
private pendingMcpElicitations;
|
|
68
68
|
private pendingLoginsByScope;
|
|
69
|
+
private externalWriterControl;
|
|
70
|
+
private forceTakeoversInProgress;
|
|
71
|
+
private pendingForceTakeovers;
|
|
69
72
|
private pendingLoginScopesById;
|
|
70
73
|
private pendingAuthAddsByLoginId;
|
|
71
74
|
private latestTurnDiffs;
|
|
@@ -116,6 +119,7 @@ export declare class BridgeSessionCore {
|
|
|
116
119
|
* (used by the Weixin adapter).
|
|
117
120
|
*/
|
|
118
121
|
dispatchInboundLikeTelegramText(event: TelegramTextEvent): void;
|
|
122
|
+
hasPendingInteraction(scopeId: string): boolean;
|
|
119
123
|
/** Start Codex app-server transport and attach RPC listeners. */
|
|
120
124
|
startCodexApp(): Promise<void>;
|
|
121
125
|
/** Begin Telegram Bot API long-polling after handlers and Codex are ready. */
|
|
@@ -132,7 +136,7 @@ export declare class BridgeSessionCore {
|
|
|
132
136
|
private handleWatchCommand;
|
|
133
137
|
private handleThreadArchiveIndexCommand;
|
|
134
138
|
private handleThreadRenameIndexCommand;
|
|
135
|
-
|
|
139
|
+
handleCallback(event: TelegramCallbackEvent): Promise<void>;
|
|
136
140
|
private handleApprovalTextCommand;
|
|
137
141
|
private handleNotification;
|
|
138
142
|
private handleServerRequest;
|
|
@@ -287,6 +291,8 @@ export declare class BridgeSessionCore {
|
|
|
287
291
|
private applyObservedSessionEvents;
|
|
288
292
|
private applyObservedTurnSnapshot;
|
|
289
293
|
private handleTakeoverCommand;
|
|
294
|
+
private prepareForceTakeover;
|
|
295
|
+
private handleForceTakeoverCallback;
|
|
290
296
|
private handleQueueCommand;
|
|
291
297
|
private handleActiveTurnInboundMessage;
|
|
292
298
|
private queueObservedThreadMessage;
|
|
@@ -322,7 +328,7 @@ export declare class BridgeSessionCore {
|
|
|
322
328
|
private clearProactiveAuthRefreshTimer;
|
|
323
329
|
private runProactiveAuthRefresh;
|
|
324
330
|
private recordProactiveAuthRefreshStatus;
|
|
325
|
-
|
|
331
|
+
handleAuthCommand(scopeId: string, locale: AppLocale, args: string[]): Promise<void>;
|
|
326
332
|
private handleAuthSyncCommand;
|
|
327
333
|
private runAuthSafeSyncAll;
|
|
328
334
|
private runAuthClusterAudit;
|
|
@@ -341,6 +347,7 @@ export declare class BridgeSessionCore {
|
|
|
341
347
|
private handleVoiceCallback;
|
|
342
348
|
private sendVoiceForText;
|
|
343
349
|
private handleLoginDeviceCommand;
|
|
350
|
+
private loginCancelKeyboard;
|
|
344
351
|
private handleLoginCancelCommand;
|
|
345
352
|
private handleLogoutCommand;
|
|
346
353
|
private handleSteerCommand;
|
|
@@ -377,6 +384,7 @@ export declare class BridgeSessionCore {
|
|
|
377
384
|
private requireReadyBinding;
|
|
378
385
|
private handleAuthPanelActionCallback;
|
|
379
386
|
private handleAuthListViewCallback;
|
|
387
|
+
private refreshRepairedAuthChoice;
|
|
380
388
|
private handleAuthRepairMenuCallback;
|
|
381
389
|
private handleAuthRepairActionCallback;
|
|
382
390
|
private handleAuthToggleCallback;
|
|
@@ -437,7 +445,9 @@ export declare class BridgeSessionCore {
|
|
|
437
445
|
private handleNavigationCallback;
|
|
438
446
|
private showWherePanel;
|
|
439
447
|
private handleThreadListNavigationCallback;
|
|
440
|
-
|
|
448
|
+
showThreadsPanel(scopeId: string, messageId?: number, searchTerm?: string | null, locale?: AppLocale, options?: {
|
|
449
|
+
offset?: number;
|
|
450
|
+
}, archived?: boolean): Promise<void>;
|
|
441
451
|
private showModelSettingsPanel;
|
|
442
452
|
private showSetupPanel;
|
|
443
453
|
private showAccessSettingsPanel;
|