androdex 1.1.5 → 1.1.6
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/README.md +21 -1
- package/package.json +1 -1
- package/src/bridge.js +25 -12
- package/src/codex-desktop-refresher.js +187 -98
- package/src/macos-launch-agent.js +9 -0
- package/src/scripts/codex-refresh.applescript +6 -2
- package/src/codex-desktop-thread-sync.js +0 -34
package/README.md
CHANGED
|
@@ -102,12 +102,32 @@ npm install
|
|
|
102
102
|
npm start
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
## Release
|
|
106
|
+
|
|
107
|
+
Publish the npm package from this directory, not from the repository root:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
cd androdex-bridge
|
|
111
|
+
npm test
|
|
112
|
+
npm pack --dry-run
|
|
113
|
+
npm version patch --no-git-tag-version
|
|
114
|
+
npm publish --access public
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Verify the published version after the release:
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
npm view androdex version
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
If your npm account requires write-time 2FA, rerun the publish command with `--otp=<code>`.
|
|
124
|
+
|
|
105
125
|
## Manual Smoke Checklist
|
|
106
126
|
|
|
107
127
|
1. Run `androdex up` and confirm the Android app can pair successfully.
|
|
108
128
|
2. Run `androdex up` inside a workspace and confirm the host keeps Codex bound to that local project.
|
|
109
129
|
3. From Android, open an existing thread and create a new one to confirm the remote client flow still works end to end.
|
|
110
|
-
4. If desktop refresh is enabled, verify phone-authored thread activity updates the host Codex desktop
|
|
130
|
+
4. If desktop refresh is enabled, verify phone-authored thread activity updates the host Codex desktop via the Settings-bounce remount workaround.
|
|
111
131
|
5. Restart the launchd service or reconnect the phone and confirm the saved pairing and active workspace recover without losing host-local state.
|
|
112
132
|
|
|
113
133
|
## Project status
|
package/package.json
CHANGED
package/src/bridge.js
CHANGED
|
@@ -11,7 +11,6 @@ const {
|
|
|
11
11
|
CodexDesktopRefresher,
|
|
12
12
|
readBridgeConfig,
|
|
13
13
|
} = require("./codex-desktop-refresher");
|
|
14
|
-
const { createDesktopThreadReadRefresher } = require("./codex-desktop-thread-sync");
|
|
15
14
|
const { createCodexRpcClient } = require("./codex-rpc-client");
|
|
16
15
|
const { createThreadRolloutActivityWatcher } = require("./rollout-watch");
|
|
17
16
|
const { printQR } = require("./qr");
|
|
@@ -132,18 +131,12 @@ function startBridge({
|
|
|
132
131
|
},
|
|
133
132
|
requestIdPrefix: `androdex-bridge-${sessionId}`,
|
|
134
133
|
});
|
|
135
|
-
const refreshDesktopThreadState = createDesktopThreadReadRefresher({
|
|
136
|
-
sendCodexRequest(method, params) {
|
|
137
|
-
return codexRpcClient.sendRequest(method, params);
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
134
|
const desktopRefresher = new CodexDesktopRefresher({
|
|
141
135
|
enabled: config.refreshEnabled,
|
|
142
136
|
debounceMs: config.refreshDebounceMs,
|
|
143
137
|
refreshCommand: config.refreshCommand,
|
|
144
138
|
bundleId: config.codexBundleId,
|
|
145
139
|
appPath: config.codexAppPath,
|
|
146
|
-
refreshThreadState: refreshDesktopThreadState,
|
|
147
140
|
});
|
|
148
141
|
const workspaceRuntime = createWorkspaceRuntime({
|
|
149
142
|
config,
|
|
@@ -273,6 +266,7 @@ function startBridge({
|
|
|
273
266
|
}
|
|
274
267
|
|
|
275
268
|
logConnectionStatus("connecting");
|
|
269
|
+
let hasSeenInboundRelayTraffic = false;
|
|
276
270
|
const nextSocket = new WebSocket(relaySessionUrl, {
|
|
277
271
|
headers: {
|
|
278
272
|
"x-role": "mac",
|
|
@@ -308,6 +302,7 @@ function startBridge({
|
|
|
308
302
|
});
|
|
309
303
|
|
|
310
304
|
nextSocket.on("message", (data) => {
|
|
305
|
+
hasSeenInboundRelayTraffic = true;
|
|
311
306
|
markRelayActivity();
|
|
312
307
|
const message = typeof data === "string" ? data : data.toString("utf8");
|
|
313
308
|
if (secureTransport.handleIncomingWireMessage(message, {
|
|
@@ -324,7 +319,14 @@ function startBridge({
|
|
|
324
319
|
}
|
|
325
320
|
});
|
|
326
321
|
|
|
327
|
-
nextSocket.on("pong",
|
|
322
|
+
nextSocket.on("pong", () => {
|
|
323
|
+
hasSeenInboundRelayTraffic = true;
|
|
324
|
+
markRelayActivity();
|
|
325
|
+
});
|
|
326
|
+
nextSocket.on("ping", () => {
|
|
327
|
+
hasSeenInboundRelayTraffic = true;
|
|
328
|
+
markRelayActivity();
|
|
329
|
+
});
|
|
328
330
|
nextSocket.on("close", (code) => {
|
|
329
331
|
clearRelayWatchdog();
|
|
330
332
|
logConnectionStatus("disconnected");
|
|
@@ -629,8 +631,7 @@ function startBridge({
|
|
|
629
631
|
}
|
|
630
632
|
|
|
631
633
|
if (codexHandshakeState !== "warm") {
|
|
632
|
-
|
|
633
|
-
return false;
|
|
634
|
+
primeCodexHandshake();
|
|
634
635
|
}
|
|
635
636
|
|
|
636
637
|
sendApplicationResponse(JSON.stringify({
|
|
@@ -645,7 +646,10 @@ function startBridge({
|
|
|
645
646
|
|
|
646
647
|
if (method === "initialized") {
|
|
647
648
|
cachedInitializedNotification = true;
|
|
648
|
-
|
|
649
|
+
if (workspaceRuntime.hasActiveWorkspace() && codexHandshakeState !== "warm") {
|
|
650
|
+
primeCodexHandshake();
|
|
651
|
+
}
|
|
652
|
+
return true;
|
|
649
653
|
}
|
|
650
654
|
|
|
651
655
|
return false;
|
|
@@ -704,7 +708,12 @@ function startBridge({
|
|
|
704
708
|
}
|
|
705
709
|
|
|
706
710
|
function primeCodexHandshake() {
|
|
707
|
-
if (
|
|
711
|
+
if (
|
|
712
|
+
!workspaceRuntime.hasActiveWorkspace()
|
|
713
|
+
|| codexHandshakeState === "warm"
|
|
714
|
+
|| !cachedInitializeParams
|
|
715
|
+
|| syntheticInitializeRequest
|
|
716
|
+
) {
|
|
708
717
|
return;
|
|
709
718
|
}
|
|
710
719
|
sendSyntheticInitialize(cachedInitializeParams, false);
|
|
@@ -797,6 +806,10 @@ function startBridge({
|
|
|
797
806
|
return;
|
|
798
807
|
}
|
|
799
808
|
|
|
809
|
+
if (!hasSeenInboundRelayTraffic) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
800
813
|
if (hasRelayConnectionGoneStale(lastRelayActivityAt)) {
|
|
801
814
|
console.warn("[androdex] relay heartbeat stalled; forcing reconnect");
|
|
802
815
|
publishBridgeStatus({
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
// Purpose: Debounced Mac desktop refresh controller for Codex.app after phone-authored conversation changes.
|
|
3
3
|
// Layer: CLI helper
|
|
4
4
|
// Exports: CodexDesktopRefresher, readBridgeConfig
|
|
5
|
-
// Depends on: child_process,
|
|
5
|
+
// Depends on: child_process, path, ./rollout-watch
|
|
6
6
|
|
|
7
7
|
const { execFile } = require("child_process");
|
|
8
|
-
const fs = require("fs");
|
|
9
8
|
const path = require("path");
|
|
10
9
|
const { createThreadRolloutActivityWatcher } = require("./rollout-watch");
|
|
11
10
|
|
|
@@ -36,7 +35,6 @@ class CodexDesktopRefresher {
|
|
|
36
35
|
now = () => Date.now(),
|
|
37
36
|
refreshExecutor = null,
|
|
38
37
|
watchThreadRolloutFactory = createThreadRolloutActivityWatcher,
|
|
39
|
-
refreshThreadState = null,
|
|
40
38
|
refreshBackend = null,
|
|
41
39
|
customRefreshFailureThreshold = DEFAULT_CUSTOM_REFRESH_FAILURE_THRESHOLD,
|
|
42
40
|
} = {}) {
|
|
@@ -53,7 +51,6 @@ class CodexDesktopRefresher {
|
|
|
53
51
|
this.now = now;
|
|
54
52
|
this.refreshExecutor = refreshExecutor;
|
|
55
53
|
this.watchThreadRolloutFactory = watchThreadRolloutFactory;
|
|
56
|
-
this.refreshThreadState = refreshThreadState;
|
|
57
54
|
this.refreshBackend = refreshBackend
|
|
58
55
|
|| (this.refreshCommand ? "command" : (this.refreshExecutor ? "command" : "applescript"));
|
|
59
56
|
this.customRefreshFailureThreshold = customRefreshFailureThreshold;
|
|
@@ -211,9 +208,23 @@ class CodexDesktopRefresher {
|
|
|
211
208
|
}
|
|
212
209
|
|
|
213
210
|
scheduleNewThreadFallback() {
|
|
214
|
-
this.
|
|
211
|
+
if (!this.canRefresh()) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (this.fallbackTimer) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
215
219
|
this.fallbackTimer = setTimeout(() => {
|
|
216
|
-
this.
|
|
220
|
+
this.fallbackTimer = null;
|
|
221
|
+
if (!this.pendingNewThread || this.pendingTargetThreadId) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.noteRefreshTarget({ threadId: null, url: NEW_THREAD_DEEP_LINK });
|
|
226
|
+
this.pendingRefreshKinds.add("phone");
|
|
227
|
+
this.scheduleRefresh("fallback thread/start");
|
|
217
228
|
}, this.fallbackNewThreadMs);
|
|
218
229
|
}
|
|
219
230
|
|
|
@@ -227,7 +238,11 @@ class CodexDesktopRefresher {
|
|
|
227
238
|
}
|
|
228
239
|
|
|
229
240
|
ensureWatcher(threadId) {
|
|
230
|
-
if (!
|
|
241
|
+
if (!this.canRefresh() || !threadId) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (this.activeWatchedThreadId === threadId && this.activeWatcher) {
|
|
231
246
|
return;
|
|
232
247
|
}
|
|
233
248
|
|
|
@@ -235,40 +250,43 @@ class CodexDesktopRefresher {
|
|
|
235
250
|
this.activeWatchedThreadId = threadId;
|
|
236
251
|
this.watchStartAt = this.now();
|
|
237
252
|
this.lastRolloutSize = null;
|
|
253
|
+
this.mode = "watching_thread";
|
|
238
254
|
this.activeWatcher = this.watchThreadRolloutFactory({
|
|
239
255
|
threadId,
|
|
240
|
-
|
|
256
|
+
lookupTimeoutMs: this.rolloutLookupTimeoutMs,
|
|
241
257
|
idleTimeoutMs: this.rolloutIdleTimeoutMs,
|
|
242
|
-
|
|
243
|
-
if (size === this.lastRolloutSize) {
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
this.lastRolloutSize = size;
|
|
248
|
-
const target = { threadId, url: buildThreadDeepLink(threadId) };
|
|
249
|
-
this.queueRefresh("phone", target, "rollout activity");
|
|
250
|
-
},
|
|
258
|
+
onEvent: (event) => this.handleWatcherEvent(event),
|
|
251
259
|
onIdle: () => {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
260
|
+
this.log(`rollout watcher idle thread=${threadId}`);
|
|
261
|
+
this.stopWatcher();
|
|
262
|
+
this.mode = this.pendingNewThread ? "pending_new_thread" : "idle";
|
|
255
263
|
},
|
|
256
264
|
onTimeout: () => {
|
|
265
|
+
this.log(`rollout watcher timeout thread=${threadId}`);
|
|
257
266
|
this.stopWatcher();
|
|
267
|
+
this.mode = this.pendingNewThread ? "pending_new_thread" : "idle";
|
|
258
268
|
},
|
|
259
|
-
onError: () => {
|
|
269
|
+
onError: (error) => {
|
|
270
|
+
this.log(`rollout watcher failed thread=${threadId}: ${error.message}`);
|
|
260
271
|
this.stopWatcher();
|
|
272
|
+
this.mode = this.pendingNewThread ? "pending_new_thread" : "idle";
|
|
261
273
|
},
|
|
262
274
|
});
|
|
263
275
|
}
|
|
264
276
|
|
|
265
277
|
stopWatcher() {
|
|
266
|
-
this.activeWatcher
|
|
278
|
+
if (!this.activeWatcher) {
|
|
279
|
+
this.activeWatchedThreadId = null;
|
|
280
|
+
this.watchStartAt = 0;
|
|
281
|
+
this.lastRolloutSize = null;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
this.activeWatcher.stop();
|
|
267
286
|
this.activeWatcher = null;
|
|
268
287
|
this.activeWatchedThreadId = null;
|
|
269
288
|
this.watchStartAt = 0;
|
|
270
289
|
this.lastRolloutSize = null;
|
|
271
|
-
this.stopWatcherAfterRefreshThreadId = null;
|
|
272
290
|
}
|
|
273
291
|
|
|
274
292
|
scheduleRefresh(reason) {
|
|
@@ -276,11 +294,18 @@ class CodexDesktopRefresher {
|
|
|
276
294
|
return;
|
|
277
295
|
}
|
|
278
296
|
|
|
279
|
-
this.
|
|
297
|
+
if (this.refreshTimer) {
|
|
298
|
+
this.log(`refresh already pending: ${reason}`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const elapsedSinceLastRefresh = this.now() - this.lastRefreshAt;
|
|
303
|
+
const waitMs = Math.max(0, this.debounceMs - elapsedSinceLastRefresh);
|
|
304
|
+
this.log(`refresh scheduled: ${reason}`);
|
|
280
305
|
this.refreshTimer = setTimeout(() => {
|
|
281
306
|
this.refreshTimer = null;
|
|
282
|
-
void this.
|
|
283
|
-
},
|
|
307
|
+
void this.runPendingRefresh();
|
|
308
|
+
}, waitMs);
|
|
284
309
|
}
|
|
285
310
|
|
|
286
311
|
clearRefreshTimer() {
|
|
@@ -292,116 +317,180 @@ class CodexDesktopRefresher {
|
|
|
292
317
|
this.refreshTimer = null;
|
|
293
318
|
}
|
|
294
319
|
|
|
295
|
-
async
|
|
296
|
-
if (
|
|
320
|
+
async runPendingRefresh() {
|
|
321
|
+
if (!this.canRefresh()) {
|
|
322
|
+
this.clearPendingState();
|
|
297
323
|
return;
|
|
298
324
|
}
|
|
299
325
|
|
|
300
|
-
this.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const completionTurnId = this.pendingCompletionTurnId;
|
|
304
|
-
const completionThreadId = this.pendingCompletionTargetThreadId;
|
|
305
|
-
const watcherThreadIdToStop = this.stopWatcherAfterRefreshThreadId;
|
|
306
|
-
const targetUrl = this.pendingTargetUrl;
|
|
307
|
-
const targetThreadId = this.pendingTargetThreadId;
|
|
308
|
-
const refreshKinds = new Set(this.pendingRefreshKinds);
|
|
326
|
+
if (!this.hasPendingRefreshWork()) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
309
329
|
|
|
310
|
-
|
|
330
|
+
if (this.refreshRunning) {
|
|
331
|
+
this.log("refresh skipped (debounced): another refresh is already running");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
311
334
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
335
|
+
const isCompletionRun = this.pendingCompletionRefresh;
|
|
336
|
+
const pendingRefreshKinds = isCompletionRun
|
|
337
|
+
? new Set(["completion"])
|
|
338
|
+
: new Set(this.pendingRefreshKinds);
|
|
339
|
+
const completionTurnId = this.pendingCompletionTurnId;
|
|
340
|
+
const targetUrl = isCompletionRun ? this.pendingCompletionTargetUrl : this.pendingTargetUrl;
|
|
341
|
+
const targetThreadId = isCompletionRun
|
|
342
|
+
? this.pendingCompletionTargetThreadId
|
|
343
|
+
: this.pendingTargetThreadId;
|
|
344
|
+
const stopWatcherAfterRefreshThreadId = isCompletionRun
|
|
345
|
+
? this.stopWatcherAfterRefreshThreadId
|
|
346
|
+
: null;
|
|
347
|
+
const shouldForceCompletionRefresh = isCompletionRun;
|
|
348
|
+
|
|
349
|
+
if (isCompletionRun) {
|
|
350
|
+
this.pendingCompletionRefresh = false;
|
|
351
|
+
this.pendingCompletionTurnId = null;
|
|
352
|
+
this.clearPendingCompletionTarget();
|
|
353
|
+
this.stopWatcherAfterRefreshThreadId = null;
|
|
354
|
+
} else {
|
|
355
|
+
this.pendingRefreshKinds.clear();
|
|
356
|
+
this.clearPendingTarget();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
this.refreshRunning = true;
|
|
360
|
+
this.log(
|
|
361
|
+
`refresh running: ${Array.from(pendingRefreshKinds).join("+")}${targetThreadId ? ` thread=${targetThreadId}` : ""}`
|
|
362
|
+
);
|
|
325
363
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
364
|
+
let didRefresh = false;
|
|
365
|
+
try {
|
|
366
|
+
const refreshSignature = `${targetUrl || "app"}|${targetThreadId || "no-thread"}`;
|
|
367
|
+
if (
|
|
368
|
+
!shouldForceCompletionRefresh
|
|
369
|
+
&& refreshSignature === this.lastRefreshSignature
|
|
370
|
+
&& this.now() - this.lastRefreshAt < this.debounceMs
|
|
371
|
+
) {
|
|
372
|
+
this.log(`refresh skipped (duplicate target): ${refreshSignature}`);
|
|
373
|
+
} else {
|
|
374
|
+
await this.executeRefresh(targetUrl);
|
|
375
|
+
this.lastRefreshAt = this.now();
|
|
376
|
+
this.lastRefreshSignature = refreshSignature;
|
|
377
|
+
this.consecutiveRefreshFailures = 0;
|
|
378
|
+
didRefresh = true;
|
|
333
379
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const message = extractErrorMessage(error);
|
|
337
|
-
this.log(`refresh failed (${reason}): ${message}`);
|
|
338
|
-
if (this.consecutiveRefreshFailures >= this.customRefreshFailureThreshold || isDesktopUnavailableError(message)) {
|
|
339
|
-
this.disableRuntimeRefresh(message);
|
|
380
|
+
if (completionTurnId && didRefresh) {
|
|
381
|
+
this.lastTurnIdRefreshed = completionTurnId;
|
|
340
382
|
}
|
|
383
|
+
} catch (error) {
|
|
384
|
+
this.handleRefreshFailure(error);
|
|
341
385
|
} finally {
|
|
342
386
|
this.refreshRunning = false;
|
|
387
|
+
if (
|
|
388
|
+
didRefresh
|
|
389
|
+
&& stopWatcherAfterRefreshThreadId
|
|
390
|
+
&& stopWatcherAfterRefreshThreadId === this.activeWatchedThreadId
|
|
391
|
+
) {
|
|
392
|
+
this.stopWatcher();
|
|
393
|
+
this.mode = this.pendingNewThread ? "pending_new_thread" : "idle";
|
|
394
|
+
}
|
|
395
|
+
// A completion refresh can queue while another refresh is still running,
|
|
396
|
+
// so retry whenever either queue still has work.
|
|
343
397
|
if (this.hasPendingRefreshWork()) {
|
|
344
|
-
this.scheduleRefresh("follow-up refresh");
|
|
398
|
+
this.scheduleRefresh("pending follow-up refresh");
|
|
345
399
|
}
|
|
346
400
|
}
|
|
347
401
|
}
|
|
348
402
|
|
|
349
|
-
|
|
350
|
-
if (
|
|
351
|
-
return
|
|
403
|
+
executeRefresh(targetUrl) {
|
|
404
|
+
if (typeof this.refreshExecutor === "function") {
|
|
405
|
+
return this.refreshExecutor({
|
|
406
|
+
targetUrl,
|
|
407
|
+
bundleId: this.bundleId,
|
|
408
|
+
appPath: this.appPath,
|
|
409
|
+
});
|
|
352
410
|
}
|
|
353
411
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
} catch (error) {
|
|
357
|
-
this.log(`thread state refresh failed for ${threadId}: ${extractErrorMessage(error)}`);
|
|
412
|
+
if (this.refreshBackend === "command") {
|
|
413
|
+
return execFilePromise("sh", ["-lc", this.refreshCommand]);
|
|
358
414
|
}
|
|
415
|
+
|
|
416
|
+
return execFilePromise("osascript", [
|
|
417
|
+
REFRESH_SCRIPT_PATH,
|
|
418
|
+
this.bundleId,
|
|
419
|
+
this.appPath,
|
|
420
|
+
targetUrl || "",
|
|
421
|
+
]);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
clearPendingCompletionTarget() {
|
|
425
|
+
this.pendingCompletionTargetUrl = "";
|
|
426
|
+
this.pendingCompletionTargetThreadId = "";
|
|
359
427
|
}
|
|
360
428
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (
|
|
364
|
-
!isCompletionRun
|
|
365
|
-
&& signature === this.lastRefreshSignature
|
|
366
|
-
&& (this.now() - this.lastRefreshAt) < this.midRunRefreshThrottleMs
|
|
367
|
-
) {
|
|
429
|
+
handleWatcherEvent(event) {
|
|
430
|
+
if (!event?.threadId || event.threadId !== this.activeWatchedThreadId) {
|
|
368
431
|
return;
|
|
369
432
|
}
|
|
370
433
|
|
|
371
|
-
|
|
372
|
-
this.
|
|
373
|
-
this.
|
|
374
|
-
|
|
434
|
+
const previousSize = this.lastRolloutSize;
|
|
435
|
+
this.lastRolloutSize = event.size;
|
|
436
|
+
this.noteRefreshTarget({
|
|
437
|
+
threadId: event.threadId,
|
|
438
|
+
url: buildThreadDeepLink(event.threadId),
|
|
439
|
+
});
|
|
375
440
|
|
|
376
|
-
if (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
});
|
|
441
|
+
if (event.reason === "materialized") {
|
|
442
|
+
this.queueRefresh("rollout_materialized", {
|
|
443
|
+
threadId: event.threadId,
|
|
444
|
+
url: buildThreadDeepLink(event.threadId),
|
|
445
|
+
}, `rollout ${event.reason}`);
|
|
382
446
|
return;
|
|
383
447
|
}
|
|
384
448
|
|
|
385
|
-
if (
|
|
386
|
-
await execFilePromise("sh", ["-lc", this.refreshCommand]);
|
|
449
|
+
if (event.reason !== "growth") {
|
|
387
450
|
return;
|
|
388
451
|
}
|
|
389
452
|
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
]);
|
|
453
|
+
if (previousSize == null) {
|
|
454
|
+
this.queueRefresh("rollout_growth", {
|
|
455
|
+
threadId: event.threadId,
|
|
456
|
+
url: buildThreadDeepLink(event.threadId),
|
|
457
|
+
}, "rollout first-growth");
|
|
458
|
+
this.lastMidRunRefreshAt = this.now();
|
|
397
459
|
return;
|
|
398
460
|
}
|
|
461
|
+
|
|
462
|
+
if (this.now() - this.lastMidRunRefreshAt < this.midRunRefreshThrottleMs) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
this.lastMidRunRefreshAt = this.now();
|
|
467
|
+
this.queueRefresh("rollout_growth", {
|
|
468
|
+
threadId: event.threadId,
|
|
469
|
+
url: buildThreadDeepLink(event.threadId),
|
|
470
|
+
}, "rollout mid-run");
|
|
399
471
|
}
|
|
400
472
|
|
|
401
473
|
log(message) {
|
|
402
474
|
console.log(`${this.logPrefix} ${message}`);
|
|
403
475
|
}
|
|
404
476
|
|
|
477
|
+
handleRefreshFailure(error) {
|
|
478
|
+
const message = extractErrorMessage(error);
|
|
479
|
+
console.error(`${this.logPrefix} refresh failed: ${message}`);
|
|
480
|
+
|
|
481
|
+
if (this.refreshBackend === "applescript" && isDesktopUnavailableError(message)) {
|
|
482
|
+
this.disableRuntimeRefresh("desktop refresh unavailable on this Mac");
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (this.refreshBackend === "command") {
|
|
487
|
+
this.consecutiveRefreshFailures += 1;
|
|
488
|
+
if (this.consecutiveRefreshFailures >= this.customRefreshFailureThreshold) {
|
|
489
|
+
this.disableRuntimeRefresh("custom refresh command kept failing");
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
405
494
|
disableRuntimeRefresh(reason) {
|
|
406
495
|
if (!this.runtimeRefreshAvailable) {
|
|
407
496
|
return;
|
|
@@ -99,6 +99,7 @@ async function startMacOSBridgeService({
|
|
|
99
99
|
osImpl,
|
|
100
100
|
nodePath,
|
|
101
101
|
cliPath,
|
|
102
|
+
refreshEnabled: Boolean(nextConfig.refreshEnabled),
|
|
102
103
|
});
|
|
103
104
|
restartLaunchAgent({
|
|
104
105
|
env,
|
|
@@ -172,6 +173,7 @@ function getMacOSBridgeServiceStatus({
|
|
|
172
173
|
const pairingFreshness = classifyPairingPayloadFreshness(pairingSession);
|
|
173
174
|
const duplicateBridgeProcesses = listDuplicateBridgeProcesses({ execFileSyncImpl, launchdPid: launchd.pid });
|
|
174
175
|
const deviceState = runCatchingLoadBridgeDeviceState(loadBridgeDeviceStateImpl);
|
|
176
|
+
const daemonConfig = readDaemonConfig({ env, fsImpl });
|
|
175
177
|
return {
|
|
176
178
|
label: SERVICE_LABEL,
|
|
177
179
|
platform: "darwin",
|
|
@@ -182,6 +184,7 @@ function getMacOSBridgeServiceStatus({
|
|
|
182
184
|
pairingSession,
|
|
183
185
|
pairingFreshness,
|
|
184
186
|
hasTrustedPhone: hasTrustedPhones(deviceState),
|
|
187
|
+
refreshEnabled: Boolean(daemonConfig?.refreshEnabled),
|
|
185
188
|
duplicateBridgeProcesses,
|
|
186
189
|
stdoutLogPath: resolveBridgeStdoutLogPath({ env }),
|
|
187
190
|
stderrLogPath: resolveBridgeStderrLogPath({ env }),
|
|
@@ -200,6 +203,7 @@ function printMacOSBridgeServiceStatus(options = {}) {
|
|
|
200
203
|
console.log(`[androdex] Bridge state: ${bridgeState}`);
|
|
201
204
|
console.log(`[androdex] Connection: ${connectionStatus}`);
|
|
202
205
|
console.log(`[androdex] Trusted phone: ${status.hasTrustedPhone ? "yes" : "no"}`);
|
|
206
|
+
console.log(`[androdex] Desktop refresh: ${status.refreshEnabled ? "enabled" : "disabled"}`);
|
|
203
207
|
console.log(`[androdex] Pairing payload: ${pairingCreatedAt} (${status.pairingFreshness})`);
|
|
204
208
|
if (status.duplicateBridgeProcesses.length > 0) {
|
|
205
209
|
console.log(`[androdex] Duplicate bridge processes: ${status.duplicateBridgeProcesses.length}`);
|
|
@@ -227,6 +231,7 @@ function writeLaunchAgentPlist({
|
|
|
227
231
|
osImpl = os,
|
|
228
232
|
nodePath = process.execPath,
|
|
229
233
|
cliPath = path.resolve(__dirname, "..", "bin", "androdex.js"),
|
|
234
|
+
refreshEnabled = false,
|
|
230
235
|
} = {}) {
|
|
231
236
|
const plistPath = resolveLaunchAgentPlistPath({ env, osImpl });
|
|
232
237
|
const stateDir = resolveAndrodexStateDir({ env, osImpl });
|
|
@@ -241,6 +246,7 @@ function writeLaunchAgentPlist({
|
|
|
241
246
|
stderrLogPath,
|
|
242
247
|
nodePath,
|
|
243
248
|
cliPath,
|
|
249
|
+
refreshEnabled,
|
|
244
250
|
});
|
|
245
251
|
|
|
246
252
|
fsImpl.mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
@@ -256,6 +262,7 @@ function buildLaunchAgentPlist({
|
|
|
256
262
|
stderrLogPath,
|
|
257
263
|
nodePath,
|
|
258
264
|
cliPath,
|
|
265
|
+
refreshEnabled = false,
|
|
259
266
|
}) {
|
|
260
267
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
261
268
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -286,6 +293,8 @@ function buildLaunchAgentPlist({
|
|
|
286
293
|
<string>${escapeXml(pathEnv)}</string>
|
|
287
294
|
<key>ANDRODEX_DEVICE_STATE_DIR</key>
|
|
288
295
|
<string>${escapeXml(stateDir)}</string>
|
|
296
|
+
<key>ANDRODEX_REFRESH_ENABLED</key>
|
|
297
|
+
<string>${escapeXml(refreshEnabled ? "true" : "false")}</string>
|
|
289
298
|
</dict>
|
|
290
299
|
<key>StandardOutPath</key>
|
|
291
300
|
<string>${escapeXml(stdoutLogPath)}</string>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- FILE: codex-refresh.applescript
|
|
2
|
-
-- Purpose:
|
|
2
|
+
-- Purpose: Forces a non-destructive route bounce inside Codex so the target thread remounts without killing runs.
|
|
3
3
|
-- Layer: UI automation helper
|
|
4
4
|
-- Args: bundle id, app path fallback, optional target deep link
|
|
5
5
|
|
|
@@ -7,6 +7,7 @@ on run argv
|
|
|
7
7
|
set bundleId to item 1 of argv
|
|
8
8
|
set appPath to item 2 of argv
|
|
9
9
|
set targetUrl to ""
|
|
10
|
+
set bounceUrl to "codex://settings"
|
|
10
11
|
|
|
11
12
|
if (count of argv) is greater than or equal to 3 then
|
|
12
13
|
set targetUrl to item 3 of argv
|
|
@@ -18,6 +19,9 @@ on run argv
|
|
|
18
19
|
|
|
19
20
|
delay 0.12
|
|
20
21
|
|
|
22
|
+
my openCodexUrl(bundleId, appPath, bounceUrl)
|
|
23
|
+
delay 0.18
|
|
24
|
+
|
|
21
25
|
if targetUrl is not "" then
|
|
22
26
|
my openCodexUrl(bundleId, appPath, targetUrl)
|
|
23
27
|
else
|
|
@@ -33,7 +37,7 @@ end run
|
|
|
33
37
|
on openCodexUrl(bundleId, appPath, targetUrl)
|
|
34
38
|
try
|
|
35
39
|
if targetUrl is not "" then
|
|
36
|
-
open
|
|
40
|
+
do shell script "open -b " & quoted form of bundleId & " " & quoted form of targetUrl
|
|
37
41
|
else
|
|
38
42
|
do shell script "open -b " & quoted form of bundleId
|
|
39
43
|
end if
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
// FILE: codex-desktop-thread-sync.js
|
|
2
|
-
// Purpose: Refreshes app-server thread state from disk before nudging the desktop UI.
|
|
3
|
-
// Layer: CLI helper
|
|
4
|
-
// Exports: createDesktopThreadReadRefresher
|
|
5
|
-
// Depends on: none
|
|
6
|
-
|
|
7
|
-
function createDesktopThreadReadRefresher({
|
|
8
|
-
sendCodexRequest,
|
|
9
|
-
includeTurns = true,
|
|
10
|
-
} = {}) {
|
|
11
|
-
if (typeof sendCodexRequest !== "function") {
|
|
12
|
-
throw new Error("createDesktopThreadReadRefresher requires a sendCodexRequest function.");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return function refreshDesktopThreadState({ threadId }) {
|
|
16
|
-
const normalizedThreadId = readString(threadId);
|
|
17
|
-
if (!normalizedThreadId) {
|
|
18
|
-
return Promise.resolve(null);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return sendCodexRequest("thread/read", {
|
|
22
|
-
threadId: normalizedThreadId,
|
|
23
|
-
includeTurns,
|
|
24
|
-
});
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function readString(value) {
|
|
29
|
-
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
module.exports = {
|
|
33
|
-
createDesktopThreadReadRefresher,
|
|
34
|
-
};
|