@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.
@@ -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, cloneJSON, normalizeToken, readString, readText, requestIdKey, resolveDefaultIpcSocketPath, safeParseJSON, writeFrame
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", 1],
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", 2],
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 INTERNAL_CONTEXT_PATTERN = /^<codex_internal_context\s+source=(?:"[a-z][a-z0-9_]*"|'[a-z][a-z0-9_]*')>[\s\S]*<\/codex_internal_context>$/;
67
- const EXTERNAL_CONTEXT_PATTERN = /^<external_([a-z0-9_-]+)>[\s\S]*<\/external_\1>$/;
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
- function isContextualUserText(text) {
95
- const raw = typeof text === "string" ? text : "";
96
- const trimmed = stripImagePlaceholders(raw).trim();
97
- if (!trimmed) {
98
- return false;
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
- // Review envelopes contain a real request after the delimiter and are never
101
- // wholly contextual, even when that request itself contains reserved markup.
102
- if (trimmed.startsWith(REVIEW_PROMPT_PREFIX) && trimmed.includes(PROMPT_REQUEST_BEGIN)) {
103
- return false;
125
+
126
+ const internal = INTERNAL_CONTEXT_PREFIX_PATTERN.exec(text);
127
+ if (internal) {
128
+ return text.slice(internal[0].length);
104
129
  }
105
- const normalized = trimmed.toLowerCase();
106
- if (normalized.startsWith(AGENTS_INSTRUCTIONS_PREFIX.toLowerCase())) {
107
- // Runtime context can concatenate AGENTS.md with any registered hidden
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
- if (INTERNAL_CONTEXT_PATTERN.test(trimmed) || EXTERNAL_CONTEXT_PATTERN.test(trimmed)) {
119
- return true;
134
+
135
+ if (lower.startsWith(AGENTS_INSTRUCTIONS_PREFIX.toLowerCase())) {
136
+ return consumeAgentsInstructionsFragment(text, lower);
120
137
  }
121
- if (LEGACY_CONTEXT_WARNING_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) {
122
- return true;
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
- return trimmed.startsWith(LEGACY_APPLY_PATCH_WARNING_PREFIX)
125
- && trimmed.endsWith(LEGACY_APPLY_PATCH_WARNING_SUFFIX);
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
- // Context bodies can contain the request delimiter as ordinary text. Classify
177
- // the complete fragment first so the delimiter cannot reveal hidden content.
178
- if (isContextualUserText(cleaned)) {
259
+ const trimmed = cleaned.trim();
260
+ if (!trimmed) {
179
261
  return "";
180
262
  }
181
- const requestIndex = cleaned.lastIndexOf(PROMPT_REQUEST_BEGIN);
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 = cleaned.slice(requestIndex + PROMPT_REQUEST_BEGIN.length).trim();
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 body = cleaned.slice(0, requestIndex).trimEnd();
195
- return isContextualUserText(body) ? "" : body;
288
+ const precedingBody = body.slice(0, requestIndex).trimEnd();
289
+ return isContextualUserText(precedingBody) ? "" : precedingBody;
196
290
  }
197
- const envelopeText = extractVisibleRuntimeEnvelope(cleaned);
291
+ const envelopeText = extractVisibleRuntimeEnvelope(body);
198
292
  if (envelopeText != null) {
199
293
  return envelopeText;
200
294
  }
201
- return cleaned;
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
- function resolveDefaultIpcSocketPath() {
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 path.join(os.tmpdir(), "codex-ipc", `ipc-${uid}.sock`);
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,