@makerbi/remodex 2.0.1 → 2.3.0
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/package.json +2 -2
- package/src/account-status.js +5 -4
- package/src/bridge.js +1809 -689
- package/src/codex-desktop-refresher.js +35 -7
- package/src/cursor-acp-client.js +242 -0
- package/src/cursor-models.js +134 -0
- package/src/cursor-provider.js +1197 -0
- package/src/desktop-ipc-action-follower.js +2331 -964
- package/src/desktop-ipc-conversation-adapter.js +1132 -0
- package/src/desktop-ipc-conversation-projector.js +1169 -0
- package/src/desktop-ipc-live-owner.js +1790 -0
- package/src/desktop-ipc-owner-transport.js +750 -0
- package/src/desktop-ipc-shared.js +473 -0
- package/src/desktop-ipc-state-patches.js +218 -0
- package/src/opencode-models.js +108 -0
- package/src/opencode-provider.js +1151 -0
- package/src/project-handler.js +50 -7
- package/src/project-registry.js +466 -0
- package/src/push-notification-tracker.js +4 -4
- package/src/rollout-live-mirror.js +930 -218
- package/src/rollout-turn-semantics.js +20 -0
- package/src/runtime-provider-models.js +164 -0
- package/src/runtime-provider-router.js +365 -0
- package/src/scripts/codex-refresh.applescript +26 -15
- package/src/secure-transport.js +204 -9
- package/src/session-jsonl-history.js +429 -39
- package/src/thread-context-handler.js +8 -6
- package/src/thread-runtime-settings-store.js +247 -0
- package/src/voice-audio.js +344 -0
- package/src/voice-handler.js +363 -173
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
// FILE: desktop-ipc-shared.js
|
|
2
|
+
// Purpose: Shared primitives for the Codex Desktop IPC modules (framing, socket path, JSON helpers).
|
|
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
|
|
6
|
+
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { createHash } = require("crypto");
|
|
10
|
+
|
|
11
|
+
const FRAME_HEADER_BYTES = 4;
|
|
12
|
+
const MAX_FRAME_BYTES = 256 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
const CLIENT_STATUS_CHANGED = "client-status-changed";
|
|
15
|
+
|
|
16
|
+
// Single source of truth for Codex Desktop's IPC method versions. Desktop's
|
|
17
|
+
// bundled map validates versions on both requests and broadcasts, and this
|
|
18
|
+
// table already drifted once while it lived in two modules.
|
|
19
|
+
const DESKTOP_IPC_METHOD_VERSIONS = new Map([
|
|
20
|
+
["initialize", 1],
|
|
21
|
+
[CLIENT_STATUS_CHANGED, 1],
|
|
22
|
+
// Desktop pins thread-stream-state-changed at version 11 and drops mismatches.
|
|
23
|
+
["thread-stream-state-changed", 11],
|
|
24
|
+
["thread-archived", 2],
|
|
25
|
+
["thread-unarchived", 1],
|
|
26
|
+
["thread-read-state-changed", 1],
|
|
27
|
+
["thread-queued-followups-changed", 1],
|
|
28
|
+
["thread-follower-start-turn", 1],
|
|
29
|
+
["thread-follower-load-complete-history", 1],
|
|
30
|
+
["thread-follower-update-thread-settings", 1],
|
|
31
|
+
["thread-follower-compact-thread", 1],
|
|
32
|
+
["thread-follower-steer-turn", 1],
|
|
33
|
+
["thread-follower-interrupt-turn", 2],
|
|
34
|
+
["thread-follower-set-model-and-reasoning", 1],
|
|
35
|
+
["thread-follower-set-collaboration-mode", 1],
|
|
36
|
+
["thread-follower-edit-last-user-turn", 2],
|
|
37
|
+
["thread-follower-command-approval-decision", 1],
|
|
38
|
+
["thread-follower-file-approval-decision", 1],
|
|
39
|
+
["thread-follower-permissions-request-approval-response", 1],
|
|
40
|
+
["thread-follower-submit-user-input", 1],
|
|
41
|
+
["thread-follower-submit-mcp-server-elicitation-response", 1],
|
|
42
|
+
["thread-follower-set-queued-follow-ups-state", 1],
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// Mirrors Codex's ContextualUserFragment registry. Keep this exact: arbitrary
|
|
46
|
+
// XML is valid user input, while these runtime-owned markers are hidden history.
|
|
47
|
+
const CONTEXT_MARKER_PAIRS = [
|
|
48
|
+
["<environment_context>", "</environment_context>"],
|
|
49
|
+
["<skill>", "</skill>"],
|
|
50
|
+
["<user_shell_command>", "</user_shell_command>"],
|
|
51
|
+
["<turn_aborted>", "</turn_aborted>"],
|
|
52
|
+
["<subagent_notification>", "</subagent_notification>"],
|
|
53
|
+
["<recommended_plugins>", "</recommended_plugins>"],
|
|
54
|
+
["<goal_context>", "</goal_context>"],
|
|
55
|
+
// Review mode records this raw handoff in history, then emits the visible
|
|
56
|
+
// review result separately. Showing it as a user bubble leaks runtime state.
|
|
57
|
+
["<user_action>", "</user_action>"],
|
|
58
|
+
];
|
|
59
|
+
const LEGACY_CONTEXT_WARNING_PREFIXES = [
|
|
60
|
+
"Warning: The maximum number of unified exec processes you can keep open is",
|
|
61
|
+
"Warning: Your account was flagged for potentially high-risk cyber activity",
|
|
62
|
+
];
|
|
63
|
+
const LEGACY_APPLY_PATCH_WARNING_PREFIX = "Warning: apply_patch was requested via ";
|
|
64
|
+
const LEGACY_APPLY_PATCH_WARNING_SUFFIX = "Use the apply_patch tool instead of exec_command.";
|
|
65
|
+
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>$/;
|
|
68
|
+
const PROMPT_REQUEST_BEGIN = "## My request for Codex:";
|
|
69
|
+
const REVIEW_PROMPT_PREFIX = "## Code review guidelines:";
|
|
70
|
+
|
|
71
|
+
// Attachments ride as input_image entries framed by "<image>"/"</image>" text
|
|
72
|
+
// entries, so an image-only item's joined text is exactly an empty tag pair.
|
|
73
|
+
// That incidentally matches the context shape, but it is NOT injected context:
|
|
74
|
+
// classifying it as such would drop image-only user messages (the image entries
|
|
75
|
+
// live in the same item the callers discard). Strip the placeholders before
|
|
76
|
+
// classifying, and never surface them as visible bubble text.
|
|
77
|
+
const IMAGE_PLACEHOLDER_PAIR = /<image>\s*<\/image>/gi;
|
|
78
|
+
const IMAGE_PLACEHOLDER_TOKEN = /^<\/?image>$/i;
|
|
79
|
+
const RUNTIME_IMAGE_OPENING_TAG = /<image\s+name=\[Image #\d+\]\s+path=(?:"[^"\r\n]+"|'[^'\r\n]+'|[^\s<>]+)\s*>/gi;
|
|
80
|
+
|
|
81
|
+
function stripImagePlaceholders(text) {
|
|
82
|
+
let sawRuntimeImageOpeningTag = false;
|
|
83
|
+
const withoutRuntimeOpeners = text.replace(RUNTIME_IMAGE_OPENING_TAG, () => {
|
|
84
|
+
sawRuntimeImageOpeningTag = true;
|
|
85
|
+
return "";
|
|
86
|
+
});
|
|
87
|
+
const withoutPairs = withoutRuntimeOpeners.replace(IMAGE_PLACEHOLDER_PAIR, "");
|
|
88
|
+
if (sawRuntimeImageOpeningTag) {
|
|
89
|
+
return withoutPairs.replace(/<\/image>/gi, "");
|
|
90
|
+
}
|
|
91
|
+
return IMAGE_PLACEHOLDER_TOKEN.test(withoutPairs.trim()) ? "" : withoutPairs;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isContextualUserText(text) {
|
|
95
|
+
const raw = typeof text === "string" ? text : "";
|
|
96
|
+
const trimmed = stripImagePlaceholders(raw).trim();
|
|
97
|
+
if (!trimmed) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
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;
|
|
104
|
+
}
|
|
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;
|
|
117
|
+
}
|
|
118
|
+
if (INTERNAL_CONTEXT_PATTERN.test(trimmed) || EXTERNAL_CONTEXT_PATTERN.test(trimmed)) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (LEGACY_CONTEXT_WARNING_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return trimmed.startsWith(LEGACY_APPLY_PATCH_WARNING_PREFIX)
|
|
125
|
+
&& trimmed.endsWith(LEGACY_APPLY_PATCH_WARNING_SUFFIX);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function decodeXmlText(text) {
|
|
129
|
+
return text.replace(/&(lt|gt|quot|apos|amp);/g, (match, entity) => ({
|
|
130
|
+
lt: "<",
|
|
131
|
+
gt: ">",
|
|
132
|
+
quot: "\"",
|
|
133
|
+
apos: "'",
|
|
134
|
+
amp: "&",
|
|
135
|
+
})[entity] || match);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function extractNestedRuntimeText(text, outerTag, innerTag) {
|
|
139
|
+
const outerPattern = new RegExp(`^<${outerTag}>[\\s\\S]*<\\/${outerTag}>$`, "i");
|
|
140
|
+
if (!outerPattern.test(text.trim())) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const innerPattern = new RegExp(`<${innerTag}>([\\s\\S]*?)<\\/${innerTag}>`, "i");
|
|
144
|
+
const match = innerPattern.exec(text);
|
|
145
|
+
return match ? decodeXmlText(match[1]).trim() : null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Some runtime wrappers are visible triggers, not hidden context. Codex.app
|
|
149
|
+
// presents only their human-facing payload and keeps transport metadata private.
|
|
150
|
+
function extractVisibleRuntimeEnvelope(text) {
|
|
151
|
+
const trimmed = text.trim();
|
|
152
|
+
const envelopePairs = [
|
|
153
|
+
["heartbeat", "instructions"],
|
|
154
|
+
["codex_delegation", "input"],
|
|
155
|
+
["realtime_delegation", "input"],
|
|
156
|
+
["sidechat_boundary", "latest_user_message"],
|
|
157
|
+
];
|
|
158
|
+
for (const [outerTag, innerTag] of envelopePairs) {
|
|
159
|
+
const extracted = extractNestedRuntimeText(trimmed, outerTag, innerTag);
|
|
160
|
+
if (extracted != null) {
|
|
161
|
+
return extracted;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const hookPrompt = /^<hook_prompt\s+hook_run_id=(?:"[^"]+"|'[^']+')>([\s\S]*?)<\/hook_prompt>$/i.exec(trimmed);
|
|
166
|
+
return hookPrompt ? decodeXmlText(hookPrompt[1]).trim() : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Mirrors Desktop's extract_prompt_request: IDE-context prompts embed the real
|
|
170
|
+
// request after the last "## My request for Codex:" delimiter.
|
|
171
|
+
function visibleUserPromptText(text) {
|
|
172
|
+
if (typeof text !== "string" || !text) {
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
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)) {
|
|
179
|
+
return "";
|
|
180
|
+
}
|
|
181
|
+
const requestIndex = cleaned.lastIndexOf(PROMPT_REQUEST_BEGIN);
|
|
182
|
+
if (requestIndex >= 0) {
|
|
183
|
+
const request = cleaned.slice(requestIndex + PROMPT_REQUEST_BEGIN.length).trim();
|
|
184
|
+
// A few IDE/review exports end with the delimiter but omit its request
|
|
185
|
+
// suffix. They still contain a real visible prompt before that marker;
|
|
186
|
+
// returning an empty string made live mirroring erase the opener while
|
|
187
|
+
// JSONL history retained it. Re-check only the body before the delimiter.
|
|
188
|
+
// A hidden runtime fragment
|
|
189
|
+
// can itself contain a trailing delimiter; falling back to the whole input
|
|
190
|
+
// would surface that fragment to the phone.
|
|
191
|
+
if (request) {
|
|
192
|
+
return request;
|
|
193
|
+
}
|
|
194
|
+
const body = cleaned.slice(0, requestIndex).trimEnd();
|
|
195
|
+
return isContextualUserText(body) ? "" : body;
|
|
196
|
+
}
|
|
197
|
+
const envelopeText = extractVisibleRuntimeEnvelope(cleaned);
|
|
198
|
+
if (envelopeText != null) {
|
|
199
|
+
return envelopeText;
|
|
200
|
+
}
|
|
201
|
+
return cleaned;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Sanitizes text fragments independently so a hidden fragment cannot cause a
|
|
205
|
+
// sibling prompt or image attachment in the same user item to be discarded.
|
|
206
|
+
function sanitizeUserInputEntries(entries) {
|
|
207
|
+
if (!Array.isArray(entries)) {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
const sanitized = [];
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
if (typeof entry === "string") {
|
|
213
|
+
const visible = visibleUserPromptText(entry);
|
|
214
|
+
if (visible) {
|
|
215
|
+
sanitized.push(visible);
|
|
216
|
+
}
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (!entry || typeof entry !== "object") {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const textKey = ["text", "message", "content"].find((key) => typeof entry[key] === "string");
|
|
223
|
+
if (!textKey) {
|
|
224
|
+
sanitized.push(entry);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const visible = visibleUserPromptText(entry[textKey]);
|
|
228
|
+
if (!visible) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
sanitized.push(visible === entry[textKey] ? entry : { ...entry, [textKey]: visible });
|
|
232
|
+
}
|
|
233
|
+
return sanitized;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function sanitizeUserRoleItem(item) {
|
|
237
|
+
if (!isUserRoleItem(item)) {
|
|
238
|
+
return item;
|
|
239
|
+
}
|
|
240
|
+
let sanitized = item;
|
|
241
|
+
let changed = false;
|
|
242
|
+
|
|
243
|
+
if (Array.isArray(item.content)) {
|
|
244
|
+
const content = sanitizeUserInputEntries(item.content);
|
|
245
|
+
changed = content.length !== item.content.length
|
|
246
|
+
|| content.some((entry, index) => entry !== item.content[index]);
|
|
247
|
+
if (changed) {
|
|
248
|
+
sanitized = { ...sanitized, content };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const key of ["text", "message"]) {
|
|
253
|
+
if (typeof sanitized[key] !== "string") {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const visible = visibleUserPromptText(sanitized[key]);
|
|
257
|
+
if (!visible && !Array.isArray(sanitized.content)) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
if (visible !== sanitized[key]) {
|
|
261
|
+
sanitized = { ...sanitized, [key]: visible };
|
|
262
|
+
changed = true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const hasContent = Array.isArray(sanitized.content) && sanitized.content.length > 0;
|
|
267
|
+
const hasDirectText = ["text", "message"].some((key) => (
|
|
268
|
+
typeof sanitized[key] === "string" && sanitized[key].trim()
|
|
269
|
+
));
|
|
270
|
+
if (Array.isArray(sanitized.content) && !hasContent && !hasDirectText) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return changed ? sanitized : item;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Extracts the visible human prompt from turn-start input entries while dropping
|
|
278
|
+
// injected context fragments. Used by Desktop IPC and rollout mirrors.
|
|
279
|
+
function visibleUserPromptFromInputEntries(input) {
|
|
280
|
+
const entries = Array.isArray(input) ? input : [input];
|
|
281
|
+
return entries
|
|
282
|
+
.map(readInputEntryText)
|
|
283
|
+
.map((text) => visibleUserPromptText(text).trim())
|
|
284
|
+
.filter(Boolean)
|
|
285
|
+
.join("\n")
|
|
286
|
+
.trim();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function readInputEntryText(entry) {
|
|
290
|
+
if (typeof entry === "string") {
|
|
291
|
+
return entry;
|
|
292
|
+
}
|
|
293
|
+
if (!entry || typeof entry !== "object") {
|
|
294
|
+
return "";
|
|
295
|
+
}
|
|
296
|
+
if (typeof entry.text === "string") {
|
|
297
|
+
return entry.text;
|
|
298
|
+
}
|
|
299
|
+
if (typeof entry.message === "string") {
|
|
300
|
+
return entry.message;
|
|
301
|
+
}
|
|
302
|
+
if (typeof entry.content === "string") {
|
|
303
|
+
return entry.content;
|
|
304
|
+
}
|
|
305
|
+
const content = Array.isArray(entry.content) ? entry.content : [];
|
|
306
|
+
return content
|
|
307
|
+
.map(readInputEntryText)
|
|
308
|
+
.filter(Boolean)
|
|
309
|
+
.join("\n");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function readString(value) {
|
|
313
|
+
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function readText(value) {
|
|
317
|
+
return typeof value === "string" ? value : "";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function hasVisiblePlanUpdate(explanation, plan) {
|
|
321
|
+
return Boolean(readString(explanation)) || (Array.isArray(plan) && plan.length > 0);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function normalizeToken(value) {
|
|
325
|
+
return typeof value === "string"
|
|
326
|
+
? value.toLowerCase().replace(/[_-\s]+/g, "")
|
|
327
|
+
: "";
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function cloneJSON(value) {
|
|
331
|
+
if (value == null) {
|
|
332
|
+
return value;
|
|
333
|
+
}
|
|
334
|
+
return JSON.parse(JSON.stringify(value));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function isPlainJSONObject(value) {
|
|
338
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Single predicate for "this timeline item is a user message", shared by the
|
|
342
|
+
// relay sanitizer, the JSONL history parser, and the Desktop-bound adapter so
|
|
343
|
+
// context filters can never drift apart across paths again.
|
|
344
|
+
function isUserRoleItem(item) {
|
|
345
|
+
const type = normalizeToken(item?.type);
|
|
346
|
+
if (type === "usermessage") {
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
return type === "message" && normalizeToken(item?.role) === "user";
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function readUserItemText(item) {
|
|
353
|
+
const direct = readString(item?.text) || readString(item?.message);
|
|
354
|
+
if (direct) {
|
|
355
|
+
return direct;
|
|
356
|
+
}
|
|
357
|
+
const content = Array.isArray(item?.content) ? item.content : [];
|
|
358
|
+
return content
|
|
359
|
+
.map((entry) => {
|
|
360
|
+
if (typeof entry === "string") {
|
|
361
|
+
return entry;
|
|
362
|
+
}
|
|
363
|
+
if (!entry || typeof entry !== "object") {
|
|
364
|
+
return "";
|
|
365
|
+
}
|
|
366
|
+
return typeof entry.text === "string" ? entry.text : "";
|
|
367
|
+
})
|
|
368
|
+
.filter(Boolean)
|
|
369
|
+
.join("\n");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// A stream snapshot that carries an actively running turn is evidence the
|
|
373
|
+
// sender's runtime is executing the conversation. Idle snapshots also arrive
|
|
374
|
+
// for threads a peer merely viewed or re-broadcast on reconnect, so they are
|
|
375
|
+
// weaker claims: strong enough to take over an idle thread, but never one the
|
|
376
|
+
// local app-server is still running.
|
|
377
|
+
function conversationSnapshotShowsActiveTurn(change) {
|
|
378
|
+
const conversationState = change?.conversationState || change?.conversation_state;
|
|
379
|
+
const turns = Array.isArray(conversationState?.turns) ? conversationState.turns : [];
|
|
380
|
+
return turns.some((turn) => {
|
|
381
|
+
const status = normalizeToken(turn?.status);
|
|
382
|
+
return status === "inprogress" || status === "running" || status === "active";
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function safeParseJSON(value) {
|
|
387
|
+
try {
|
|
388
|
+
return JSON.parse(value);
|
|
389
|
+
} catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function requestIdKey(value) {
|
|
395
|
+
if (typeof value === "string" && value) {
|
|
396
|
+
return value;
|
|
397
|
+
}
|
|
398
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
399
|
+
return String(value);
|
|
400
|
+
}
|
|
401
|
+
return "";
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function writeFrame(socket, payload, callback) {
|
|
405
|
+
const body = Buffer.from(payload, "utf8");
|
|
406
|
+
const header = Buffer.alloc(FRAME_HEADER_BYTES);
|
|
407
|
+
header.writeUInt32LE(body.length, 0);
|
|
408
|
+
socket.write(Buffer.concat([header, body]), callback);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function resolveDefaultIpcSocketPath() {
|
|
412
|
+
if (process.platform === "win32") {
|
|
413
|
+
return "\\\\.\\pipe\\codex-ipc";
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
417
|
+
return path.join(os.tmpdir(), "codex-ipc", `ipc-${uid}.sock`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// A source-neutral alias joins the same assistant prose when rollout events
|
|
421
|
+
// lack Codex's provider item id but JSONL/canonical history has one. It is
|
|
422
|
+
// deliberately scoped by turn and text, never used as a global text dedupe.
|
|
423
|
+
function buildRemodexSourceItemKey(turnId, text) {
|
|
424
|
+
const normalizedTurnId = readString(turnId) || "turnless";
|
|
425
|
+
const normalizedText = readString(text);
|
|
426
|
+
if (!normalizedText) {
|
|
427
|
+
return "";
|
|
428
|
+
}
|
|
429
|
+
const textHash = createHash("sha256").update(normalizedText).digest("hex").slice(0, 16);
|
|
430
|
+
return `${normalizedTurnId}:${textHash}`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function responseItemMessageText(payload) {
|
|
434
|
+
const direct = readString(payload?.text) || readString(payload?.message);
|
|
435
|
+
if (direct) return direct;
|
|
436
|
+
const content = Array.isArray(payload?.content) ? payload.content : [];
|
|
437
|
+
return content.map((part) => {
|
|
438
|
+
const type = normalizeToken(part?.type).replace(/[_-]/g, "");
|
|
439
|
+
if (type === "skill") return `$${readString(part?.id) || readString(part?.name)}`;
|
|
440
|
+
if (type === "mention") return `@${readString(part?.name) || readString(part?.id)}`;
|
|
441
|
+
return readString(part?.text)
|
|
442
|
+
|| readString(part?.content)
|
|
443
|
+
|| readString(part?.message)
|
|
444
|
+
|| readString(part?.data?.text);
|
|
445
|
+
}).filter(Boolean).join("\n");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
module.exports = {
|
|
449
|
+
CLIENT_STATUS_CHANGED,
|
|
450
|
+
buildRemodexSourceItemKey,
|
|
451
|
+
DESKTOP_IPC_METHOD_VERSIONS,
|
|
452
|
+
FRAME_HEADER_BYTES,
|
|
453
|
+
MAX_FRAME_BYTES,
|
|
454
|
+
cloneJSON,
|
|
455
|
+
conversationSnapshotShowsActiveTurn,
|
|
456
|
+
hasVisiblePlanUpdate,
|
|
457
|
+
isContextualUserText,
|
|
458
|
+
isPlainJSONObject,
|
|
459
|
+
isUserRoleItem,
|
|
460
|
+
normalizeToken,
|
|
461
|
+
readString,
|
|
462
|
+
readText,
|
|
463
|
+
readUserItemText,
|
|
464
|
+
responseItemMessageText,
|
|
465
|
+
requestIdKey,
|
|
466
|
+
resolveDefaultIpcSocketPath,
|
|
467
|
+
safeParseJSON,
|
|
468
|
+
sanitizeUserInputEntries,
|
|
469
|
+
sanitizeUserRoleItem,
|
|
470
|
+
visibleUserPromptText,
|
|
471
|
+
visibleUserPromptFromInputEntries,
|
|
472
|
+
writeFrame,
|
|
473
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// FILE: desktop-ipc-state-patches.js
|
|
2
|
+
// Purpose: Immer-style JSON diffing and patch replay for Desktop conversationState broadcasts.
|
|
3
|
+
// Layer: CLI helper
|
|
4
|
+
// Exports: buildConversationStatePatches, applyPatchesToBaselineState, patch size defaults
|
|
5
|
+
// Depends on: ./desktop-ipc-shared
|
|
6
|
+
|
|
7
|
+
const { cloneJSON, isPlainJSONObject } = require("./desktop-ipc-shared");
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MAX_PATCH_COUNT = 2_000;
|
|
10
|
+
const DEFAULT_MAX_PATCH_BYTES = 512 * 1024;
|
|
11
|
+
|
|
12
|
+
function buildConversationStatePatches(previousState, currentState, {
|
|
13
|
+
maxPatchCount = DEFAULT_MAX_PATCH_COUNT,
|
|
14
|
+
maxPatchBytes = DEFAULT_MAX_PATCH_BYTES,
|
|
15
|
+
} = {}) {
|
|
16
|
+
if (!previousState || typeof previousState !== "object" || !currentState || typeof currentState !== "object") {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const patches = [];
|
|
20
|
+
const ok = collectJSONPatches(previousState, currentState, [], patches, maxPatchCount);
|
|
21
|
+
if (!ok) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
if (patches.length === 0) {
|
|
25
|
+
return patches;
|
|
26
|
+
}
|
|
27
|
+
const patchBytes = Buffer.byteLength(JSON.stringify(patches), "utf8");
|
|
28
|
+
if (patchBytes > maxPatchBytes) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return patches;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function collectJSONPatches(previousValue, currentValue, pathParts, patches, maxPatchCount) {
|
|
35
|
+
if (jsonValuesEqual(previousValue, currentValue)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (patches.length > maxPatchCount) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (Array.isArray(previousValue) || Array.isArray(currentValue)) {
|
|
43
|
+
if (!Array.isArray(previousValue) || !Array.isArray(currentValue)) {
|
|
44
|
+
return pushPatch(patches, maxPatchCount, {
|
|
45
|
+
op: "replace",
|
|
46
|
+
path: pathParts,
|
|
47
|
+
value: cloneJSON(currentValue),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return collectArrayPatches(previousValue, currentValue, pathParts, patches, maxPatchCount);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (isPlainJSONObject(previousValue) || isPlainJSONObject(currentValue)) {
|
|
54
|
+
if (!isPlainJSONObject(previousValue) || !isPlainJSONObject(currentValue)) {
|
|
55
|
+
return pushPatch(patches, maxPatchCount, {
|
|
56
|
+
op: "replace",
|
|
57
|
+
path: pathParts,
|
|
58
|
+
value: cloneJSON(currentValue),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return collectObjectPatches(previousValue, currentValue, pathParts, patches, maxPatchCount);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return pushPatch(patches, maxPatchCount, {
|
|
65
|
+
op: "replace",
|
|
66
|
+
path: pathParts,
|
|
67
|
+
value: cloneJSON(currentValue),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function collectArrayPatches(previousArray, currentArray, pathParts, patches, maxPatchCount) {
|
|
72
|
+
const sharedLength = Math.min(previousArray.length, currentArray.length);
|
|
73
|
+
for (let index = 0; index < sharedLength; index += 1) {
|
|
74
|
+
if (!collectJSONPatches(previousArray[index], currentArray[index], [...pathParts, index], patches, maxPatchCount)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (let index = previousArray.length - 1; index >= currentArray.length; index -= 1) {
|
|
79
|
+
if (!pushPatch(patches, maxPatchCount, {
|
|
80
|
+
op: "remove",
|
|
81
|
+
path: [...pathParts, index],
|
|
82
|
+
})) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (let index = sharedLength; index < currentArray.length; index += 1) {
|
|
87
|
+
if (!pushPatch(patches, maxPatchCount, {
|
|
88
|
+
op: "add",
|
|
89
|
+
path: [...pathParts, index],
|
|
90
|
+
value: cloneJSON(currentArray[index]),
|
|
91
|
+
})) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function collectObjectPatches(previousObject, currentObject, pathParts, patches, maxPatchCount) {
|
|
99
|
+
for (const key of Object.keys(previousObject)) {
|
|
100
|
+
if (Object.prototype.hasOwnProperty.call(currentObject, key)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!pushPatch(patches, maxPatchCount, {
|
|
104
|
+
op: "remove",
|
|
105
|
+
path: [...pathParts, key],
|
|
106
|
+
})) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const key of Object.keys(currentObject)) {
|
|
112
|
+
if (!Object.prototype.hasOwnProperty.call(previousObject, key)) {
|
|
113
|
+
if (!pushPatch(patches, maxPatchCount, {
|
|
114
|
+
op: "add",
|
|
115
|
+
path: [...pathParts, key],
|
|
116
|
+
value: cloneJSON(currentObject[key]),
|
|
117
|
+
})) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!collectJSONPatches(previousObject[key], currentObject[key], [...pathParts, key], patches, maxPatchCount)) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function pushPatch(patches, maxPatchCount, patch) {
|
|
130
|
+
if (patch.path.length === 0) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
patches.push(patch);
|
|
134
|
+
return patches.length <= maxPatchCount;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function applyPatchesToBaselineState(baselineState, patches) {
|
|
138
|
+
for (const patch of patches) {
|
|
139
|
+
if (!applyPatchToBaselineNode(baselineState, patch)) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function applyPatchToBaselineNode(target, patch) {
|
|
147
|
+
const pathParts = Array.isArray(patch?.path) ? patch.path : [];
|
|
148
|
+
const op = patch?.op;
|
|
149
|
+
if (pathParts.length === 0) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let parent = target;
|
|
154
|
+
for (let index = 0; index < pathParts.length - 1; index += 1) {
|
|
155
|
+
parent = parent?.[pathParts[index]];
|
|
156
|
+
if (parent == null || typeof parent !== "object") {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const key = pathParts[pathParts.length - 1];
|
|
162
|
+
const isArrayIndex = Array.isArray(parent) && Number.isInteger(key);
|
|
163
|
+
if (op === "remove") {
|
|
164
|
+
if (isArrayIndex) {
|
|
165
|
+
if (key < 0 || key >= parent.length) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
parent.splice(key, 1);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (parent && typeof parent === "object") {
|
|
172
|
+
delete parent[key];
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
if (op === "add" && isArrayIndex) {
|
|
178
|
+
if (key < 0 || key > parent.length) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
parent.splice(key, 0, patch.value);
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
if (op === "add" || op === "replace") {
|
|
185
|
+
if (isArrayIndex) {
|
|
186
|
+
if (key < 0 || key >= parent.length) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
parent[key] = patch.value;
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
if (parent && typeof parent === "object") {
|
|
193
|
+
parent[key] = patch.value;
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function jsonValuesEqual(left, right) {
|
|
201
|
+
if (left === right) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
if (left == null || right == null) {
|
|
205
|
+
return left === right;
|
|
206
|
+
}
|
|
207
|
+
if (typeof left !== "object" || typeof right !== "object") {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
DEFAULT_MAX_PATCH_BYTES,
|
|
215
|
+
DEFAULT_MAX_PATCH_COUNT,
|
|
216
|
+
applyPatchesToBaselineState,
|
|
217
|
+
buildConversationStatePatches,
|
|
218
|
+
};
|