@makerbi/remodex 2.3.1 → 2.4.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.
@@ -220,6 +220,34 @@ function applyAppServerMessageToConversationState({
220
220
  conversation.updatedAt = now();
221
221
  return { threadId, changed: true };
222
222
  }
223
+ case "thread/goal/updated": {
224
+ const threadId = readThreadIdFromParams(message.params);
225
+ const goal = normalizeThreadGoal(message.params?.goal, threadId);
226
+ if (!threadId || !shouldOwnThread(threadId) || !goal) {
227
+ return null;
228
+ }
229
+ const conversation = ensureConversationInMap(conversations, threadId, { hostId, now });
230
+ if (goal.status === "complete") {
231
+ conversation.threadGoal = null;
232
+ conversation.completedThreadGoal = goal;
233
+ } else {
234
+ conversation.threadGoal = goal;
235
+ conversation.completedThreadGoal = null;
236
+ }
237
+ conversation.updatedAt = now();
238
+ return { threadId, changed: true };
239
+ }
240
+ case "thread/goal/cleared": {
241
+ const threadId = readThreadIdFromParams(message.params);
242
+ if (!threadId || !shouldOwnThread(threadId)) {
243
+ return null;
244
+ }
245
+ const conversation = ensureConversationInMap(conversations, threadId, { hostId, now });
246
+ conversation.threadGoal = null;
247
+ conversation.completedThreadGoal = null;
248
+ conversation.updatedAt = now();
249
+ return { threadId, changed: true };
250
+ }
223
251
  case "turn/started":
224
252
  case "turn/completed": {
225
253
  const threadId = readThreadIdFromParams(message.params);
@@ -322,6 +350,30 @@ function applyAppServerMessageToConversationState({
322
350
  conversation.updatedAt = now();
323
351
  return { threadId, changed: true };
324
352
  }
353
+ case "item/autoApprovalReview/started":
354
+ case "item/autoApprovalReview/completed": {
355
+ const threadId = readThreadIdFromParams(message.params);
356
+ if (!threadId || !shouldOwnThread(threadId)) {
357
+ return null;
358
+ }
359
+ const item = automaticApprovalReviewItemFromParams(message.params);
360
+ if (!item) {
361
+ return { threadId, changed: false };
362
+ }
363
+ const conversation = ensureConversationInMap(conversations, threadId, { hostId, now });
364
+ const turn = ensureTurn(conversation, resolveTurnIdForParams({
365
+ conversation,
366
+ params: message.params,
367
+ fallbackTurnIdsByThreadId,
368
+ now,
369
+ }), { now });
370
+ if (turn) {
371
+ upsertItem(turn, item);
372
+ turn.firstTurnWorkItemStartedAtMs = turn.firstTurnWorkItemStartedAtMs || now();
373
+ }
374
+ conversation.updatedAt = now();
375
+ return { threadId, changed: true };
376
+ }
325
377
  case "item/agentMessage/delta":
326
378
  case "item/plan/delta":
327
379
  case "item/reasoning/summaryTextDelta":
@@ -582,6 +634,7 @@ function buildConversationTurn(turn, {
582
634
  // Drop it here, position-independently, so no Desktop snapshot path leaks it
583
635
  // as a user bubble regardless of where the app-server placed it in the turn.
584
636
  builtTurn.items = builtTurn.items
637
+ .map(normalizeDesktopItemCompatibility)
585
638
  .map(sanitizeUserMessageItem)
586
639
  .filter(Boolean);
587
640
  // Hydrated turns from thread/read carry the prompt as an item with empty
@@ -731,6 +784,34 @@ function sanitizeUserMessageItem(item) {
731
784
  };
732
785
  }
733
786
 
787
+ // Codex CLI 0.144.1 can omit receiverThreads from persisted collab tool calls,
788
+ // while the matching Desktop renderer reads that collection without a fallback.
789
+ // Keep the richer snapshots unchanged and synthesize lightweight references from
790
+ // receiverThreadIds for older/CLI-owned rollouts so opening them cannot crash.
791
+ function normalizeDesktopItemCompatibility(item) {
792
+ if (!item || typeof item !== "object" || normalizeToken(item.type) !== "collabagenttoolcall") {
793
+ return item;
794
+ }
795
+
796
+ const receiverThreads = Array.isArray(item.receiverThreads)
797
+ ? item.receiverThreads
798
+ : [];
799
+ const receiverThreadIds = Array.isArray(item.receiverThreadIds)
800
+ ? item.receiverThreadIds.map(readString).filter(Boolean)
801
+ : receiverThreads.map((entry) => readString(entry?.threadId)).filter(Boolean);
802
+ if (Array.isArray(item.receiverThreads) && Array.isArray(item.receiverThreadIds)) {
803
+ return item;
804
+ }
805
+
806
+ return {
807
+ ...item,
808
+ receiverThreadIds,
809
+ receiverThreads: Array.isArray(item.receiverThreads)
810
+ ? receiverThreads
811
+ : receiverThreadIds.map((threadId) => ({ threadId })),
812
+ };
813
+ }
814
+
734
815
  function isInitialPromptUserMessageItem(turn, item) {
735
816
  if (!isUserMessageItem(item)) {
736
817
  return false;
@@ -908,8 +989,10 @@ function upsertItem(turn, item) {
908
989
  // user items too; no Codex UI renders it, so it must not reach the stream.
909
990
  // Also evict any copy that slipped into the state before this filter existed.
910
991
  const index = turn.items.findIndex((candidate) => readString(candidate?.id) === itemId);
911
- const sanitizedItem = sanitizeUserMessageItem(item);
912
- const existingItem = index >= 0 ? sanitizeUserMessageItem(turn.items[index]) : null;
992
+ const sanitizedItem = sanitizeUserMessageItem(normalizeDesktopItemCompatibility(item));
993
+ const existingItem = index >= 0
994
+ ? sanitizeUserMessageItem(normalizeDesktopItemCompatibility(turn.items[index]))
995
+ : null;
913
996
  if (!sanitizedItem) {
914
997
  if (index >= 0) {
915
998
  turn.items.splice(index, 1);
@@ -1099,10 +1182,62 @@ function readTurnIdFromTurn(turn) {
1099
1182
  || readString(turn?.turn_id);
1100
1183
  }
1101
1184
 
1185
+ function automaticApprovalReviewItemFromParams(params) {
1186
+ const reviewId = readString(params?.reviewId);
1187
+ const review = params?.review && typeof params.review === "object" ? params.review : null;
1188
+ const status = readString(review?.status);
1189
+ if (!reviewId || !status || !params?.action) {
1190
+ return null;
1191
+ }
1192
+ return {
1193
+ id: `automatic-approval-review:${reviewId}`,
1194
+ type: "automaticApprovalReview",
1195
+ reviewId,
1196
+ targetItemId: readString(params?.targetItemId) || null,
1197
+ status,
1198
+ startedAtMs: params?.startedAtMs ?? null,
1199
+ completedAtMs: params?.completedAtMs ?? null,
1200
+ decisionSource: readString(params?.decisionSource) || null,
1201
+ review: cloneJSON(review),
1202
+ action: cloneJSON(params.action),
1203
+ remodexGuardianRetrySupported: false,
1204
+ };
1205
+ }
1206
+
1102
1207
  function timestampSecondsToMs(value) {
1103
1208
  return Number.isFinite(value) && value > 0 ? Math.round(value * 1000) : 0;
1104
1209
  }
1105
1210
 
1211
+ function normalizeThreadGoal(value, fallbackThreadId = "") {
1212
+ if (!value || typeof value !== "object") {
1213
+ return null;
1214
+ }
1215
+ const threadId = readString(value.threadId) || readString(value.thread_id) || readString(fallbackThreadId);
1216
+ const objective = readString(value.objective);
1217
+ const statusByToken = {
1218
+ active: "active",
1219
+ paused: "paused",
1220
+ blocked: "blocked",
1221
+ usagelimited: "usageLimited",
1222
+ budgetlimited: "budgetLimited",
1223
+ complete: "complete",
1224
+ };
1225
+ const status = statusByToken[normalizeToken(value.status)] || "";
1226
+ if (!threadId || !objective || !status) {
1227
+ return null;
1228
+ }
1229
+ return {
1230
+ threadId,
1231
+ objective,
1232
+ status,
1233
+ tokenBudget: value.tokenBudget ?? value.token_budget ?? null,
1234
+ tokensUsed: Number(value.tokensUsed ?? value.tokens_used) || 0,
1235
+ timeUsedSeconds: Number(value.timeUsedSeconds ?? value.time_used_seconds) || 0,
1236
+ createdAt: Number(value.createdAt ?? value.created_at) || 0,
1237
+ updatedAt: Number(value.updatedAt ?? value.updated_at) || 0,
1238
+ };
1239
+ }
1240
+
1106
1241
  const REQUEST_METHODS_WITH_THREAD = new Set([
1107
1242
  "item/commandExecution/requestApproval",
1108
1243
  "item/fileChange/requestApproval",
@@ -1123,6 +1258,7 @@ module.exports = {
1123
1258
  createEmptyConversationState,
1124
1259
  ensureConversationInMap,
1125
1260
  mergeConversationTurnsFromThread,
1261
+ normalizeThreadGoal,
1126
1262
  readThreadIdFromParams,
1127
1263
  readTurnIdFromParams,
1128
1264
  readTurnIdFromTurn,
@@ -172,6 +172,10 @@ function projectDesktopConversationStateToThread(threadId, rawState, { now = ()
172
172
  return projectConversationState(threadId, rawState, { now }).thread;
173
173
  }
174
174
 
175
+ function projectDesktopConversationStateToGoal(threadId, rawState) {
176
+ return latestThreadGoal(rawState, threadId);
177
+ }
178
+
175
179
  function projectConversationState(threadId, rawState, {
176
180
  now = () => Date.now(),
177
181
  turnCache = null,
@@ -215,9 +219,12 @@ function projectConversationState(threadId, rawState, {
215
219
  turns,
216
220
  };
217
221
 
222
+ const goal = latestThreadGoal(rawState, threadId);
223
+
218
224
  return {
219
225
  thread,
220
226
  turns,
227
+ goal,
221
228
  activeTurnId,
222
229
  status: thread.status,
223
230
  };
@@ -332,6 +339,9 @@ function bootstrapNotifications(
332
339
  const notifications = includeThreadStarted && shouldEmitThreadStarted(projection.thread)
333
340
  ? [threadStartedNotification(projection.thread)]
334
341
  : [];
342
+ if (projection.goal) {
343
+ notifications.push(threadGoalUpdatedNotification(threadId, projection.goal));
344
+ }
335
345
  const activeTurns = includeAllActiveTurns
336
346
  ? projection.turns.filter((turn) => isActiveTurnStatus(turn.status))
337
347
  : [projection.activeTurnId
@@ -367,12 +377,26 @@ function diffProjections(threadId, previousProjection, nextProjection) {
367
377
  const notifications = [];
368
378
 
369
379
  notifications.push(...diffThreadMetadata(previousProjection.thread, nextProjection.thread));
380
+ notifications.push(...diffThreadGoal(threadId, previousProjection.goal, nextProjection.goal));
370
381
  notifications.push(...diffTurnLifecycle(threadId, previousProjection, nextProjection));
371
382
  notifications.push(...diffTurnItems(threadId, previousProjection, nextProjection));
372
383
 
373
384
  return notifications;
374
385
  }
375
386
 
387
+ function diffThreadGoal(threadId, previousGoal, nextGoal) {
388
+ if (JSON.stringify(previousGoal || null) === JSON.stringify(nextGoal || null)) {
389
+ return [];
390
+ }
391
+ if (nextGoal) {
392
+ return [threadGoalUpdatedNotification(threadId, nextGoal)];
393
+ }
394
+ return [tagNotification({
395
+ method: "thread/goal/cleared",
396
+ params: { threadId },
397
+ })];
398
+ }
399
+
376
400
  function diffThreadMetadata(previousThread, nextThread) {
377
401
  const notifications = [];
378
402
  const previousRuntimeRevision = Number(previousThread.runtimeSettingsRevision) || 0;
@@ -486,7 +510,11 @@ function diffTurnItems(threadId, previousProjection, nextProjection) {
486
510
  continue;
487
511
  }
488
512
  if (!isActiveTurn) {
489
- notifications.push(itemCompletedNotification(threadId, nextTurn.id, nextItem));
513
+ if (isAutoApprovalReviewItem(nextItem) && !isTerminalItemState(nextItem)) {
514
+ notifications.push(itemStartedNotification(threadId, nextTurn.id, nextItem));
515
+ } else {
516
+ notifications.push(itemCompletedNotification(threadId, nextTurn.id, nextItem));
517
+ }
490
518
  continue;
491
519
  }
492
520
  notifications.push(...diffItem(threadId, nextTurn.id, previousItem, nextItem));
@@ -502,6 +530,11 @@ function diffItem(threadId, turnId, previousItem, nextItem) {
502
530
  // Previous text lengths come straight from the previous projection, which is
503
531
  // exactly what the per-thread snapshot map used to store.
504
532
  const snapshot = snapshotItem(previousItem);
533
+ if (isAutoApprovalReviewItem(nextItem)) {
534
+ return [isTerminalItemState(nextItem)
535
+ ? itemCompletedNotification(threadId, turnId, nextItem)
536
+ : itemStartedNotification(threadId, turnId, nextItem)];
537
+ }
505
538
  if (isAssistantMessageItem(nextItem)) {
506
539
  const previousText = assistantMessageText(previousItem);
507
540
  const nextText = assistantMessageText(nextItem);
@@ -644,6 +677,17 @@ function turnStartedNotification(threadId, turn) {
644
677
  });
645
678
  }
646
679
 
680
+ function threadGoalUpdatedNotification(threadId, goal) {
681
+ return tagNotification({
682
+ method: "thread/goal/updated",
683
+ params: {
684
+ threadId,
685
+ turnId: null,
686
+ goal: cloneJSON(goal),
687
+ },
688
+ });
689
+ }
690
+
647
691
  function turnCompletedNotification(threadId, turn) {
648
692
  return tagNotification({
649
693
  method: "turn/completed",
@@ -658,6 +702,9 @@ function turnCompletedNotification(threadId, turn) {
658
702
  }
659
703
 
660
704
  function itemStartedNotification(threadId, turnId, item) {
705
+ if (isAutoApprovalReviewItem(item)) {
706
+ return autoApprovalReviewNotification("item/autoApprovalReview/started", threadId, turnId, item);
707
+ }
661
708
  return tagNotification({
662
709
  method: "item/started",
663
710
  params: {
@@ -670,6 +717,9 @@ function itemStartedNotification(threadId, turnId, item) {
670
717
  }
671
718
 
672
719
  function itemCompletedNotification(threadId, turnId, item) {
720
+ if (isAutoApprovalReviewItem(item)) {
721
+ return autoApprovalReviewNotification("item/autoApprovalReview/completed", threadId, turnId, item);
722
+ }
673
723
  return tagNotification({
674
724
  method: "item/completed",
675
725
  params: {
@@ -681,6 +731,21 @@ function itemCompletedNotification(threadId, turnId, item) {
681
731
  });
682
732
  }
683
733
 
734
+ // Guardian reviews have no ThreadItem variant in the app-server protocol; the
735
+ // live wire shape is the dedicated `item/autoApprovalReview/*` notification.
736
+ // Re-emit that shape so mobile reuses one decoder for owned and mirrored threads.
737
+ function autoApprovalReviewNotification(method, threadId, turnId, item) {
738
+ const { type, id, status, ...payload } = item;
739
+ return tagNotification({
740
+ method,
741
+ params: {
742
+ threadId,
743
+ turnId,
744
+ ...cloneJSON(payload),
745
+ },
746
+ });
747
+ }
748
+
684
749
  function deltaNotification(method, threadId, turnId, itemId, delta, extraParams = {}) {
685
750
  return tagNotification({
686
751
  method,
@@ -994,6 +1059,9 @@ function sanitizeUserInputEntries(entries) {
994
1059
  // Keep the original type as metadata, but emit the generic shape iOS already decodes.
995
1060
  // Returns null when the item has nothing user-visible left after sanitizing.
996
1061
  function projectItemForMobile(item, itemType = normalizeToken(item?.type)) {
1062
+ if (itemType === "automaticapprovalreview") {
1063
+ return projectAutoApprovalReviewItem(item);
1064
+ }
997
1065
  if (itemType === "usermessage") {
998
1066
  const visibleContent = sanitizeUserInputEntries(
999
1067
  Array.isArray(item?.content) ? item.content : []
@@ -1024,8 +1092,53 @@ function projectItemForMobile(item, itemType = normalizeToken(item?.type)) {
1024
1092
  return projected;
1025
1093
  }
1026
1094
 
1095
+ const AUTO_APPROVAL_REVIEW_ITEM_ID_PREFIX = "automatic-approval-review:";
1096
+
1097
+ function isAutoApprovalReviewItem(item) {
1098
+ return normalizeToken(item?.type) === "automaticapprovalreview";
1099
+ }
1100
+
1101
+ // Desktop flattens `item/autoApprovalReview/*` notifications into synthetic
1102
+ // `automaticApprovalReview` turn items. Normalize back to the app-server
1103
+ // notification shape; keep top-level `status` for lifecycle checks.
1104
+ function projectAutoApprovalReviewItem(item) {
1105
+ const rawId = itemIdOf(item);
1106
+ if (!rawId) {
1107
+ return null;
1108
+ }
1109
+ const review = item?.review && typeof item.review === "object" ? item.review : item;
1110
+ const status = readString(review.status);
1111
+ if (!status) {
1112
+ return null;
1113
+ }
1114
+ return {
1115
+ type: "automaticApprovalReview",
1116
+ id: rawId,
1117
+ reviewId: readString(item.reviewId)
1118
+ || (rawId.startsWith(AUTO_APPROVAL_REVIEW_ITEM_ID_PREFIX)
1119
+ ? rawId.slice(AUTO_APPROVAL_REVIEW_ITEM_ID_PREFIX.length)
1120
+ : rawId),
1121
+ targetItemId: readString(item.targetItemId) || null,
1122
+ status,
1123
+ startedAtMs: item.startedAtMs ?? null,
1124
+ completedAtMs: item.completedAtMs ?? null,
1125
+ decisionSource: readString(item.decisionSource)
1126
+ || readString(item?.event?.decision_source)
1127
+ || null,
1128
+ review: {
1129
+ status,
1130
+ riskLevel: readString(review.riskLevel) || null,
1131
+ userAuthorization: readString(review.userAuthorization) || null,
1132
+ rationale: readString(review.rationale) || null,
1133
+ },
1134
+ action: cloneJSON(item.action ?? null),
1135
+ ...MIRROR_TAG,
1136
+ };
1137
+ }
1138
+
1027
1139
  function isSupportedItemType(type) {
1028
1140
  return type === "usermessage"
1141
+ || type === "automaticapprovalreview"
1029
1142
  || type === "hookprompt"
1030
1143
  || type === "agentmessage"
1031
1144
  || type === "assistantmessage"
@@ -1161,9 +1274,42 @@ function normalizeTimestamp(value) {
1161
1274
  return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
1162
1275
  }
1163
1276
 
1277
+ function latestThreadGoal(rawState, threadId) {
1278
+ const candidates = [rawState?.threadGoal, rawState?.completedThreadGoal]
1279
+ .map((goal) => normalizeProjectedThreadGoal(goal, threadId))
1280
+ .filter(Boolean);
1281
+ return candidates.sort((left, right) => right.updatedAt - left.updatedAt)[0] || null;
1282
+ }
1283
+
1284
+ function normalizeProjectedThreadGoal(value, fallbackThreadId) {
1285
+ if (!value || typeof value !== "object") {
1286
+ return null;
1287
+ }
1288
+ const statusByToken = {
1289
+ active: "active",
1290
+ paused: "paused",
1291
+ blocked: "blocked",
1292
+ usagelimited: "usageLimited",
1293
+ budgetlimited: "budgetLimited",
1294
+ complete: "complete",
1295
+ };
1296
+ const goal = {
1297
+ threadId: readString(value.threadId) || readString(value.thread_id) || fallbackThreadId,
1298
+ objective: readString(value.objective),
1299
+ status: statusByToken[normalizeToken(value.status)] || "",
1300
+ tokenBudget: value.tokenBudget ?? value.token_budget ?? null,
1301
+ tokensUsed: Number(value.tokensUsed ?? value.tokens_used) || 0,
1302
+ timeUsedSeconds: Number(value.timeUsedSeconds ?? value.time_used_seconds) || 0,
1303
+ createdAt: Number(value.createdAt ?? value.created_at) || 0,
1304
+ updatedAt: Number(value.updatedAt ?? value.updated_at) || 0,
1305
+ };
1306
+ return goal.threadId && goal.objective && goal.status ? goal : null;
1307
+ }
1308
+
1164
1309
  module.exports = {
1165
1310
  createDesktopConversationProjector,
1166
1311
  desktopTurnsShareLogicalIdentity,
1167
1312
  matchDesktopTurnIdentityContinuities,
1313
+ projectDesktopConversationStateToGoal,
1168
1314
  projectDesktopConversationStateToThread,
1169
1315
  };
@@ -63,8 +63,10 @@ const LEGACY_CONTEXT_WARNING_PREFIXES = [
63
63
  const LEGACY_APPLY_PATCH_WARNING_PREFIX = "Warning: apply_patch was requested via ";
64
64
  const LEGACY_APPLY_PATCH_WARNING_SUFFIX = "Use the apply_patch tool instead of exec_command.";
65
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>$/;
66
+ const AGENTS_INSTRUCTIONS_BEGIN = "<instructions>";
67
+ const AGENTS_INSTRUCTIONS_END = "</instructions>";
68
+ 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>/;
69
+ const EXTERNAL_CONTEXT_PREFIX_PATTERN = /^<external_([a-z0-9_-]+)>[\s\S]*?<\/external_\1>/;
68
70
  const PROMPT_REQUEST_BEGIN = "## My request for Codex:";
69
71
  const REVIEW_PROMPT_PREFIX = "## Code review guidelines:";
70
72
 
@@ -91,38 +93,116 @@ function stripImagePlaceholders(text) {
91
93
  return IMAGE_PLACEHOLDER_TOKEN.test(withoutPairs.trim()) ? "" : withoutPairs;
92
94
  }
93
95
 
94
- function isContextualUserText(text) {
95
- const raw = typeof text === "string" ? text : "";
96
- const trimmed = stripImagePlaceholders(raw).trim();
97
- if (!trimmed) {
98
- return false;
96
+ // Review envelopes contain a real request after the delimiter and are never
97
+ // wholly contextual, even when that request itself contains reserved markup.
98
+ function isReviewEnvelopeText(trimmed) {
99
+ return trimmed.startsWith(REVIEW_PROMPT_PREFIX) && trimmed.includes(PROMPT_REQUEST_BEGIN);
100
+ }
101
+
102
+ // Consumes one runtime-owned fragment anchored at the start of `text` and
103
+ // returns what follows it, or null when the text does not open with one.
104
+ // Codex packs several fragments into a single user item, so the opening and
105
+ // closing markers routinely belong to different fragments (a desktop opener
106
+ // reads "<recommended_plugins>...</recommended_plugins>" + AGENTS.md
107
+ // instructions + "<environment_context>...</environment_context>"). Matching
108
+ // the blob as a whole classifies that item as visible and turns the entire
109
+ // injected preamble into the thread's first user bubble.
110
+ function consumeLeadingContextFragment(text) {
111
+ const lower = text.toLowerCase();
112
+
113
+ for (const [start, end] of CONTEXT_MARKER_PAIRS) {
114
+ if (!lower.startsWith(start)) {
115
+ continue;
116
+ }
117
+ const closeIndex = lower.indexOf(end, start.length);
118
+ // An unterminated marker is not a fragment we can bound. Leave it visible
119
+ // rather than swallow a message that merely opens with reserved markup.
120
+ return closeIndex === -1 ? null : text.slice(closeIndex + end.length);
99
121
  }
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;
122
+
123
+ const internal = INTERNAL_CONTEXT_PREFIX_PATTERN.exec(text);
124
+ if (internal) {
125
+ return text.slice(internal[0].length);
104
126
  }
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;
127
+ const external = EXTERNAL_CONTEXT_PREFIX_PATTERN.exec(text);
128
+ if (external) {
129
+ return text.slice(external[0].length);
117
130
  }
118
- if (INTERNAL_CONTEXT_PATTERN.test(trimmed) || EXTERNAL_CONTEXT_PATTERN.test(trimmed)) {
119
- return true;
131
+
132
+ if (lower.startsWith(AGENTS_INSTRUCTIONS_PREFIX.toLowerCase())) {
133
+ return consumeAgentsInstructionsFragment(text, lower);
120
134
  }
121
- if (LEGACY_CONTEXT_WARNING_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) {
122
- return true;
135
+
136
+ // Unlike the marked fragments above, runtime warnings carry no closing marker
137
+ // and trail free-form runtime lines ("Shell cwd was reset to ..."), so there is
138
+ // no boundary to peel at. They are emitted as whole items that never contain a
139
+ // user request, hence consuming the remainder rather than bounding it.
140
+ if (LEGACY_CONTEXT_WARNING_PREFIXES.some((prefix) => text.startsWith(prefix))) {
141
+ return "";
142
+ }
143
+ return text.startsWith(LEGACY_APPLY_PATCH_WARNING_PREFIX)
144
+ && text.endsWith(LEGACY_APPLY_PATCH_WARNING_SUFFIX)
145
+ ? ""
146
+ : null;
147
+ }
148
+
149
+ function consumeAgentsInstructionsFragment(text, lower) {
150
+ const closeIndex = lower.indexOf(AGENTS_INSTRUCTIONS_END);
151
+ if (closeIndex >= 0) {
152
+ return consumeChainedInstructionsBlocks(text.slice(closeIndex + AGENTS_INSTRUCTIONS_END.length));
153
+ }
154
+ // Older runtimes emit the AGENTS.md body unwrapped and then append another
155
+ // registered fragment; that next marker is the only reliable boundary.
156
+ const nextMarkerIndex = CONTEXT_MARKER_PAIRS
157
+ .map(([start]) => lower.indexOf(start, 1))
158
+ .filter((index) => index > 0)
159
+ .sort((left, right) => left - right)[0];
160
+ return nextMarkerIndex === undefined ? null : text.slice(nextMarkerIndex);
161
+ }
162
+
163
+ // A single "# AGENTS.md instructions" header can carry one <INSTRUCTIONS> block
164
+ // per nested AGENTS.md file. Stopping at the first close would leave the rest of
165
+ // the preamble in front of the real request, where no registered marker matches
166
+ // and the peeler gives up, turning the leftover into the first user bubble.
167
+ function consumeChainedInstructionsBlocks(text) {
168
+ let rest = text;
169
+ for (;;) {
170
+ const trimmed = rest.trimStart();
171
+ const lower = trimmed.toLowerCase();
172
+ if (!lower.startsWith(AGENTS_INSTRUCTIONS_BEGIN)) {
173
+ return rest;
174
+ }
175
+ const closeIndex = lower.indexOf(AGENTS_INSTRUCTIONS_END, AGENTS_INSTRUCTIONS_BEGIN.length);
176
+ // Unterminated: same rule as everywhere else, leave it visible rather than
177
+ // guess where the injected block ends.
178
+ if (closeIndex === -1) {
179
+ return rest;
180
+ }
181
+ rest = trimmed.slice(closeIndex + AGENTS_INSTRUCTIONS_END.length);
182
+ }
183
+ }
184
+
185
+ // Peels every injected fragment off the front of an already trimmed message and
186
+ // returns the text the user actually typed (empty when nothing else remains).
187
+ function stripLeadingContextFragments(trimmed) {
188
+ let rest = trimmed;
189
+ while (rest) {
190
+ const remainder = consumeLeadingContextFragment(rest);
191
+ if (remainder === null) {
192
+ break;
193
+ }
194
+ rest = remainder.trim();
195
+ }
196
+ return rest;
197
+ }
198
+
199
+ function isContextualUserText(text) {
200
+ const raw = typeof text === "string" ? text : "";
201
+ const trimmed = stripImagePlaceholders(raw).trim();
202
+ if (!trimmed || isReviewEnvelopeText(trimmed)) {
203
+ return false;
123
204
  }
124
- return trimmed.startsWith(LEGACY_APPLY_PATCH_WARNING_PREFIX)
125
- && trimmed.endsWith(LEGACY_APPLY_PATCH_WARNING_SUFFIX);
205
+ return stripLeadingContextFragments(trimmed) === "";
126
206
  }
127
207
 
128
208
  function decodeXmlText(text) {
@@ -173,14 +253,25 @@ function visibleUserPromptText(text) {
173
253
  return "";
174
254
  }
175
255
  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)) {
256
+ const trimmed = cleaned.trim();
257
+ if (!trimmed) {
179
258
  return "";
180
259
  }
181
- const requestIndex = cleaned.lastIndexOf(PROMPT_REQUEST_BEGIN);
260
+ // Context bodies can contain the request delimiter as ordinary text. Peel the
261
+ // injected fragments off first so the delimiter cannot reveal hidden content,
262
+ // and so a real request that trails them survives instead of the whole blob.
263
+ const stripped = isReviewEnvelopeText(trimmed)
264
+ ? trimmed
265
+ : stripLeadingContextFragments(trimmed);
266
+ if (!stripped) {
267
+ return "";
268
+ }
269
+ // Untouched prompts keep their original spacing so callers can still detect
270
+ // "nothing changed" by identity and skip cloning the item.
271
+ const body = stripped === trimmed ? cleaned : stripped;
272
+ const requestIndex = body.lastIndexOf(PROMPT_REQUEST_BEGIN);
182
273
  if (requestIndex >= 0) {
183
- const request = cleaned.slice(requestIndex + PROMPT_REQUEST_BEGIN.length).trim();
274
+ const request = body.slice(requestIndex + PROMPT_REQUEST_BEGIN.length).trim();
184
275
  // A few IDE/review exports end with the delimiter but omit its request
185
276
  // suffix. They still contain a real visible prompt before that marker;
186
277
  // returning an empty string made live mirroring erase the opener while
@@ -191,14 +282,14 @@ function visibleUserPromptText(text) {
191
282
  if (request) {
192
283
  return request;
193
284
  }
194
- const body = cleaned.slice(0, requestIndex).trimEnd();
195
- return isContextualUserText(body) ? "" : body;
285
+ const precedingBody = body.slice(0, requestIndex).trimEnd();
286
+ return isContextualUserText(precedingBody) ? "" : precedingBody;
196
287
  }
197
- const envelopeText = extractVisibleRuntimeEnvelope(cleaned);
288
+ const envelopeText = extractVisibleRuntimeEnvelope(body);
198
289
  if (envelopeText != null) {
199
290
  return envelopeText;
200
291
  }
201
- return cleaned;
292
+ return body;
202
293
  }
203
294
 
204
295
  // Sanitizes text fragments independently so a hidden fragment cannot cause a
@@ -327,6 +418,24 @@ function normalizeToken(value) {
327
418
  : "";
328
419
  }
329
420
 
421
+ // The phone's running-state probe, as opposed to a history page: the explicit
422
+ // marker on current clients, or the probe's unique legacy shape (Remodex iPhone
423
+ // 2.1 predates the marker, and real history pages use limits 1 and 5). Every
424
+ // live source answers this request, so they must all recognize it identically.
425
+ function isThreadTurnStateProbeRequest(message) {
426
+ const params = message?.params;
427
+ if (readString(message?.method) !== "thread/turns/list"
428
+ || readString(params?.cursor)
429
+ || params?.remodexRequireCanonical === true) {
430
+ return false;
431
+ }
432
+ if (params?.remodexTurnStateOnly === true) {
433
+ return true;
434
+ }
435
+ return Number(params?.limit) === 8
436
+ && normalizeToken(readString(params?.sortDirection) || "desc") === "desc";
437
+ }
438
+
330
439
  function cloneJSON(value) {
331
440
  if (value == null) {
332
441
  return value;
@@ -456,6 +565,7 @@ module.exports = {
456
565
  hasVisiblePlanUpdate,
457
566
  isContextualUserText,
458
567
  isPlainJSONObject,
568
+ isThreadTurnStateProbeRequest,
459
569
  isUserRoleItem,
460
570
  normalizeToken,
461
571
  readString,