@leoustc/ai-runner-codex 0.1.0 → 0.1.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.
- package/.env.example +3 -0
- package/README.md +12 -0
- package/package.json +1 -1
- package/src/codex.js +1 -1
- package/src/config.js +3 -0
- package/src/hub.js +99 -4
package/.env.example
CHANGED
|
@@ -19,6 +19,9 @@ CODEX_SANDBOX=read-only
|
|
|
19
19
|
CODEX_APPROVAL_POLICY=never
|
|
20
20
|
CODEX_REQUEST_TIMEOUT_MS=120000
|
|
21
21
|
HEARTBEAT_INTERVAL_MS=60000
|
|
22
|
+
HUB_STALE_TIMEOUT_MS=180000
|
|
23
|
+
HUB_WATCHDOG_INTERVAL_MS=30000
|
|
24
|
+
OUTBOUND_QUEUE_MAX=100
|
|
22
25
|
RECONNECT_MIN_MS=2000
|
|
23
26
|
RECONNECT_MAX_MS=60000
|
|
24
27
|
LOG_LEVEL=info
|
package/README.md
CHANGED
|
@@ -65,6 +65,9 @@ CODEX_SANDBOX=read-only
|
|
|
65
65
|
CODEX_APPROVAL_POLICY=never
|
|
66
66
|
CODEX_REQUEST_TIMEOUT_MS=120000
|
|
67
67
|
HEARTBEAT_INTERVAL_MS=60000
|
|
68
|
+
HUB_STALE_TIMEOUT_MS=180000
|
|
69
|
+
HUB_WATCHDOG_INTERVAL_MS=30000
|
|
70
|
+
OUTBOUND_QUEUE_MAX=100
|
|
68
71
|
RECONNECT_MIN_MS=2000
|
|
69
72
|
RECONNECT_MAX_MS=60000
|
|
70
73
|
LOG_LEVEL=info
|
|
@@ -85,6 +88,15 @@ If Codex returns an auth/login error, the runner starts `CODEX_LOGIN_COMMAND`,
|
|
|
85
88
|
posts the device login URL/code back to the Hub as an agent message, waits for
|
|
86
89
|
login completion, then retries the Codex request once.
|
|
87
90
|
|
|
91
|
+
Hub WebSocket behavior:
|
|
92
|
+
|
|
93
|
+
- `HEARTBEAT_INTERVAL_MS` defaults to `60000`, so the runner sends a heartbeat
|
|
94
|
+
every 60 seconds while connected.
|
|
95
|
+
- If the socket closes or is detected stale, the runner reconnects with bounded
|
|
96
|
+
exponential backoff between `RECONNECT_MIN_MS` and `RECONNECT_MAX_MS`.
|
|
97
|
+
- Agent replies produced while the Hub socket is briefly down are queued up to
|
|
98
|
+
`OUTBOUND_QUEUE_MAX` and flushed after reconnect.
|
|
99
|
+
|
|
88
100
|
Prompt behavior:
|
|
89
101
|
|
|
90
102
|
- `ask`/`chat`: answer from workspace knowledge without editing files.
|
package/package.json
CHANGED
package/src/codex.js
CHANGED
package/src/config.js
CHANGED
|
@@ -64,8 +64,11 @@ export async function loadConfig(configPath) {
|
|
|
64
64
|
codexSandbox: env.CODEX_SANDBOX || "read-only",
|
|
65
65
|
codexApprovalPolicy: env.CODEX_APPROVAL_POLICY || "never",
|
|
66
66
|
heartbeatIntervalMs: numberValue(env.HEARTBEAT_INTERVAL_MS, 60_000),
|
|
67
|
+
hubStaleTimeoutMs: numberValue(env.HUB_STALE_TIMEOUT_MS, 180_000),
|
|
68
|
+
hubWatchdogIntervalMs: numberValue(env.HUB_WATCHDOG_INTERVAL_MS, 30_000),
|
|
67
69
|
logLevel: env.LOG_LEVEL || "info",
|
|
68
70
|
model: env.ROOM_AGENT_MODEL || "codex",
|
|
71
|
+
outboundQueueMax: numberValue(env.OUTBOUND_QUEUE_MAX, 100),
|
|
69
72
|
reconnectMaxMs: numberValue(env.RECONNECT_MAX_MS, 60_000),
|
|
70
73
|
reconnectMinMs: numberValue(env.RECONNECT_MIN_MS, 2_000),
|
|
71
74
|
role:
|
package/src/hub.js
CHANGED
|
@@ -7,9 +7,13 @@ export class HubClient {
|
|
|
7
7
|
this.config = config;
|
|
8
8
|
this.log = log;
|
|
9
9
|
this.heartbeatTimer = null;
|
|
10
|
+
this.lastSocketActivity = 0;
|
|
11
|
+
this.outboundQueue = [];
|
|
10
12
|
this.reconnectAttempt = 0;
|
|
13
|
+
this.reconnectTimer = null;
|
|
11
14
|
this.stopped = false;
|
|
12
15
|
this.socket = null;
|
|
16
|
+
this.watchdogTimer = null;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
start() {
|
|
@@ -19,10 +23,13 @@ export class HubClient {
|
|
|
19
23
|
stop() {
|
|
20
24
|
this.stopped = true;
|
|
21
25
|
this.stopHeartbeat();
|
|
26
|
+
this.stopWatchdog();
|
|
27
|
+
this.clearReconnectTimer();
|
|
22
28
|
this.socket?.close();
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
connect() {
|
|
32
|
+
this.clearReconnectTimer();
|
|
26
33
|
const url = new URL(this.config.roomWss);
|
|
27
34
|
url.protocol = url.protocol === "https:" ? "wss:" : url.protocol;
|
|
28
35
|
url.searchParams.set("connector", this.config.agentId);
|
|
@@ -35,23 +42,41 @@ export class HubClient {
|
|
|
35
42
|
this.socket = socket;
|
|
36
43
|
|
|
37
44
|
socket.addEventListener("open", () => {
|
|
45
|
+
if (this.socket !== socket) {
|
|
46
|
+
socket.close();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
this.reconnectAttempt = 0;
|
|
51
|
+
this.markSocketActivity();
|
|
39
52
|
this.log.info("hub websocket connected");
|
|
40
|
-
this.
|
|
53
|
+
this.sendNow(registration(this.config));
|
|
54
|
+
this.flushOutboundQueue();
|
|
41
55
|
this.startHeartbeat();
|
|
56
|
+
this.startWatchdog();
|
|
42
57
|
});
|
|
43
58
|
|
|
44
59
|
socket.addEventListener("message", (event) => {
|
|
60
|
+
this.markSocketActivity();
|
|
45
61
|
void this.handleMessage(event.data);
|
|
46
62
|
});
|
|
47
63
|
|
|
48
64
|
socket.addEventListener("close", (event) => {
|
|
65
|
+
if (this.socket !== socket) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
49
69
|
this.log.info(`hub websocket closed code=${event.code} reason=${event.reason || ""}`);
|
|
50
70
|
this.stopHeartbeat();
|
|
71
|
+
this.stopWatchdog();
|
|
51
72
|
this.scheduleReconnect();
|
|
52
73
|
});
|
|
53
74
|
|
|
54
75
|
socket.addEventListener("error", () => {
|
|
76
|
+
if (this.socket !== socket) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
55
80
|
this.log.info("hub websocket error");
|
|
56
81
|
});
|
|
57
82
|
}
|
|
@@ -105,7 +130,7 @@ export class HubClient {
|
|
|
105
130
|
|
|
106
131
|
startHeartbeat() {
|
|
107
132
|
this.stopHeartbeat();
|
|
108
|
-
const send = () => this.
|
|
133
|
+
const send = () => this.sendNow(heartbeat(this.config));
|
|
109
134
|
send();
|
|
110
135
|
this.heartbeatTimer = setInterval(send, this.config.heartbeatIntervalMs);
|
|
111
136
|
}
|
|
@@ -117,6 +142,30 @@ export class HubClient {
|
|
|
117
142
|
}
|
|
118
143
|
}
|
|
119
144
|
|
|
145
|
+
startWatchdog() {
|
|
146
|
+
this.stopWatchdog();
|
|
147
|
+
this.watchdogTimer = setInterval(() => {
|
|
148
|
+
if (this.socket?.readyState !== WebSocket.OPEN) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const idleMs = Date.now() - this.lastSocketActivity;
|
|
153
|
+
if (idleMs < this.config.hubStaleTimeoutMs) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.log.info(`hub websocket stale for ${idleMs}ms; reconnecting`);
|
|
158
|
+
this.socket.close();
|
|
159
|
+
}, this.config.hubWatchdogIntervalMs);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
stopWatchdog() {
|
|
163
|
+
if (this.watchdogTimer) {
|
|
164
|
+
clearInterval(this.watchdogTimer);
|
|
165
|
+
this.watchdogTimer = null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
120
169
|
scheduleReconnect() {
|
|
121
170
|
if (this.stopped) {
|
|
122
171
|
return;
|
|
@@ -131,19 +180,65 @@ export class HubClient {
|
|
|
131
180
|
const delay = base + jitter;
|
|
132
181
|
|
|
133
182
|
this.log.info(`reconnecting in ${delay}ms`);
|
|
134
|
-
setTimeout(() => {
|
|
183
|
+
this.reconnectTimer = setTimeout(() => {
|
|
184
|
+
this.reconnectTimer = null;
|
|
135
185
|
if (!this.stopped) {
|
|
136
186
|
this.connect();
|
|
137
187
|
}
|
|
138
188
|
}, delay);
|
|
139
189
|
}
|
|
140
190
|
|
|
191
|
+
clearReconnectTimer() {
|
|
192
|
+
if (this.reconnectTimer) {
|
|
193
|
+
clearTimeout(this.reconnectTimer);
|
|
194
|
+
this.reconnectTimer = null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
markSocketActivity() {
|
|
199
|
+
this.lastSocketActivity = Date.now();
|
|
200
|
+
}
|
|
201
|
+
|
|
141
202
|
send(value) {
|
|
142
203
|
if (this.socket?.readyState !== WebSocket.OPEN) {
|
|
204
|
+
this.queueOutbound(value);
|
|
143
205
|
return;
|
|
144
206
|
}
|
|
145
207
|
|
|
146
|
-
this.
|
|
208
|
+
if (!this.sendNow(value)) {
|
|
209
|
+
this.queueOutbound(value);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
sendNow(value) {
|
|
214
|
+
if (this.socket?.readyState !== WebSocket.OPEN) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
this.socket.send(JSON.stringify(value));
|
|
220
|
+
this.markSocketActivity();
|
|
221
|
+
return true;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
this.log.info(`hub websocket send failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
224
|
+
this.socket.close();
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
queueOutbound(value) {
|
|
230
|
+
if (this.outboundQueue.length >= this.config.outboundQueueMax) {
|
|
231
|
+
this.outboundQueue.shift();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this.outboundQueue.push(value);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
flushOutboundQueue() {
|
|
238
|
+
while (this.outboundQueue.length > 0 && this.socket?.readyState === WebSocket.OPEN) {
|
|
239
|
+
const value = this.outboundQueue.shift();
|
|
240
|
+
this.sendNow(value);
|
|
241
|
+
}
|
|
147
242
|
}
|
|
148
243
|
}
|
|
149
244
|
|