@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.
- package/package.json +1 -1
- package/src/bridge.js +34 -0
- package/src/desktop-ipc-action-follower.js +310 -87
- package/src/desktop-ipc-conversation-adapter.js +138 -2
- package/src/desktop-ipc-conversation-projector.js +147 -1
- package/src/desktop-ipc-shared.js +148 -38
- package/src/push-notification-tracker.js +134 -0
- package/src/rollout-live-mirror.js +170 -11
- package/src/session-jsonl-history.js +5 -1
|
@@ -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
|
|
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
|
-
|
|
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
|
|
67
|
-
const
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
return
|
|
122
|
+
|
|
123
|
+
const internal = INTERNAL_CONTEXT_PREFIX_PATTERN.exec(text);
|
|
124
|
+
if (internal) {
|
|
125
|
+
return text.slice(internal[0].length);
|
|
104
126
|
}
|
|
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;
|
|
127
|
+
const external = EXTERNAL_CONTEXT_PREFIX_PATTERN.exec(text);
|
|
128
|
+
if (external) {
|
|
129
|
+
return text.slice(external[0].length);
|
|
117
130
|
}
|
|
118
|
-
|
|
119
|
-
|
|
131
|
+
|
|
132
|
+
if (lower.startsWith(AGENTS_INSTRUCTIONS_PREFIX.toLowerCase())) {
|
|
133
|
+
return consumeAgentsInstructionsFragment(text, lower);
|
|
120
134
|
}
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
if (isContextualUserText(cleaned)) {
|
|
256
|
+
const trimmed = cleaned.trim();
|
|
257
|
+
if (!trimmed) {
|
|
179
258
|
return "";
|
|
180
259
|
}
|
|
181
|
-
|
|
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 =
|
|
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
|
|
195
|
-
return isContextualUserText(
|
|
285
|
+
const precedingBody = body.slice(0, requestIndex).trimEnd();
|
|
286
|
+
return isContextualUserText(precedingBody) ? "" : precedingBody;
|
|
196
287
|
}
|
|
197
|
-
const envelopeText = extractVisibleRuntimeEnvelope(
|
|
288
|
+
const envelopeText = extractVisibleRuntimeEnvelope(body);
|
|
198
289
|
if (envelopeText != null) {
|
|
199
290
|
return envelopeText;
|
|
200
291
|
}
|
|
201
|
-
return
|
|
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,
|