@makerbi/remodex 2.4.0 → 2.5.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/bin/remodex.js +25 -2
- package/package.json +1 -1
- package/src/bridge.js +46 -61
- package/src/codex-desktop-refresher.js +173 -8
- package/src/codex-tool-wrapper.js +523 -0
- package/src/desktop-ipc-action-follower.js +292 -54
- package/src/desktop-ipc-live-owner.js +148 -6
- package/src/desktop-ipc-owner-transport.js +143 -73
- package/src/desktop-ipc-shared.js +125 -8
- package/src/git-handler.js +189 -108
- package/src/index.js +2 -0
- package/src/macos-launch-agent.js +65 -34
- package/src/rollout-live-mirror.js +134 -13
- package/src/rollout-watch.js +176 -54
- package/src/session-jsonl-history.js +72 -32
- package/src/thread-list-provenance.js +105 -0
- package/src/thread-row-enrichment.js +43 -0
- package/src/thread-runtime-settings-store.js +3 -21
- package/src/worktree-origin.js +192 -0
|
@@ -15,16 +15,19 @@ const {
|
|
|
15
15
|
projectDesktopConversationStateToThread,
|
|
16
16
|
} = require("./desktop-ipc-conversation-projector");
|
|
17
17
|
const {
|
|
18
|
+
CLIENT_STATUS_CHANGED,
|
|
18
19
|
DESKTOP_IPC_METHOD_VERSIONS: METHOD_VERSION_BY_NAME,
|
|
19
|
-
|
|
20
|
-
MAX_FRAME_BYTES,
|
|
20
|
+
buildIpcRequestEnvelope,
|
|
21
21
|
cloneJSON,
|
|
22
|
+
createFrameReader,
|
|
22
23
|
isThreadTurnStateProbeRequest,
|
|
23
24
|
normalizeToken,
|
|
24
25
|
readString,
|
|
25
26
|
requestIdKey,
|
|
26
27
|
resolveDefaultIpcSocketPath,
|
|
28
|
+
resolveIpcSocketPathCandidates,
|
|
27
29
|
safeParseJSON,
|
|
30
|
+
toSocketPathCandidatesResolver,
|
|
28
31
|
writeFrame,
|
|
29
32
|
} = require("./desktop-ipc-shared");
|
|
30
33
|
|
|
@@ -42,6 +45,8 @@ const MAX_QUEUED_CHANGES_PER_THREAD = 300;
|
|
|
42
45
|
const MAX_ACTIVE_THREAD_IDS = 512;
|
|
43
46
|
const DESKTOP_IPC_ACTION_SOURCE = "desktop-ipc-action-follower";
|
|
44
47
|
const REMODEX_LIVE_OWNER_SOURCE = "desktop-ipc-live-owner";
|
|
48
|
+
const THREAD_STREAM_FOLLOWING_CHANGED = "thread-stream-following-changed";
|
|
49
|
+
const THREAD_STREAM_FOLLOWING_STATUS_REQUESTED = "thread-stream-following-status-requested";
|
|
45
50
|
const DESKTOP_STATE_READ_METHODS = new Set([
|
|
46
51
|
"thread/read",
|
|
47
52
|
"thread/resume",
|
|
@@ -52,6 +57,12 @@ const DESKTOP_STATE_READ_METHODS = new Set([
|
|
|
52
57
|
// this, a phone with no selected chat never connects to the Desktop bus and
|
|
53
58
|
// cannot discover runs that started on the Mac.
|
|
54
59
|
const DESKTOP_BACKGROUND_DISCOVERY_METHODS = new Set(["thread/list"]);
|
|
60
|
+
// A run is announced to the phone exactly once, and the phone ignores buffered
|
|
61
|
+
// replays of that announcement by design. An app session that starts after the
|
|
62
|
+
// run did would otherwise never learn about it and show no sidebar badge until
|
|
63
|
+
// the chat is opened. Sidebar refreshes re-announce still-active turns instead,
|
|
64
|
+
// throttled per thread so steady polling costs one idempotent notification.
|
|
65
|
+
const BACKGROUND_TURN_REANNOUNCE_MIN_INTERVAL_MS = 30_000;
|
|
55
66
|
const DESKTOP_TURNS_CURSOR_PREFIX = "remodex-desktop-turns:";
|
|
56
67
|
// Per-thread activity can be quiet during tools and subagents, so the short
|
|
57
68
|
// freshness window alone does not revoke a healthy Desktop owner.
|
|
@@ -221,13 +232,16 @@ function createDesktopIpcActionFollower({
|
|
|
221
232
|
normalizeTurnStartParams = (params) => params,
|
|
222
233
|
runtimeSettingsStore = null,
|
|
223
234
|
logPrefix = "[remodex]",
|
|
224
|
-
|
|
235
|
+
// Resolved per connect: Codex Desktop can start, stop, or move its bus while
|
|
236
|
+
// the bridge stays up.
|
|
237
|
+
socketPath = resolveIpcSocketPathCandidates,
|
|
225
238
|
netModule = net,
|
|
226
239
|
now = () => Date.now(),
|
|
227
240
|
snapshotDebounceMs = 0,
|
|
228
241
|
setTimeoutFn = setTimeout,
|
|
229
242
|
clearTimeoutFn = clearTimeout,
|
|
230
243
|
onNormalizedHistoryIndexRebuilt = () => {},
|
|
244
|
+
onFollowerStateChanged = null,
|
|
231
245
|
requestTimeoutMs = REQUEST_TIMEOUT_MS,
|
|
232
246
|
ownershipProbeTimeoutMs = OWNERSHIP_PROBE_TIMEOUT_MS,
|
|
233
247
|
} = {}) {
|
|
@@ -239,6 +253,7 @@ function createDesktopIpcActionFollower({
|
|
|
239
253
|
logPrefix,
|
|
240
254
|
onEnvelope,
|
|
241
255
|
onConnected() {
|
|
256
|
+
announceDesktopFollowForActiveThreads();
|
|
242
257
|
probeHeldFollowerRequests();
|
|
243
258
|
},
|
|
244
259
|
onDisconnect,
|
|
@@ -260,6 +275,8 @@ function createDesktopIpcActionFollower({
|
|
|
260
275
|
const conversationProjector = createDesktopConversationProjector({ now });
|
|
261
276
|
const pendingRoutesByRequestId = new Map();
|
|
262
277
|
const activeThreadIds = new Set();
|
|
278
|
+
const desktopFollowThreadIds = new Set();
|
|
279
|
+
const followerClientIdsByThreadId = new Map();
|
|
263
280
|
// Threads discovered from Litter snapshots before the phone reads them.
|
|
264
281
|
// Their raw state is retained for lifecycle detection, but their transcript
|
|
265
282
|
// stays off the relay until the user actually opens the chat.
|
|
@@ -268,6 +285,7 @@ function createDesktopIpcActionFollower({
|
|
|
268
285
|
// baseline. Otherwise a disconnect/eviction can erase the only evidence
|
|
269
286
|
// needed to send the matching completion and leave a phantom running badge.
|
|
270
287
|
const announcedBackgroundTurnsByThreadId = new Map();
|
|
288
|
+
const backgroundTurnReannouncedAtByThreadId = new Map();
|
|
271
289
|
// JS Set preserves insertion order; delete-before-add refreshes recency, and
|
|
272
290
|
// cap eviction skips threads with pending prompts so approvals are not lost.
|
|
273
291
|
function rememberActiveThread(threadId) {
|
|
@@ -283,6 +301,35 @@ function createDesktopIpcActionFollower({
|
|
|
283
301
|
}
|
|
284
302
|
}
|
|
285
303
|
|
|
304
|
+
function followDesktopThread(threadId, targetClientIds = undefined) {
|
|
305
|
+
if (!threadId || isLocallyOwnedThread(threadId) || liveOwnerThreadIds.has(threadId)) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
desktopFollowThreadIds.add(threadId);
|
|
309
|
+
return ipc.sendBroadcast(THREAD_STREAM_FOLLOWING_CHANGED, {
|
|
310
|
+
hostId: "local",
|
|
311
|
+
conversationId: threadId,
|
|
312
|
+
following: true,
|
|
313
|
+
}, { targetClientIds });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function unfollowDesktopThread(threadId) {
|
|
317
|
+
if (!desktopFollowThreadIds.delete(threadId)) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
return ipc.sendBroadcast(THREAD_STREAM_FOLLOWING_CHANGED, {
|
|
321
|
+
hostId: "local",
|
|
322
|
+
conversationId: threadId,
|
|
323
|
+
following: false,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function announceDesktopFollowForActiveThreads() {
|
|
328
|
+
for (const threadId of desktopFollowThreadIds) {
|
|
329
|
+
followDesktopThread(threadId);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
286
333
|
function oldestEvictableActiveThreadId() {
|
|
287
334
|
for (const threadId of activeThreadIds) {
|
|
288
335
|
if (!hasPendingProjectedActions(threadId)
|
|
@@ -307,6 +354,7 @@ function createDesktopIpcActionFollower({
|
|
|
307
354
|
// without rejecting held requests (removeDesktopThreadState handles real removal).
|
|
308
355
|
function forgetEvictedThreadState(threadId) {
|
|
309
356
|
cancelPendingSnapshot(threadId);
|
|
357
|
+
unfollowDesktopThread(threadId);
|
|
310
358
|
settleAnnouncedBackgroundTurn(threadId, "interrupted");
|
|
311
359
|
backgroundOnlyThreadIds.delete(threadId);
|
|
312
360
|
rawStatesByThreadId.delete(threadId);
|
|
@@ -346,6 +394,7 @@ function createDesktopIpcActionFollower({
|
|
|
346
394
|
const method = readString(message?.method);
|
|
347
395
|
if (DESKTOP_BACKGROUND_DISCOVERY_METHODS.has(method)) {
|
|
348
396
|
ipc.ensureConnected();
|
|
397
|
+
reannounceActiveBackgroundTurns();
|
|
349
398
|
}
|
|
350
399
|
if (DESKTOP_STATE_READ_METHODS.has(method)) {
|
|
351
400
|
const threadId = readThreadId(message?.params);
|
|
@@ -364,6 +413,7 @@ function createDesktopIpcActionFollower({
|
|
|
364
413
|
settleAnnouncedBackgroundTurn(threadId, "interrupted");
|
|
365
414
|
}
|
|
366
415
|
announcedBackgroundTurnsByThreadId.delete(threadId);
|
|
416
|
+
backgroundTurnReannouncedAtByThreadId.delete(threadId);
|
|
367
417
|
if (rawState) {
|
|
368
418
|
const liveState = boundedDesktopLiveStateForThread(threadId, rawState);
|
|
369
419
|
const hasCanonicalNormalizedHistory = canonicalHistoryThreadIds.has(threadId)
|
|
@@ -478,6 +528,7 @@ function createDesktopIpcActionFollower({
|
|
|
478
528
|
ownershipProbeDeadlinesByThreadId.set(threadId, now() + ownershipProbeTimeoutMs);
|
|
479
529
|
}
|
|
480
530
|
ipc.ensureConnected();
|
|
531
|
+
followDesktopThread(threadId);
|
|
481
532
|
return false;
|
|
482
533
|
}
|
|
483
534
|
|
|
@@ -495,9 +546,15 @@ function createDesktopIpcActionFollower({
|
|
|
495
546
|
normalizedReviewFingerprintsByThreadId.clear();
|
|
496
547
|
conversationProjector.reset();
|
|
497
548
|
pendingRoutesByRequestId.clear();
|
|
549
|
+
for (const threadId of desktopFollowThreadIds) {
|
|
550
|
+
unfollowDesktopThread(threadId);
|
|
551
|
+
}
|
|
552
|
+
desktopFollowThreadIds.clear();
|
|
498
553
|
activeThreadIds.clear();
|
|
554
|
+
followerClientIdsByThreadId.clear();
|
|
499
555
|
backgroundOnlyThreadIds.clear();
|
|
500
556
|
announcedBackgroundTurnsByThreadId.clear();
|
|
557
|
+
backgroundTurnReannouncedAtByThreadId.clear();
|
|
501
558
|
recoveringThreadIds.clear();
|
|
502
559
|
baselineRecoveryStateByThreadId.clear();
|
|
503
560
|
queuedChangesByThreadId.clear();
|
|
@@ -516,6 +573,25 @@ function createDesktopIpcActionFollower({
|
|
|
516
573
|
|
|
517
574
|
// Desktop broadcasts carry the live conversation state Litter projects from.
|
|
518
575
|
function onEnvelope(envelope) {
|
|
576
|
+
if (envelope?.type === "broadcast" && envelope.method === CLIENT_STATUS_CHANGED) {
|
|
577
|
+
if (normalizeToken(envelope.params?.status) === "disconnected") {
|
|
578
|
+
removeFollowerClient(envelope.params?.clientId || envelope.sourceClientId);
|
|
579
|
+
}
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (envelope?.type === "broadcast" && envelope.method === THREAD_STREAM_FOLLOWING_CHANGED) {
|
|
583
|
+
updateFollowerState(envelope);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (envelope?.type === "broadcast"
|
|
587
|
+
&& envelope.method === THREAD_STREAM_FOLLOWING_STATUS_REQUESTED) {
|
|
588
|
+
const params = envelope.params || {};
|
|
589
|
+
const threadId = readString(params.conversationId) || readString(params.conversation_id);
|
|
590
|
+
if (threadId && desktopFollowThreadIds.has(threadId)) {
|
|
591
|
+
followDesktopThread(threadId, [envelope.sourceClientId].filter(Boolean));
|
|
592
|
+
}
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
519
595
|
if (envelope?.type === "broadcast"
|
|
520
596
|
&& (envelope.method === "thread-archived" || envelope.method === "thread-unarchived")) {
|
|
521
597
|
syncThreadArchiveBroadcast(envelope);
|
|
@@ -560,6 +636,14 @@ function createDesktopIpcActionFollower({
|
|
|
560
636
|
backgroundOnlyThreadIds.add(threadId);
|
|
561
637
|
}
|
|
562
638
|
if (!activeThreadIds.has(threadId)) {
|
|
639
|
+
// After a bridge restart Desktop can re-announce that a renderer follows
|
|
640
|
+
// the thread without replaying its baseline snapshot. A request patch may
|
|
641
|
+
// then be the first state update we see. Preserve that blocking action
|
|
642
|
+
// immediately instead of waiting for a phone read that may never arrive
|
|
643
|
+
// while the already-open screen is parked on the question.
|
|
644
|
+
if (commitSpeculativeActionPatch(threadId, params.change, { backgroundOnly: true })) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
563
647
|
return;
|
|
564
648
|
}
|
|
565
649
|
|
|
@@ -581,21 +665,11 @@ function createDesktopIpcActionFollower({
|
|
|
581
665
|
const previousState = rawStatesByThreadId.get(threadId) || null;
|
|
582
666
|
const nextState = applyConversationStateChange(previousState, params.change);
|
|
583
667
|
if (!nextState) {
|
|
584
|
-
if (
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const speculativeActions = projectPendingDesktopActions(threadId, speculativeState);
|
|
588
|
-
if (speculativeActions.length > 0) {
|
|
589
|
-
rawStatesByThreadId.set(threadId, speculativeState);
|
|
590
|
-
rawStateUpdatedAtByThreadId.set(threadId, now());
|
|
591
|
-
if (!backgroundOnlyThreadIds.has(threadId)) {
|
|
592
|
-
conversationProjector.seed(threadId, speculativeState);
|
|
593
|
-
}
|
|
594
|
-
syncProjectedActions(threadId, speculativeActions);
|
|
595
|
-
releaseHeldFollowerRequests(threadId, { toDesktop: true });
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
668
|
+
if (commitSpeculativeActionPatch(threadId, params.change)) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
598
671
|
|
|
672
|
+
if (isPatchChange(params.change)) {
|
|
599
673
|
if (typeof readConversationState !== "function") {
|
|
600
674
|
return;
|
|
601
675
|
}
|
|
@@ -617,6 +691,31 @@ function createDesktopIpcActionFollower({
|
|
|
617
691
|
});
|
|
618
692
|
}
|
|
619
693
|
|
|
694
|
+
function commitSpeculativeActionPatch(threadId, change, { backgroundOnly = false } = {}) {
|
|
695
|
+
if (!isPatchChange(change)) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const speculativeState = applyConversationStateChange(createEmptyConversationState(), change);
|
|
700
|
+
const speculativeActions = projectPendingDesktopActions(threadId, speculativeState);
|
|
701
|
+
if (speculativeActions.length === 0) {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (backgroundOnly) {
|
|
706
|
+
rememberActiveThread(threadId);
|
|
707
|
+
backgroundOnlyThreadIds.add(threadId);
|
|
708
|
+
}
|
|
709
|
+
rawStatesByThreadId.set(threadId, speculativeState);
|
|
710
|
+
rawStateUpdatedAtByThreadId.set(threadId, now());
|
|
711
|
+
if (!backgroundOnlyThreadIds.has(threadId)) {
|
|
712
|
+
conversationProjector.seed(threadId, speculativeState);
|
|
713
|
+
}
|
|
714
|
+
syncProjectedActions(threadId, speculativeActions);
|
|
715
|
+
releaseHeldFollowerRequests(threadId, { toDesktop: true });
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
718
|
+
|
|
620
719
|
function schedulePendingSnapshot(threadId, state) {
|
|
621
720
|
cancelPendingSnapshot(threadId);
|
|
622
721
|
const timer = setTimeoutFn(() => {
|
|
@@ -735,6 +834,7 @@ function createDesktopIpcActionFollower({
|
|
|
735
834
|
queuedChangesByThreadId.clear();
|
|
736
835
|
pendingOwnershipProbeTokensByThreadId.clear();
|
|
737
836
|
desktopOwnedByProbeThreadIds.clear();
|
|
837
|
+
clearFollowerState();
|
|
738
838
|
// A lost IPC connection is not evidence that Desktop stopped the turn.
|
|
739
839
|
// Keep announced lifecycle state until a reconnect snapshot, archive, or
|
|
740
840
|
// another authoritative state transition supplies a real terminal status.
|
|
@@ -750,6 +850,67 @@ function createDesktopIpcActionFollower({
|
|
|
750
850
|
// a proven delivery failure falls back to the local app-server.
|
|
751
851
|
}
|
|
752
852
|
|
|
853
|
+
// The same route-follow handshake is used whether Remodex's app-server owns
|
|
854
|
+
// the thread or Codex Desktop owns it. The live-owner component covers the
|
|
855
|
+
// former; this follower covers Desktop-owned threads so the navigation
|
|
856
|
+
// controller can stop retrying as soon as the renderer is actually mounted.
|
|
857
|
+
function updateFollowerState(envelope) {
|
|
858
|
+
const params = envelope?.params || {};
|
|
859
|
+
const threadId = readString(params.conversationId) || readString(params.conversation_id);
|
|
860
|
+
const clientId = readString(envelope?.sourceClientId);
|
|
861
|
+
if (!threadId
|
|
862
|
+
|| !clientId
|
|
863
|
+
|| clientId === ipc.clientId
|
|
864
|
+
|| isLocallyOwnedThread(threadId)
|
|
865
|
+
|| liveOwnerThreadIds.has(threadId)) {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const followers = followerClientIdsByThreadId.get(threadId) || new Set();
|
|
870
|
+
if (params.following === true) {
|
|
871
|
+
rememberActiveThread(threadId);
|
|
872
|
+
followDesktopThread(threadId);
|
|
873
|
+
const wasUnfollowed = followers.size === 0;
|
|
874
|
+
followers.add(clientId);
|
|
875
|
+
followerClientIdsByThreadId.set(threadId, followers);
|
|
876
|
+
if (wasUnfollowed) {
|
|
877
|
+
onFollowerStateChanged?.(threadId, true);
|
|
878
|
+
}
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (!followers.delete(clientId)) {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (followers.size === 0) {
|
|
886
|
+
followerClientIdsByThreadId.delete(threadId);
|
|
887
|
+
onFollowerStateChanged?.(threadId, false);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function removeFollowerClient(clientId) {
|
|
892
|
+
const normalizedClientId = readString(clientId);
|
|
893
|
+
if (!normalizedClientId) {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
for (const [threadId, followers] of followerClientIdsByThreadId) {
|
|
897
|
+
if (!followers.delete(normalizedClientId)) {
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
if (followers.size === 0) {
|
|
901
|
+
followerClientIdsByThreadId.delete(threadId);
|
|
902
|
+
onFollowerStateChanged?.(threadId, false);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function clearFollowerState() {
|
|
908
|
+
for (const threadId of followerClientIdsByThreadId.keys()) {
|
|
909
|
+
onFollowerStateChanged?.(threadId, false);
|
|
910
|
+
}
|
|
911
|
+
followerClientIdsByThreadId.clear();
|
|
912
|
+
}
|
|
913
|
+
|
|
753
914
|
// The bridge's own live owner just claimed this thread's stream, so drop stale
|
|
754
915
|
// Desktop state instead of hijacking future phone requests into Desktop IPC.
|
|
755
916
|
function releaseDesktopThreadState(threadId) {
|
|
@@ -759,6 +920,7 @@ function createDesktopIpcActionFollower({
|
|
|
759
920
|
activeThreadIds.delete(threadId);
|
|
760
921
|
}
|
|
761
922
|
liveOwnerThreadIds.add(threadId);
|
|
923
|
+
unfollowDesktopThread(threadId);
|
|
762
924
|
ownershipProbeDeadlinesByThreadId.delete(threadId);
|
|
763
925
|
pendingOwnershipProbeTokensByThreadId.delete(threadId);
|
|
764
926
|
desktopOwnedByProbeThreadIds.delete(threadId);
|
|
@@ -789,6 +951,7 @@ function createDesktopIpcActionFollower({
|
|
|
789
951
|
activeThreadIds.delete(threadId);
|
|
790
952
|
}
|
|
791
953
|
liveOwnerThreadIds.delete(threadId);
|
|
954
|
+
unfollowDesktopThread(threadId);
|
|
792
955
|
ownershipProbeDeadlinesByThreadId.delete(threadId);
|
|
793
956
|
pendingOwnershipProbeTokensByThreadId.delete(threadId);
|
|
794
957
|
desktopOwnedByProbeThreadIds.delete(threadId);
|
|
@@ -1003,23 +1166,30 @@ function createDesktopIpcActionFollower({
|
|
|
1003
1166
|
}
|
|
1004
1167
|
|
|
1005
1168
|
function syncProjectedActions(threadId, actions) {
|
|
1006
|
-
const nextRequestIds = new Set(actions.map((action) => action.id));
|
|
1169
|
+
const nextRequestIds = new Set(actions.map((action) => requestIdKey(action.id)));
|
|
1007
1170
|
for (const [requestId, route] of Array.from(pendingRoutesByRequestId.entries())) {
|
|
1008
1171
|
if (route.threadId !== threadId || nextRequestIds.has(requestId)) {
|
|
1009
1172
|
continue;
|
|
1010
1173
|
}
|
|
1011
1174
|
|
|
1012
1175
|
pendingRoutesByRequestId.delete(requestId);
|
|
1013
|
-
sendApplicationResponse(JSON.stringify(
|
|
1176
|
+
sendApplicationResponse(JSON.stringify(
|
|
1177
|
+
projectedResolvedNotification(threadId, route.desktopRequestId)
|
|
1178
|
+
));
|
|
1014
1179
|
}
|
|
1015
1180
|
|
|
1016
1181
|
for (const action of actions) {
|
|
1017
|
-
|
|
1182
|
+
const requestId = requestIdKey(action.id);
|
|
1183
|
+
if (!requestId || pendingRoutesByRequestId.has(requestId)) {
|
|
1018
1184
|
continue;
|
|
1019
1185
|
}
|
|
1020
1186
|
|
|
1021
|
-
pendingRoutesByRequestId.set(
|
|
1022
|
-
requestId
|
|
1187
|
+
pendingRoutesByRequestId.set(requestId, {
|
|
1188
|
+
requestId,
|
|
1189
|
+
// Preserve the JSON-RPC id's original scalar type. Codex Desktop uses
|
|
1190
|
+
// numeric ids for real requestUserInput calls, and its follower command
|
|
1191
|
+
// must receive that same number rather than the phone-facing map key.
|
|
1192
|
+
desktopRequestId: action.id,
|
|
1023
1193
|
method: action.method,
|
|
1024
1194
|
threadId,
|
|
1025
1195
|
});
|
|
@@ -1551,9 +1721,36 @@ function createDesktopIpcActionFollower({
|
|
|
1551
1721
|
settledTurn
|
|
1552
1722
|
)));
|
|
1553
1723
|
announcedBackgroundTurnsByThreadId.delete(threadId);
|
|
1724
|
+
backgroundTurnReannouncedAtByThreadId.delete(threadId);
|
|
1554
1725
|
return true;
|
|
1555
1726
|
}
|
|
1556
1727
|
|
|
1728
|
+
// Re-sends the start of runs that are still in flight, so a phone that missed
|
|
1729
|
+
// the original announcement (fresh app session, reconnect after the run began)
|
|
1730
|
+
// gets its sidebar badge on the next sidebar refresh instead of only when the
|
|
1731
|
+
// chat is opened. The phone treats a repeated start for the same turn as a
|
|
1732
|
+
// no-op, and the identity marker keeps a phone that already saw it from
|
|
1733
|
+
// treating this as a second run. Only worth doing while Desktop IPC is
|
|
1734
|
+
// responsive: the matching completion arrives over the same link, so this
|
|
1735
|
+
// cannot keep a phantom badge alive on its own.
|
|
1736
|
+
function reannounceActiveBackgroundTurns() {
|
|
1737
|
+
if (announcedBackgroundTurnsByThreadId.size === 0 || !hasResponsiveDesktopIpc()) {
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
const reannouncedAt = now();
|
|
1742
|
+
for (const [threadId, announcedTurn] of announcedBackgroundTurnsByThreadId) {
|
|
1743
|
+
const lastReannouncedAt = backgroundTurnReannouncedAtByThreadId.get(threadId) || 0;
|
|
1744
|
+
if (reannouncedAt - lastReannouncedAt < BACKGROUND_TURN_REANNOUNCE_MIN_INTERVAL_MS) {
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1747
|
+
backgroundTurnReannouncedAtByThreadId.set(threadId, reannouncedAt);
|
|
1748
|
+
sendApplicationResponse(JSON.stringify(notificationWithTurnIdentityContinuity(
|
|
1749
|
+
backgroundTurnLifecycleNotification("turn/started", threadId, announcedTurn)
|
|
1750
|
+
)));
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1557
1754
|
function syncThreadArchiveBroadcast(envelope) {
|
|
1558
1755
|
const params = envelope.params || {};
|
|
1559
1756
|
const threadId = readString(params.conversationId) || readString(params.conversation_id);
|
|
@@ -1616,7 +1813,7 @@ function createDesktopIpcActionFollower({
|
|
|
1616
1813
|
.then(() => {
|
|
1617
1814
|
pendingRoutesByRequestId.delete(route.requestId);
|
|
1618
1815
|
sendApplicationResponse(JSON.stringify(
|
|
1619
|
-
projectedResolvedNotification(route.threadId, route.
|
|
1816
|
+
projectedResolvedNotification(route.threadId, route.desktopRequestId)
|
|
1620
1817
|
));
|
|
1621
1818
|
})
|
|
1622
1819
|
.catch((error) => {
|
|
@@ -1998,21 +2195,37 @@ function createDesktopIpcClient({
|
|
|
1998
2195
|
onConnected,
|
|
1999
2196
|
onDisconnect,
|
|
2000
2197
|
}) {
|
|
2198
|
+
const resolveSocketPaths = toSocketPathCandidatesResolver(socketPath);
|
|
2001
2199
|
let socket = null;
|
|
2002
2200
|
let clientId = "";
|
|
2003
2201
|
let isConnecting = false;
|
|
2004
2202
|
let lastActivityAt = 0;
|
|
2005
|
-
let
|
|
2203
|
+
let remainingSocketPaths = [];
|
|
2006
2204
|
const pendingRequests = new Map();
|
|
2007
2205
|
const pendingDiscoveries = new Map();
|
|
2206
|
+
const frameReader = createFrameReader({
|
|
2207
|
+
onFrame: (envelope) => dispatchEnvelope(envelope),
|
|
2208
|
+
onOverflow: () => close(),
|
|
2209
|
+
});
|
|
2008
2210
|
|
|
2009
2211
|
function ensureConnected() {
|
|
2010
2212
|
if (socket || isConnecting) {
|
|
2011
2213
|
return;
|
|
2012
2214
|
}
|
|
2013
2215
|
|
|
2216
|
+
remainingSocketPaths = resolveSocketPaths();
|
|
2217
|
+
connectNextSocket();
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
function connectNextSocket() {
|
|
2221
|
+
const nextSocketPath = remainingSocketPaths.shift();
|
|
2222
|
+
if (!nextSocketPath) {
|
|
2223
|
+
isConnecting = false;
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2014
2227
|
isConnecting = true;
|
|
2015
|
-
const nextSocket = netModule.createConnection(
|
|
2228
|
+
const nextSocket = netModule.createConnection(nextSocketPath);
|
|
2016
2229
|
socket = nextSocket;
|
|
2017
2230
|
|
|
2018
2231
|
nextSocket.on("connect", () => {
|
|
@@ -2028,14 +2241,29 @@ function createDesktopIpcClient({
|
|
|
2028
2241
|
});
|
|
2029
2242
|
});
|
|
2030
2243
|
nextSocket.on("data", handleData);
|
|
2031
|
-
nextSocket.on("close", handleClose);
|
|
2244
|
+
nextSocket.on("close", () => handleClose(nextSocket));
|
|
2032
2245
|
nextSocket.on("error", (error) => {
|
|
2246
|
+
if ((error?.code === "ENOENT" || error?.code === "ECONNREFUSED")
|
|
2247
|
+
&& remainingSocketPaths.length > 0) {
|
|
2248
|
+
retryNextSocket(nextSocket);
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2033
2251
|
if (error?.code !== "ENOENT" && error?.code !== "ECONNREFUSED") {
|
|
2034
2252
|
console.warn(`${logPrefix} desktop IPC connection failed: ${error.message}`);
|
|
2035
2253
|
}
|
|
2036
2254
|
});
|
|
2037
2255
|
}
|
|
2038
2256
|
|
|
2257
|
+
function retryNextSocket(failedSocket) {
|
|
2258
|
+
if (socket === failedSocket) {
|
|
2259
|
+
socket = null;
|
|
2260
|
+
}
|
|
2261
|
+
isConnecting = false;
|
|
2262
|
+
frameReader.reset();
|
|
2263
|
+
failedSocket.destroy();
|
|
2264
|
+
connectNextSocket();
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2039
2267
|
function sendRequest(method, params) {
|
|
2040
2268
|
ensureConnected();
|
|
2041
2269
|
if (!socket || socket.destroyed) {
|
|
@@ -2043,14 +2271,13 @@ function createDesktopIpcClient({
|
|
|
2043
2271
|
}
|
|
2044
2272
|
|
|
2045
2273
|
const requestId = `remodex-${now().toString(36)}-${Math.random().toString(16).slice(2)}`;
|
|
2046
|
-
const envelope = {
|
|
2047
|
-
type: "request",
|
|
2274
|
+
const envelope = buildIpcRequestEnvelope({
|
|
2048
2275
|
requestId,
|
|
2049
|
-
sourceClientId: method === "initialize" ? "initializing-client" : clientId || "remodex-bridge",
|
|
2050
|
-
version: METHOD_VERSION_BY_NAME.get(method) || 1,
|
|
2051
2276
|
method,
|
|
2052
|
-
params
|
|
2053
|
-
|
|
2277
|
+
params,
|
|
2278
|
+
clientId,
|
|
2279
|
+
initializing: method === "initialize",
|
|
2280
|
+
});
|
|
2054
2281
|
|
|
2055
2282
|
return new Promise((resolve, reject) => {
|
|
2056
2283
|
const timeout = setTimeout(() => {
|
|
@@ -2112,28 +2339,30 @@ function createDesktopIpcClient({
|
|
|
2112
2339
|
});
|
|
2113
2340
|
}
|
|
2114
2341
|
|
|
2342
|
+
function sendBroadcast(method, params, { targetClientIds } = {}) {
|
|
2343
|
+
ensureConnected();
|
|
2344
|
+
if (!socket || socket.destroyed || !clientId) {
|
|
2345
|
+
return false;
|
|
2346
|
+
}
|
|
2347
|
+
const envelope = {
|
|
2348
|
+
type: "broadcast",
|
|
2349
|
+
method,
|
|
2350
|
+
sourceClientId: clientId,
|
|
2351
|
+
params: params || {},
|
|
2352
|
+
version: METHOD_VERSION_BY_NAME.get(method) || 1,
|
|
2353
|
+
};
|
|
2354
|
+
if (Array.isArray(targetClientIds) && targetClientIds.length > 0) {
|
|
2355
|
+
envelope.targetClientIds = targetClientIds;
|
|
2356
|
+
}
|
|
2357
|
+
writeEnvelope(envelope);
|
|
2358
|
+
return true;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2115
2361
|
function handleData(chunk) {
|
|
2116
2362
|
if (chunk.length > 0) {
|
|
2117
2363
|
lastActivityAt = now();
|
|
2118
2364
|
}
|
|
2119
|
-
|
|
2120
|
-
while (readBuffer.length >= FRAME_HEADER_BYTES) {
|
|
2121
|
-
const frameLength = readBuffer.readUInt32LE(0);
|
|
2122
|
-
if (frameLength > MAX_FRAME_BYTES) {
|
|
2123
|
-
close();
|
|
2124
|
-
return;
|
|
2125
|
-
}
|
|
2126
|
-
if (readBuffer.length < FRAME_HEADER_BYTES + frameLength) {
|
|
2127
|
-
return;
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
const payload = readBuffer.slice(FRAME_HEADER_BYTES, FRAME_HEADER_BYTES + frameLength).toString("utf8");
|
|
2131
|
-
readBuffer = readBuffer.slice(FRAME_HEADER_BYTES + frameLength);
|
|
2132
|
-
const envelope = safeParseJSON(payload);
|
|
2133
|
-
if (envelope) {
|
|
2134
|
-
dispatchEnvelope(envelope);
|
|
2135
|
-
}
|
|
2136
|
-
}
|
|
2365
|
+
frameReader.push(chunk);
|
|
2137
2366
|
}
|
|
2138
2367
|
|
|
2139
2368
|
function dispatchEnvelope(envelope) {
|
|
@@ -2187,12 +2416,15 @@ function createDesktopIpcClient({
|
|
|
2187
2416
|
onEnvelope(envelope);
|
|
2188
2417
|
}
|
|
2189
2418
|
|
|
2190
|
-
function handleClose() {
|
|
2419
|
+
function handleClose(closedSocket) {
|
|
2420
|
+
if (socket && socket !== closedSocket) {
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2191
2423
|
socket = null;
|
|
2192
2424
|
clientId = "";
|
|
2193
2425
|
isConnecting = false;
|
|
2194
2426
|
lastActivityAt = 0;
|
|
2195
|
-
|
|
2427
|
+
frameReader.reset();
|
|
2196
2428
|
for (const waiter of pendingRequests.values()) {
|
|
2197
2429
|
clearTimeout(waiter.timeout);
|
|
2198
2430
|
waiter.reject(new Error("Desktop IPC connection closed."));
|
|
@@ -2226,6 +2458,9 @@ function createDesktopIpcClient({
|
|
|
2226
2458
|
}
|
|
2227
2459
|
|
|
2228
2460
|
return {
|
|
2461
|
+
get clientId() {
|
|
2462
|
+
return clientId;
|
|
2463
|
+
},
|
|
2229
2464
|
ensureConnected,
|
|
2230
2465
|
isConnected() {
|
|
2231
2466
|
return Boolean(socket && !socket.destroyed && clientId);
|
|
@@ -2237,6 +2472,7 @@ function createDesktopIpcClient({
|
|
|
2237
2472
|
},
|
|
2238
2473
|
sendRequest,
|
|
2239
2474
|
sendDiscoveryRequest,
|
|
2475
|
+
sendBroadcast,
|
|
2240
2476
|
close,
|
|
2241
2477
|
};
|
|
2242
2478
|
}
|
|
@@ -2257,7 +2493,7 @@ function desktopFollowerPayloadForResponse(route, responseMessage) {
|
|
|
2257
2493
|
method,
|
|
2258
2494
|
params: {
|
|
2259
2495
|
conversationId: route.threadId,
|
|
2260
|
-
requestId: route.requestId,
|
|
2496
|
+
requestId: route.desktopRequestId ?? route.requestId,
|
|
2261
2497
|
response: {
|
|
2262
2498
|
answers,
|
|
2263
2499
|
},
|
|
@@ -2435,7 +2671,9 @@ function projectPendingDesktopAction(threadId, request) {
|
|
|
2435
2671
|
}
|
|
2436
2672
|
|
|
2437
2673
|
return {
|
|
2438
|
-
|
|
2674
|
+
// Keep the original JSON-RPC scalar for the relay request and the Desktop
|
|
2675
|
+
// follower reply. requestIdKey is only an internal map/deduplication key.
|
|
2676
|
+
id: request.id,
|
|
2439
2677
|
method,
|
|
2440
2678
|
params: {
|
|
2441
2679
|
...params,
|