@makerbi/remodex 2.3.2 → 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 +80 -61
- package/src/codex-desktop-refresher.js +173 -8
- package/src/codex-tool-wrapper.js +523 -0
- package/src/desktop-ipc-action-follower.js +412 -129
- package/src/desktop-ipc-live-owner.js +148 -6
- package/src/desktop-ipc-owner-transport.js +143 -73
- package/src/desktop-ipc-shared.js +273 -46
- 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 +282 -22
- package/src/rollout-watch.js +176 -54
- package/src/session-jsonl-history.js +77 -33
- 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
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// FILE: desktop-ipc-shared.js
|
|
2
|
-
// Purpose: Shared primitives for the Codex Desktop IPC modules (framing, socket path, JSON helpers).
|
|
2
|
+
// Purpose: Shared primitives for the Codex Desktop IPC modules (framing, socket path, envelopes, JSON helpers).
|
|
3
3
|
// Layer: CLI helper
|
|
4
|
-
// Exports: FRAME_HEADER_BYTES, MAX_FRAME_BYTES,
|
|
5
|
-
// Depends on: os, path
|
|
4
|
+
// Exports: CLIENT_STATUS_CHANGED, DESKTOP_IPC_METHOD_VERSIONS, FRAME_HEADER_BYTES, MAX_FRAME_BYTES, buildIpcRequestEnvelope, createFrameReader, resolveDefaultIpcSocketPath, resolveIpcSocketPathCandidates, toSocketPathResolver, writeFrame, plus JSON/text helpers
|
|
5
|
+
// Depends on: crypto, fs, os, path
|
|
6
6
|
|
|
7
|
+
const fs = require("fs");
|
|
7
8
|
const os = require("os");
|
|
8
9
|
const path = require("path");
|
|
9
10
|
const { createHash } = require("crypto");
|
|
@@ -21,16 +22,18 @@ const DESKTOP_IPC_METHOD_VERSIONS = new Map([
|
|
|
21
22
|
[CLIENT_STATUS_CHANGED, 1],
|
|
22
23
|
// Desktop pins thread-stream-state-changed at version 11 and drops mismatches.
|
|
23
24
|
["thread-stream-state-changed", 11],
|
|
25
|
+
["thread-stream-following-changed", 1],
|
|
26
|
+
["thread-stream-following-status-requested", 1],
|
|
24
27
|
["thread-archived", 2],
|
|
25
28
|
["thread-unarchived", 1],
|
|
26
|
-
["thread-read-state-changed",
|
|
29
|
+
["thread-read-state-changed", 2],
|
|
27
30
|
["thread-queued-followups-changed", 1],
|
|
28
31
|
["thread-follower-start-turn", 1],
|
|
29
32
|
["thread-follower-load-complete-history", 1],
|
|
30
33
|
["thread-follower-update-thread-settings", 1],
|
|
31
34
|
["thread-follower-compact-thread", 1],
|
|
32
35
|
["thread-follower-steer-turn", 1],
|
|
33
|
-
["thread-follower-interrupt-turn",
|
|
36
|
+
["thread-follower-interrupt-turn", 3],
|
|
34
37
|
["thread-follower-set-model-and-reasoning", 1],
|
|
35
38
|
["thread-follower-set-collaboration-mode", 1],
|
|
36
39
|
["thread-follower-edit-last-user-turn", 2],
|
|
@@ -63,8 +66,10 @@ const LEGACY_CONTEXT_WARNING_PREFIXES = [
|
|
|
63
66
|
const LEGACY_APPLY_PATCH_WARNING_PREFIX = "Warning: apply_patch was requested via ";
|
|
64
67
|
const LEGACY_APPLY_PATCH_WARNING_SUFFIX = "Use the apply_patch tool instead of exec_command.";
|
|
65
68
|
const AGENTS_INSTRUCTIONS_PREFIX = "# AGENTS.md instructions";
|
|
66
|
-
const
|
|
67
|
-
const
|
|
69
|
+
const AGENTS_INSTRUCTIONS_BEGIN = "<instructions>";
|
|
70
|
+
const AGENTS_INSTRUCTIONS_END = "</instructions>";
|
|
71
|
+
const INTERNAL_CONTEXT_PREFIX_PATTERN = /^<codex_internal_context\s+source=(?:"[a-z][a-z0-9_]*"|'[a-z][a-z0-9_]*')>[\s\S]*?<\/codex_internal_context>/;
|
|
72
|
+
const EXTERNAL_CONTEXT_PREFIX_PATTERN = /^<external_([a-z0-9_-]+)>[\s\S]*?<\/external_\1>/;
|
|
68
73
|
const PROMPT_REQUEST_BEGIN = "## My request for Codex:";
|
|
69
74
|
const REVIEW_PROMPT_PREFIX = "## Code review guidelines:";
|
|
70
75
|
|
|
@@ -91,38 +96,116 @@ function stripImagePlaceholders(text) {
|
|
|
91
96
|
return IMAGE_PLACEHOLDER_TOKEN.test(withoutPairs.trim()) ? "" : withoutPairs;
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
// Review envelopes contain a real request after the delimiter and are never
|
|
100
|
+
// wholly contextual, even when that request itself contains reserved markup.
|
|
101
|
+
function isReviewEnvelopeText(trimmed) {
|
|
102
|
+
return trimmed.startsWith(REVIEW_PROMPT_PREFIX) && trimmed.includes(PROMPT_REQUEST_BEGIN);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Consumes one runtime-owned fragment anchored at the start of `text` and
|
|
106
|
+
// returns what follows it, or null when the text does not open with one.
|
|
107
|
+
// Codex packs several fragments into a single user item, so the opening and
|
|
108
|
+
// closing markers routinely belong to different fragments (a desktop opener
|
|
109
|
+
// reads "<recommended_plugins>...</recommended_plugins>" + AGENTS.md
|
|
110
|
+
// instructions + "<environment_context>...</environment_context>"). Matching
|
|
111
|
+
// the blob as a whole classifies that item as visible and turns the entire
|
|
112
|
+
// injected preamble into the thread's first user bubble.
|
|
113
|
+
function consumeLeadingContextFragment(text) {
|
|
114
|
+
const lower = text.toLowerCase();
|
|
115
|
+
|
|
116
|
+
for (const [start, end] of CONTEXT_MARKER_PAIRS) {
|
|
117
|
+
if (!lower.startsWith(start)) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const closeIndex = lower.indexOf(end, start.length);
|
|
121
|
+
// An unterminated marker is not a fragment we can bound. Leave it visible
|
|
122
|
+
// rather than swallow a message that merely opens with reserved markup.
|
|
123
|
+
return closeIndex === -1 ? null : text.slice(closeIndex + end.length);
|
|
99
124
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
return
|
|
125
|
+
|
|
126
|
+
const internal = INTERNAL_CONTEXT_PREFIX_PATTERN.exec(text);
|
|
127
|
+
if (internal) {
|
|
128
|
+
return text.slice(internal[0].length);
|
|
104
129
|
}
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
// fragment. Only classify the whole item as hidden when its final fragment
|
|
109
|
-
// is also runtime-owned; a following real user request must stay visible.
|
|
110
|
-
return normalized.endsWith("</instructions>")
|
|
111
|
-
|| CONTEXT_MARKER_PAIRS.some(([, end]) => normalized.endsWith(end));
|
|
112
|
-
}
|
|
113
|
-
if (CONTEXT_MARKER_PAIRS.some(([start, end]) => (
|
|
114
|
-
normalized.startsWith(start) && normalized.endsWith(end)
|
|
115
|
-
))) {
|
|
116
|
-
return true;
|
|
130
|
+
const external = EXTERNAL_CONTEXT_PREFIX_PATTERN.exec(text);
|
|
131
|
+
if (external) {
|
|
132
|
+
return text.slice(external[0].length);
|
|
117
133
|
}
|
|
118
|
-
|
|
119
|
-
|
|
134
|
+
|
|
135
|
+
if (lower.startsWith(AGENTS_INSTRUCTIONS_PREFIX.toLowerCase())) {
|
|
136
|
+
return consumeAgentsInstructionsFragment(text, lower);
|
|
120
137
|
}
|
|
121
|
-
|
|
122
|
-
|
|
138
|
+
|
|
139
|
+
// Unlike the marked fragments above, runtime warnings carry no closing marker
|
|
140
|
+
// and trail free-form runtime lines ("Shell cwd was reset to ..."), so there is
|
|
141
|
+
// no boundary to peel at. They are emitted as whole items that never contain a
|
|
142
|
+
// user request, hence consuming the remainder rather than bounding it.
|
|
143
|
+
if (LEGACY_CONTEXT_WARNING_PREFIXES.some((prefix) => text.startsWith(prefix))) {
|
|
144
|
+
return "";
|
|
145
|
+
}
|
|
146
|
+
return text.startsWith(LEGACY_APPLY_PATCH_WARNING_PREFIX)
|
|
147
|
+
&& text.endsWith(LEGACY_APPLY_PATCH_WARNING_SUFFIX)
|
|
148
|
+
? ""
|
|
149
|
+
: null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function consumeAgentsInstructionsFragment(text, lower) {
|
|
153
|
+
const closeIndex = lower.indexOf(AGENTS_INSTRUCTIONS_END);
|
|
154
|
+
if (closeIndex >= 0) {
|
|
155
|
+
return consumeChainedInstructionsBlocks(text.slice(closeIndex + AGENTS_INSTRUCTIONS_END.length));
|
|
156
|
+
}
|
|
157
|
+
// Older runtimes emit the AGENTS.md body unwrapped and then append another
|
|
158
|
+
// registered fragment; that next marker is the only reliable boundary.
|
|
159
|
+
const nextMarkerIndex = CONTEXT_MARKER_PAIRS
|
|
160
|
+
.map(([start]) => lower.indexOf(start, 1))
|
|
161
|
+
.filter((index) => index > 0)
|
|
162
|
+
.sort((left, right) => left - right)[0];
|
|
163
|
+
return nextMarkerIndex === undefined ? null : text.slice(nextMarkerIndex);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// A single "# AGENTS.md instructions" header can carry one <INSTRUCTIONS> block
|
|
167
|
+
// per nested AGENTS.md file. Stopping at the first close would leave the rest of
|
|
168
|
+
// the preamble in front of the real request, where no registered marker matches
|
|
169
|
+
// and the peeler gives up, turning the leftover into the first user bubble.
|
|
170
|
+
function consumeChainedInstructionsBlocks(text) {
|
|
171
|
+
let rest = text;
|
|
172
|
+
for (;;) {
|
|
173
|
+
const trimmed = rest.trimStart();
|
|
174
|
+
const lower = trimmed.toLowerCase();
|
|
175
|
+
if (!lower.startsWith(AGENTS_INSTRUCTIONS_BEGIN)) {
|
|
176
|
+
return rest;
|
|
177
|
+
}
|
|
178
|
+
const closeIndex = lower.indexOf(AGENTS_INSTRUCTIONS_END, AGENTS_INSTRUCTIONS_BEGIN.length);
|
|
179
|
+
// Unterminated: same rule as everywhere else, leave it visible rather than
|
|
180
|
+
// guess where the injected block ends.
|
|
181
|
+
if (closeIndex === -1) {
|
|
182
|
+
return rest;
|
|
183
|
+
}
|
|
184
|
+
rest = trimmed.slice(closeIndex + AGENTS_INSTRUCTIONS_END.length);
|
|
123
185
|
}
|
|
124
|
-
|
|
125
|
-
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Peels every injected fragment off the front of an already trimmed message and
|
|
189
|
+
// returns the text the user actually typed (empty when nothing else remains).
|
|
190
|
+
function stripLeadingContextFragments(trimmed) {
|
|
191
|
+
let rest = trimmed;
|
|
192
|
+
while (rest) {
|
|
193
|
+
const remainder = consumeLeadingContextFragment(rest);
|
|
194
|
+
if (remainder === null) {
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
rest = remainder.trim();
|
|
198
|
+
}
|
|
199
|
+
return rest;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isContextualUserText(text) {
|
|
203
|
+
const raw = typeof text === "string" ? text : "";
|
|
204
|
+
const trimmed = stripImagePlaceholders(raw).trim();
|
|
205
|
+
if (!trimmed || isReviewEnvelopeText(trimmed)) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
return stripLeadingContextFragments(trimmed) === "";
|
|
126
209
|
}
|
|
127
210
|
|
|
128
211
|
function decodeXmlText(text) {
|
|
@@ -173,14 +256,25 @@ function visibleUserPromptText(text) {
|
|
|
173
256
|
return "";
|
|
174
257
|
}
|
|
175
258
|
const cleaned = stripImagePlaceholders(text);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (isContextualUserText(cleaned)) {
|
|
259
|
+
const trimmed = cleaned.trim();
|
|
260
|
+
if (!trimmed) {
|
|
179
261
|
return "";
|
|
180
262
|
}
|
|
181
|
-
|
|
263
|
+
// Context bodies can contain the request delimiter as ordinary text. Peel the
|
|
264
|
+
// injected fragments off first so the delimiter cannot reveal hidden content,
|
|
265
|
+
// and so a real request that trails them survives instead of the whole blob.
|
|
266
|
+
const stripped = isReviewEnvelopeText(trimmed)
|
|
267
|
+
? trimmed
|
|
268
|
+
: stripLeadingContextFragments(trimmed);
|
|
269
|
+
if (!stripped) {
|
|
270
|
+
return "";
|
|
271
|
+
}
|
|
272
|
+
// Untouched prompts keep their original spacing so callers can still detect
|
|
273
|
+
// "nothing changed" by identity and skip cloning the item.
|
|
274
|
+
const body = stripped === trimmed ? cleaned : stripped;
|
|
275
|
+
const requestIndex = body.lastIndexOf(PROMPT_REQUEST_BEGIN);
|
|
182
276
|
if (requestIndex >= 0) {
|
|
183
|
-
const request =
|
|
277
|
+
const request = body.slice(requestIndex + PROMPT_REQUEST_BEGIN.length).trim();
|
|
184
278
|
// A few IDE/review exports end with the delimiter but omit its request
|
|
185
279
|
// suffix. They still contain a real visible prompt before that marker;
|
|
186
280
|
// returning an empty string made live mirroring erase the opener while
|
|
@@ -191,14 +285,14 @@ function visibleUserPromptText(text) {
|
|
|
191
285
|
if (request) {
|
|
192
286
|
return request;
|
|
193
287
|
}
|
|
194
|
-
const
|
|
195
|
-
return isContextualUserText(
|
|
288
|
+
const precedingBody = body.slice(0, requestIndex).trimEnd();
|
|
289
|
+
return isContextualUserText(precedingBody) ? "" : precedingBody;
|
|
196
290
|
}
|
|
197
|
-
const envelopeText = extractVisibleRuntimeEnvelope(
|
|
291
|
+
const envelopeText = extractVisibleRuntimeEnvelope(body);
|
|
198
292
|
if (envelopeText != null) {
|
|
199
293
|
return envelopeText;
|
|
200
294
|
}
|
|
201
|
-
return
|
|
295
|
+
return body;
|
|
202
296
|
}
|
|
203
297
|
|
|
204
298
|
// Sanitizes text fragments independently so a hidden fragment cannot cause a
|
|
@@ -327,6 +421,24 @@ function normalizeToken(value) {
|
|
|
327
421
|
: "";
|
|
328
422
|
}
|
|
329
423
|
|
|
424
|
+
// The phone's running-state probe, as opposed to a history page: the explicit
|
|
425
|
+
// marker on current clients, or the probe's unique legacy shape (Remodex iPhone
|
|
426
|
+
// 2.1 predates the marker, and real history pages use limits 1 and 5). Every
|
|
427
|
+
// live source answers this request, so they must all recognize it identically.
|
|
428
|
+
function isThreadTurnStateProbeRequest(message) {
|
|
429
|
+
const params = message?.params;
|
|
430
|
+
if (readString(message?.method) !== "thread/turns/list"
|
|
431
|
+
|| readString(params?.cursor)
|
|
432
|
+
|| params?.remodexRequireCanonical === true) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
if (params?.remodexTurnStateOnly === true) {
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
return Number(params?.limit) === 8
|
|
439
|
+
&& normalizeToken(readString(params?.sortDirection) || "desc") === "desc";
|
|
440
|
+
}
|
|
441
|
+
|
|
330
442
|
function cloneJSON(value) {
|
|
331
443
|
if (value == null) {
|
|
332
444
|
return value;
|
|
@@ -408,13 +520,121 @@ function writeFrame(socket, payload, callback) {
|
|
|
408
520
|
socket.write(Buffer.concat([header, body]), callback);
|
|
409
521
|
}
|
|
410
522
|
|
|
411
|
-
|
|
523
|
+
// Codex moved its IPC bus into the Codex home directory; older desktop and CLI
|
|
524
|
+
// builds still expose it under the temp directory. Both are in the wild, so the
|
|
525
|
+
// bus is looked up in that order instead of being pinned to one location.
|
|
526
|
+
// Every participant on the bus — both clients and the fallback router's per-peer
|
|
527
|
+
// sockets — reads the same length-prefixed framing; only the dispatch differs.
|
|
528
|
+
// `onOverflow` owns the connection, so it decides how to drop a bogus peer.
|
|
529
|
+
function createFrameReader({ onFrame, onOverflow }) {
|
|
530
|
+
let buffer = Buffer.alloc(0);
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
push(chunk) {
|
|
534
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
535
|
+
while (buffer.length >= FRAME_HEADER_BYTES) {
|
|
536
|
+
const frameLength = buffer.readUInt32LE(0);
|
|
537
|
+
if (frameLength > MAX_FRAME_BYTES) {
|
|
538
|
+
buffer = Buffer.alloc(0);
|
|
539
|
+
onOverflow?.();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (buffer.length < FRAME_HEADER_BYTES + frameLength) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const payload = buffer
|
|
547
|
+
.slice(FRAME_HEADER_BYTES, FRAME_HEADER_BYTES + frameLength)
|
|
548
|
+
.toString("utf8");
|
|
549
|
+
buffer = buffer.slice(FRAME_HEADER_BYTES + frameLength);
|
|
550
|
+
const envelope = safeParseJSON(payload);
|
|
551
|
+
if (envelope) {
|
|
552
|
+
onFrame(envelope);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
reset() {
|
|
557
|
+
buffer = Buffer.alloc(0);
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Desktop validates the method version and refuses requests from an unknown
|
|
563
|
+
// sender, so both clients must build this envelope the same way.
|
|
564
|
+
function buildIpcRequestEnvelope({ requestId, method, params, clientId, initializing = false }) {
|
|
565
|
+
return {
|
|
566
|
+
type: "request",
|
|
567
|
+
requestId,
|
|
568
|
+
sourceClientId: initializing ? "initializing-client" : clientId || "remodex-bridge",
|
|
569
|
+
version: DESKTOP_IPC_METHOD_VERSIONS.get(method) || 1,
|
|
570
|
+
method,
|
|
571
|
+
params: params || {},
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Newer app-server builds omit every turn unless thread/read opts in explicitly.
|
|
576
|
+
// Internal hydration callers always need a complete baseline: publishing the
|
|
577
|
+
// metadata-only default as a Desktop snapshot would replace the visible history.
|
|
578
|
+
function buildCompleteThreadReadParams(threadId) {
|
|
579
|
+
return {
|
|
580
|
+
threadId: readString(threadId),
|
|
581
|
+
includeTurns: true,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function resolveIpcSocketPathCandidates() {
|
|
412
586
|
if (process.platform === "win32") {
|
|
413
|
-
return "\\\\.\\pipe\\codex-ipc";
|
|
587
|
+
return ["\\\\.\\pipe\\codex-ipc"];
|
|
414
588
|
}
|
|
415
589
|
|
|
590
|
+
const configuredCodexHome = readString(process.env.CODEX_HOME);
|
|
591
|
+
const codexHome = configuredCodexHome
|
|
592
|
+
? path.resolve(configuredCodexHome)
|
|
593
|
+
: path.join(os.homedir(), ".codex");
|
|
416
594
|
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
417
|
-
return
|
|
595
|
+
return [
|
|
596
|
+
path.join(codexHome, "ipc", "ipc.sock"),
|
|
597
|
+
path.join(os.tmpdir(), "codex-ipc", `ipc-${uid}.sock`),
|
|
598
|
+
];
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Compatibility helper for callers that need one path synchronously. Transports
|
|
602
|
+
// use resolveIpcSocketPathCandidates instead because only a connection attempt
|
|
603
|
+
// can distinguish a live listener from a stale socket inode.
|
|
604
|
+
function resolveDefaultIpcSocketPath() {
|
|
605
|
+
const candidates = resolveIpcSocketPathCandidates();
|
|
606
|
+
for (const candidate of candidates) {
|
|
607
|
+
try {
|
|
608
|
+
if (fs.statSync(candidate).isSocket()) {
|
|
609
|
+
return candidate;
|
|
610
|
+
}
|
|
611
|
+
} catch {
|
|
612
|
+
// Missing or unreadable candidate: keep looking.
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return candidates[0];
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// The bus location is only known at connect time, so transports accept either a
|
|
619
|
+
// fixed path or a resolver and always ask again before reconnecting.
|
|
620
|
+
function toSocketPathResolver(socketPath) {
|
|
621
|
+
return typeof socketPath === "function" ? socketPath : () => socketPath;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// A socket inode can outlive its listener. Clients therefore need the full
|
|
625
|
+
// ordered candidate list so they can try the legacy bus after a refused current
|
|
626
|
+
// socket instead of mistaking file existence for liveness.
|
|
627
|
+
function toSocketPathCandidatesResolver(socketPath) {
|
|
628
|
+
const resolveSocketPath = toSocketPathResolver(socketPath);
|
|
629
|
+
return () => {
|
|
630
|
+
const resolved = resolveSocketPath();
|
|
631
|
+
const candidates = Array.isArray(resolved) ? resolved : [resolved];
|
|
632
|
+
return candidates.filter((candidate, index) => (
|
|
633
|
+
typeof candidate === "string"
|
|
634
|
+
&& candidate.length > 0
|
|
635
|
+
&& candidates.indexOf(candidate) === index
|
|
636
|
+
));
|
|
637
|
+
};
|
|
418
638
|
}
|
|
419
639
|
|
|
420
640
|
// A source-neutral alias joins the same assistant prose when rollout events
|
|
@@ -447,7 +667,10 @@ function responseItemMessageText(payload) {
|
|
|
447
667
|
|
|
448
668
|
module.exports = {
|
|
449
669
|
CLIENT_STATUS_CHANGED,
|
|
670
|
+
buildCompleteThreadReadParams,
|
|
671
|
+
buildIpcRequestEnvelope,
|
|
450
672
|
buildRemodexSourceItemKey,
|
|
673
|
+
createFrameReader,
|
|
451
674
|
DESKTOP_IPC_METHOD_VERSIONS,
|
|
452
675
|
FRAME_HEADER_BYTES,
|
|
453
676
|
MAX_FRAME_BYTES,
|
|
@@ -456,6 +679,7 @@ module.exports = {
|
|
|
456
679
|
hasVisiblePlanUpdate,
|
|
457
680
|
isContextualUserText,
|
|
458
681
|
isPlainJSONObject,
|
|
682
|
+
isThreadTurnStateProbeRequest,
|
|
459
683
|
isUserRoleItem,
|
|
460
684
|
normalizeToken,
|
|
461
685
|
readString,
|
|
@@ -464,9 +688,12 @@ module.exports = {
|
|
|
464
688
|
responseItemMessageText,
|
|
465
689
|
requestIdKey,
|
|
466
690
|
resolveDefaultIpcSocketPath,
|
|
691
|
+
resolveIpcSocketPathCandidates,
|
|
467
692
|
safeParseJSON,
|
|
468
693
|
sanitizeUserInputEntries,
|
|
469
694
|
sanitizeUserRoleItem,
|
|
695
|
+
toSocketPathCandidatesResolver,
|
|
696
|
+
toSocketPathResolver,
|
|
470
697
|
visibleUserPromptText,
|
|
471
698
|
visibleUserPromptFromInputEntries,
|
|
472
699
|
writeFrame,
|