@makerbi/remodex 2.0.0 → 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.
@@ -4,6 +4,19 @@
4
4
 
5
5
  const fs = require("fs");
6
6
  const { buildApplyPatchFileChangeItem } = require("./apply-patch-changes");
7
+ const { terminalEventClosesTrackedTurn } = require("./rollout-turn-semantics");
8
+ const {
9
+ buildRemodexSourceItemKey,
10
+ isContextualUserText,
11
+ isUserRoleItem,
12
+ responseItemMessageText: sharedResponseItemMessageText,
13
+ visibleUserPromptText,
14
+ } = require("./desktop-ipc-shared");
15
+
16
+ const JSONL_OLDER_HANDOFF_CURSOR = "remodex-jsonl-fallback-older-unavailable";
17
+ const DEFAULT_SESSION_JSONL_METADATA_HEAD_BYTES = 256 * 1024;
18
+ const DEFAULT_SESSION_JSONL_INITIAL_TAIL_BYTES = 4 * 1024 * 1024;
19
+ const DEFAULT_SESSION_JSONL_MAX_TAIL_BYTES = 64 * 1024 * 1024;
7
20
 
8
21
  function readThreadTurnsListPageFromSessionJsonl(filePath, {
9
22
  threadId = "",
@@ -11,13 +24,27 @@ function readThreadTurnsListPageFromSessionJsonl(filePath, {
11
24
  maxLimit = 5,
12
25
  cursor = null,
13
26
  fsModule = fs,
27
+ metadataHeadBytes = DEFAULT_SESSION_JSONL_METADATA_HEAD_BYTES,
28
+ initialTailBytes = DEFAULT_SESSION_JSONL_INITIAL_TAIL_BYTES,
29
+ maxTailBytes = DEFAULT_SESSION_JSONL_MAX_TAIL_BYTES,
14
30
  } = {}) {
15
31
  if (!filePath || cursor != null) {
16
32
  return null;
17
33
  }
18
34
 
19
- const content = fsModule.readFileSync(filePath, "utf8");
20
- const turns = parseSessionJsonlTurns(content, { threadId });
35
+ const recent = readRecentSessionJsonlTurns(filePath, {
36
+ threadId,
37
+ limit: Math.min(
38
+ Number.isInteger(limit) && limit > 0 ? limit : 5,
39
+ Number.isInteger(maxLimit) && maxLimit > 0 ? maxLimit : 5,
40
+ 5
41
+ ),
42
+ fsModule,
43
+ metadataHeadBytes,
44
+ initialTailBytes,
45
+ maxTailBytes,
46
+ });
47
+ const turns = recent?.turns || [];
21
48
  if (turns.length === 0) {
22
49
  return null;
23
50
  }
@@ -28,11 +55,272 @@ function readThreadTurnsListPageFromSessionJsonl(filePath, {
28
55
  const pageTurns = turns.slice(-safeLimit).reverse();
29
56
  return {
30
57
  data: pageTurns,
31
- nextCursor: turns.length > pageTurns.length ? "remodex-jsonl-fallback-older-unavailable" : null,
58
+ nextCursor: recent.hasOlderTurns || turns.length > pageTurns.length
59
+ ? JSONL_OLDER_HANDOFF_CURSOR
60
+ : null,
32
61
  remodexJsonlFallback: true,
33
62
  };
34
63
  }
35
64
 
65
+ // Reads only a bounded snapshot of an append-only rollout. Truncated tails are
66
+ // accepted only when the latest turn's own start and visible user prompt are in
67
+ // the window, so a fast page can never invent ownership for cut-off items.
68
+ function readRecentSessionJsonlTurns(filePath, {
69
+ threadId = "",
70
+ limit = 5,
71
+ fsModule = fs,
72
+ metadataHeadBytes = DEFAULT_SESSION_JSONL_METADATA_HEAD_BYTES,
73
+ initialTailBytes = DEFAULT_SESSION_JSONL_INITIAL_TAIL_BYTES,
74
+ maxTailBytes = DEFAULT_SESSION_JSONL_MAX_TAIL_BYTES,
75
+ } = {}) {
76
+ if (!filePath) {
77
+ return null;
78
+ }
79
+
80
+ // Keep simple in-memory test doubles compatible without weakening the real
81
+ // filesystem path, which must never stringify a multi-gigabyte rollout.
82
+ if (!supportsBoundedSessionJsonlReads(fsModule)) {
83
+ const content = fsModule.readFileSync(filePath, "utf8");
84
+ const turns = parseSessionJsonlTurns(content, { threadId });
85
+ return turns.length > 0 ? { turns, hasOlderTurns: false, bytesRead: Buffer.byteLength(content, "utf8") } : null;
86
+ }
87
+
88
+ const stat = fsModule.statSync(filePath);
89
+ const snapshotSize = Math.max(0, Number(stat?.size) || 0);
90
+ if (snapshotSize === 0) {
91
+ return null;
92
+ }
93
+
94
+ const safeLimit = Math.max(1, Math.min(Number.isInteger(limit) ? limit : 5, 5));
95
+ const safeMetadataHeadBytes = Math.max(1, Math.min(metadataHeadBytes, snapshotSize));
96
+ const safeMaximumTailBytes = Math.max(1, Math.min(maxTailBytes, snapshotSize));
97
+ let tailBytes = Math.max(1, Math.min(initialTailBytes, safeMaximumTailBytes));
98
+ const fileHandle = fsModule.openSync(filePath, "r");
99
+
100
+ try {
101
+ const metadataBuffer = readSessionJsonlRange(
102
+ fileHandle,
103
+ 0,
104
+ safeMetadataHeadBytes,
105
+ fsModule
106
+ );
107
+ const initialMetadata = parseSessionJsonlInitialMetadata(metadataBuffer.toString("utf8"));
108
+
109
+ while (true) {
110
+ const rawStart = Math.max(0, snapshotSize - tailBytes);
111
+ const tailBuffer = readSessionJsonlRange(
112
+ fileHandle,
113
+ rawStart,
114
+ snapshotSize - rawStart,
115
+ fsModule
116
+ );
117
+ const aligned = alignSessionJsonlTailBuffer(tailBuffer, rawStart);
118
+ if (aligned) {
119
+ const content = aligned.buffer.toString("utf8");
120
+ const sourceLineByteOffsets = sessionJsonlLineByteOffsets(
121
+ aligned.buffer,
122
+ aligned.sourceByteOffset
123
+ );
124
+ const turns = parseSessionJsonlTurns(content, {
125
+ threadId,
126
+ initialMetadata,
127
+ sourceByteOffset: aligned.sourceByteOffset,
128
+ sourceLineByteOffsets,
129
+ });
130
+ const observedStartedTurnIDs = observedTaskStartedTurnIDs(content, {
131
+ sourceByteOffset: aligned.sourceByteOffset,
132
+ sourceLineByteOffsets,
133
+ });
134
+ const safeTurns = rawStart === 0
135
+ ? turns
136
+ : turns.filter((turn) => (
137
+ observedStartedTurnIDs.has(normalizeString(turn?.id))
138
+ && turnHasVisibleUserItem(turn)
139
+ ));
140
+
141
+ if (safeTurns.length >= safeLimit || tailBytes >= safeMaximumTailBytes || rawStart === 0) {
142
+ if (safeTurns.length === 0) {
143
+ return null;
144
+ }
145
+ return {
146
+ turns: safeTurns,
147
+ hasOlderTurns: rawStart > 0 || turns.length > safeTurns.length,
148
+ bytesRead: metadataBuffer.length + tailBuffer.length,
149
+ };
150
+ }
151
+ }
152
+
153
+ if (tailBytes >= safeMaximumTailBytes || rawStart === 0) {
154
+ return null;
155
+ }
156
+ tailBytes = Math.min(safeMaximumTailBytes, tailBytes * 2);
157
+ }
158
+ } finally {
159
+ fsModule.closeSync(fileHandle);
160
+ }
161
+ }
162
+
163
+ function readSessionJsonlMetadataFromFile(filePath, {
164
+ fsModule = fs,
165
+ metadataHeadBytes = DEFAULT_SESSION_JSONL_METADATA_HEAD_BYTES,
166
+ } = {}) {
167
+ if (!filePath) {
168
+ return { threadId: "", cwd: "" };
169
+ }
170
+ if (!supportsBoundedSessionJsonlReads(fsModule)) {
171
+ return parseSessionJsonlMetadata(fsModule.readFileSync(filePath, "utf8"));
172
+ }
173
+
174
+ const stat = fsModule.statSync(filePath);
175
+ const snapshotSize = Math.max(0, Number(stat?.size) || 0);
176
+ if (snapshotSize === 0) {
177
+ return { threadId: "", cwd: "" };
178
+ }
179
+ const fileHandle = fsModule.openSync(filePath, "r");
180
+ try {
181
+ const head = readSessionJsonlRange(
182
+ fileHandle,
183
+ 0,
184
+ Math.min(snapshotSize, Math.max(1, metadataHeadBytes)),
185
+ fsModule
186
+ );
187
+ const metadata = parseSessionJsonlInitialMetadata(head.toString("utf8"));
188
+ return { threadId: metadata.threadId, cwd: metadata.cwd };
189
+ } finally {
190
+ fsModule.closeSync(fileHandle);
191
+ }
192
+ }
193
+
194
+ function supportsBoundedSessionJsonlReads(fsModule) {
195
+ return typeof fsModule?.statSync === "function"
196
+ && typeof fsModule?.openSync === "function"
197
+ && typeof fsModule?.readSync === "function"
198
+ && typeof fsModule?.closeSync === "function";
199
+ }
200
+
201
+ function readSessionJsonlRange(fileHandle, start, length, fsModule) {
202
+ const safeLength = Math.max(0, length);
203
+ const buffer = Buffer.allocUnsafe(safeLength);
204
+ const bytesRead = safeLength > 0
205
+ ? fsModule.readSync(fileHandle, buffer, 0, safeLength, start)
206
+ : 0;
207
+ return buffer.subarray(0, bytesRead);
208
+ }
209
+
210
+ function alignSessionJsonlTailBuffer(buffer, rawStart) {
211
+ if (rawStart === 0) {
212
+ return { buffer, sourceByteOffset: 0 };
213
+ }
214
+ const firstLineFeed = buffer.indexOf(0x0a);
215
+ if (firstLineFeed === -1 || firstLineFeed + 1 >= buffer.length) {
216
+ return null;
217
+ }
218
+ return {
219
+ buffer: buffer.subarray(firstLineFeed + 1),
220
+ sourceByteOffset: rawStart + firstLineFeed + 1,
221
+ };
222
+ }
223
+
224
+ function sessionJsonlLineByteOffsets(buffer, sourceByteOffset) {
225
+ const offsets = [sourceByteOffset];
226
+ for (let index = 0; index < buffer.length; index += 1) {
227
+ if (buffer[index] === 0x0a && index + 1 < buffer.length) {
228
+ offsets.push(sourceByteOffset + index + 1);
229
+ }
230
+ }
231
+ return offsets;
232
+ }
233
+
234
+ function observedTaskStartedTurnIDs(content, {
235
+ sourceByteOffset = 0,
236
+ sourceLineByteOffsets = null,
237
+ } = {}) {
238
+ const turnIDs = new Set();
239
+ const raw = String(content || "");
240
+ let lineIndex = -1;
241
+ let lineStart = 0;
242
+ let fallbackSourceByteOffset = sourceByteOffset;
243
+ while (lineStart < raw.length) {
244
+ lineIndex += 1;
245
+ let lineEnd = raw.indexOf("\n", lineStart);
246
+ if (lineEnd === -1) {
247
+ lineEnd = raw.length;
248
+ }
249
+ const currentLineStart = lineStart;
250
+ const lineEndWithSeparator = lineEnd < raw.length ? lineEnd + 1 : lineEnd;
251
+ const sourceLineNumber = sourceLineByteOffsets?.[lineIndex]
252
+ ?? fallbackSourceByteOffset;
253
+ fallbackSourceByteOffset += Buffer.byteLength(
254
+ raw.substring(currentLineStart, lineEndWithSeparator),
255
+ "utf8"
256
+ );
257
+ const line = raw.substring(lineStart, lineEnd).trim();
258
+ lineStart = lineEnd + 1;
259
+ if (!line) {
260
+ continue;
261
+ }
262
+ try {
263
+ const entry = JSON.parse(line);
264
+ const payload = objectValue(entry?.payload);
265
+ if (entry?.type !== "event_msg" || normalizeString(payload?.type) !== "task_started") {
266
+ continue;
267
+ }
268
+ const turnID = normalizeString(payload?.turn_id)
269
+ || normalizeString(payload?.turnId)
270
+ || `turn-line-${sourceLineNumber}`;
271
+ if (turnID) {
272
+ turnIDs.add(turnID);
273
+ }
274
+ } catch {
275
+ // A live rollout may end with a partial line; ignore it until the next read.
276
+ }
277
+ }
278
+ return turnIDs;
279
+ }
280
+
281
+ function turnHasVisibleUserItem(turn) {
282
+ return Array.isArray(turn?.items) && turn.items.some((item) => isUserRoleItem(item));
283
+ }
284
+
285
+ function parseSessionJsonlInitialMetadata(content) {
286
+ let threadId = "";
287
+ let cwd = "";
288
+ let timeZone = "";
289
+ const raw = String(content || "");
290
+ let lineStart = 0;
291
+ while (lineStart < raw.length) {
292
+ let lineEnd = raw.indexOf("\n", lineStart);
293
+ if (lineEnd === -1) {
294
+ lineEnd = raw.length;
295
+ }
296
+ const line = raw.substring(lineStart, lineEnd).trim();
297
+ lineStart = lineEnd + 1;
298
+ if (!line) {
299
+ continue;
300
+ }
301
+ try {
302
+ const entry = JSON.parse(line);
303
+ if (entry?.type !== "session_meta") {
304
+ continue;
305
+ }
306
+ const payload = objectValue(entry.payload);
307
+ threadId = normalizeString(payload?.id)
308
+ || normalizeString(payload?.thread_id)
309
+ || normalizeString(payload?.threadId);
310
+ cwd = normalizeString(payload?.cwd)
311
+ || normalizeString(payload?.current_working_directory)
312
+ || normalizeString(payload?.working_directory);
313
+ timeZone = normalizeString(payload?.timezone)
314
+ || normalizeString(payload?.timeZone)
315
+ || normalizeString(payload?.time_zone);
316
+ break;
317
+ } catch {
318
+ // Metadata is expected at the head; an incomplete oversized line is not trusted.
319
+ }
320
+ }
321
+ return { threadId, cwd, timeZone };
322
+ }
323
+
36
324
  // Extracts thread-level context that app-server history can omit for desktop-origin runs.
37
325
  function parseSessionJsonlMetadata(content) {
38
326
  let threadId = "";
@@ -78,26 +366,42 @@ function parseSessionJsonlMetadata(content) {
78
366
  return { threadId, cwd };
79
367
  }
80
368
 
81
- function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
369
+ function parseSessionJsonlTurns(content, {
370
+ threadId = "",
371
+ initialMetadata = null,
372
+ sourceByteOffset = 0,
373
+ sourceLineByteOffsets = null,
374
+ } = {}) {
82
375
  const turns = [];
83
376
  const turnsById = new Map();
84
377
  let activeTurnId = "";
85
- let sessionThreadId = normalizeString(threadId);
86
- let sessionCwd = "";
87
- let sessionTimeZone = "";
378
+ let pendingSyntheticTerminal = null;
379
+ let sessionThreadId = normalizeString(threadId) || normalizeString(initialMetadata?.threadId);
380
+ let sessionCwd = normalizeString(initialMetadata?.cwd);
381
+ let sessionTimeZone = normalizeString(initialMetadata?.timeZone);
88
382
  const skippedCallIds = new Set();
89
383
  const toolCallsByCallId = new Map();
90
384
  const pendingUserMessages = [];
385
+ const assistantAliasOccurrencesByBaseKey = new Map();
91
386
 
92
387
  const raw = String(content || "");
93
388
  let index = -1;
94
389
  let lineStart = 0;
390
+ let fallbackSourceByteOffset = sourceByteOffset;
95
391
  while (lineStart < raw.length) {
96
392
  index += 1;
97
393
  let lineEnd = raw.indexOf("\n", lineStart);
98
394
  if (lineEnd === -1) {
99
395
  lineEnd = raw.length;
100
396
  }
397
+ const currentLineStart = lineStart;
398
+ const lineEndWithSeparator = lineEnd < raw.length ? lineEnd + 1 : lineEnd;
399
+ const sourceLineNumber = sourceLineByteOffsets?.[index]
400
+ ?? fallbackSourceByteOffset;
401
+ fallbackSourceByteOffset += Buffer.byteLength(
402
+ raw.substring(currentLineStart, lineEndWithSeparator),
403
+ "utf8"
404
+ );
101
405
  const line = raw.substring(lineStart, lineEnd).trim();
102
406
  lineStart = lineEnd + 1;
103
407
  if (!line) {
@@ -144,30 +448,53 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
144
448
  const payload = objectValue(entry.payload);
145
449
  const eventType = normalizeString(payload?.type);
146
450
  if (eventType === "task_started") {
451
+ if (pendingSyntheticTerminal) {
452
+ closeSyntheticHistoryTurn(turnsById, pendingSyntheticTerminal);
453
+ pendingSyntheticTerminal = null;
454
+ activeTurnId = "";
455
+ }
147
456
  activeTurnId = normalizeString(payload?.turn_id)
148
457
  || normalizeString(payload?.turnId)
149
458
  || activeTurnId
150
- || `turn-line-${index + 1}`;
459
+ || `turn-line-${sourceLineNumber}`;
151
460
  const turn = ensureTurn(turns, turnsById, activeTurnId, sessionThreadId, entry.timestamp);
152
461
  applyHistoryTimeZone(turn, sessionTimeZone);
153
462
  flushPendingUserMessagesToTurn(turn, pendingUserMessages);
154
463
  continue;
155
464
  }
156
465
 
157
- if (eventType === "task_complete") {
158
- const turn = ensureTurn(
159
- turns,
160
- turnsById,
161
- normalizeString(payload?.turn_id) || normalizeString(payload?.turnId) || activeTurnId || `turn-line-${index + 1}`,
162
- sessionThreadId,
163
- entry.timestamp
164
- );
165
- applyHistoryTimeZone(turn, sessionTimeZone);
166
- turn.status = "completed";
167
- activeTurnId = "";
466
+ if (eventType === "task_complete" || eventType === "turn_aborted" || eventType === "error") {
467
+ const explicitTurnId = normalizeString(payload?.turn_id) || normalizeString(payload?.turnId);
468
+ const terminalTurnId = explicitTurnId || activeTurnId;
469
+ if (terminalTurnId) {
470
+ const turn = ensureTurn(turns, turnsById, terminalTurnId, sessionThreadId, entry.timestamp);
471
+ applyHistoryTimeZone(turn, sessionTimeZone);
472
+ // Aborted/failed runs never write task_complete; without a terminal
473
+ // status here the history page would report them as still running.
474
+ turn.status = eventType === "task_complete"
475
+ ? "completed"
476
+ : (eventType === "error" ? "failed" : "aborted");
477
+ }
478
+ // Desktop interleaves parallel turns in one rollout. A sibling turn's
479
+ // terminal event must not orphan the still-running turn's context:
480
+ // keeping activeTurnId prevents later turn-less items from spawning
481
+ // synthetic "turn-line-N" running turns that pin the thread as active.
482
+ if (terminalEventClosesTrackedTurn(explicitTurnId, activeTurnId)) {
483
+ activeTurnId = "";
484
+ pendingSyntheticTerminal = null;
485
+ } else if (isSyntheticHistoryTurnId(activeTurnId) && explicitTurnId) {
486
+ pendingSyntheticTerminal = {
487
+ turnId: activeTurnId,
488
+ status: terminalStatusForEventType(eventType),
489
+ };
490
+ }
168
491
  continue;
169
492
  }
170
493
 
494
+ if (eventType) {
495
+ pendingSyntheticTerminal = null;
496
+ }
497
+
171
498
  if (eventType === "item_completed") {
172
499
  const completedItem = objectValue(payload?.item);
173
500
  if (!completedItem) {
@@ -177,11 +504,15 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
177
504
  const turn = ensureTurn(
178
505
  turns,
179
506
  turnsById,
180
- normalizeString(payload?.turn_id) || normalizeString(payload?.turnId) || activeTurnId || `turn-line-${index + 1}`,
507
+ normalizeString(payload?.turn_id)
508
+ || normalizeString(payload?.turnId)
509
+ || responseItemTurnId(completedItem)
510
+ || activeTurnId
511
+ || `turn-line-${sourceLineNumber}`,
181
512
  sessionThreadId,
182
513
  entry.timestamp
183
514
  );
184
- const item = normalizeResponseItemForHistory(completedItem, index + 1, {
515
+ const item = normalizeResponseItemForHistory(completedItem, sourceLineNumber, {
185
516
  cwd: sessionCwd,
186
517
  toolCallsByCallId,
187
518
  });
@@ -194,7 +525,10 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
194
525
 
195
526
  if (eventType === "user_message") {
196
527
  const explicitTurnId = normalizeString(payload?.turn_id) || normalizeString(payload?.turnId);
197
- const item = createUserMessageHistoryItem(payload, index + 1, entry.timestamp);
528
+ const item = createUserMessageHistoryItem(payload, sourceLineNumber, entry.timestamp);
529
+ if (!item.text) {
530
+ continue;
531
+ }
198
532
  applyHistoryTimeZone(item, sessionTimeZone);
199
533
  if (!explicitTurnId && !activeTurnId) {
200
534
  pushPendingUserMessage(pendingUserMessages, item);
@@ -204,7 +538,7 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
204
538
  const turn = ensureTurn(
205
539
  turns,
206
540
  turnsById,
207
- explicitTurnId || activeTurnId || `turn-line-${index + 1}`,
541
+ explicitTurnId || activeTurnId || `turn-line-${sourceLineNumber}`,
208
542
  sessionThreadId,
209
543
  entry.timestamp
210
544
  );
@@ -219,6 +553,7 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
219
553
  }
220
554
 
221
555
  if (entry?.type === "response_item") {
556
+ pendingSyntheticTerminal = null;
222
557
  const payload = objectValue(entry.payload);
223
558
  if (!payload) {
224
559
  continue;
@@ -230,12 +565,12 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
230
565
  const turn = ensureTurn(
231
566
  turns,
232
567
  turnsById,
233
- normalizeString(payload.turn_id) || normalizeString(payload.turnId) || activeTurnId || `turn-line-${index + 1}`,
568
+ responseItemTurnId(payload) || activeTurnId || `turn-line-${sourceLineNumber}`,
234
569
  sessionThreadId,
235
570
  entry.timestamp
236
571
  );
237
572
  applyHistoryTimeZone(turn, sessionTimeZone);
238
- const item = normalizeResponseItemForHistory(payload, index + 1, {
573
+ const item = normalizeResponseItemForHistory(payload, sourceLineNumber, {
239
574
  cwd: sessionCwd,
240
575
  toolCallsByCallId,
241
576
  });
@@ -251,21 +586,51 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
251
586
  item.timestamp = itemTimestamp;
252
587
  }
253
588
  applyHistoryTimeZone(item, sessionTimeZone);
589
+ applyHistoryAssistantSourceAlias(item, turn.id, assistantAliasOccurrencesByBaseKey);
254
590
  addHistoryItemToTurn(turn, item);
255
591
  }
256
592
  }
257
593
  }
258
594
 
595
+ if (pendingSyntheticTerminal) {
596
+ closeSyntheticHistoryTurn(turnsById, pendingSyntheticTerminal);
597
+ }
598
+
259
599
  return turns.filter((turn) => turn.items.length > 0);
260
600
  }
261
601
 
602
+ function closeSyntheticHistoryTurn(turnsById, terminal) {
603
+ const turn = turnsById.get(terminal.turnId);
604
+ if (turn) {
605
+ turn.status = terminal.status || "completed";
606
+ }
607
+ }
608
+
609
+ function terminalStatusForEventType(eventType) {
610
+ if (eventType === "turn_aborted") {
611
+ return "aborted";
612
+ }
613
+ if (eventType === "error") {
614
+ return "failed";
615
+ }
616
+ return "completed";
617
+ }
618
+
619
+ function isSyntheticHistoryTurnId(turnId) {
620
+ return /^turn-line-\d+$/.test(normalizeString(turnId));
621
+ }
622
+
262
623
  function createUserMessageHistoryItem(payload, lineNumber, timestamp) {
263
624
  const createdAt = historyItemTimestamp(payload, timestamp);
264
625
  return {
265
626
  id: normalizeString(payload?.id) || `user-message-line-${lineNumber}`,
266
627
  type: "user_message",
267
628
  role: "user",
268
- text: normalizeString(payload?.message) || normalizeString(payload?.text),
629
+ // Event-shaped user messages can carry injected context or IDE prompt
630
+ // wrappers too; keep only the visible request like every other path.
631
+ text: visibleUserPromptText(
632
+ normalizeString(payload?.message) || normalizeString(payload?.text)
633
+ ),
269
634
  createdAt: createdAt || undefined,
270
635
  timestamp: createdAt || undefined,
271
636
  };
@@ -378,7 +743,7 @@ function historyItemTimestamp(item, fallbackTimestamp = "") {
378
743
  }
379
744
 
380
745
  function isUserHistoryItem(item) {
381
- return normalizeHistoryToken(item?.type) === "usermessage"
746
+ return isUserRoleItem(item)
382
747
  || normalizeString(item?.role).toLowerCase() === "user";
383
748
  }
384
749
 
@@ -500,6 +865,20 @@ function normalizeResponseItemForHistory(payload, lineNumber, { cwd = "", toolCa
500
865
  return item;
501
866
  }
502
867
 
868
+ function applyHistoryAssistantSourceAlias(item, turnId, occurrencesByBaseKey = new Map()) {
869
+ if (normalizeHistoryToken(item?.type) !== "message"
870
+ || normalizeString(item?.role).toLowerCase() !== "assistant") {
871
+ return item;
872
+ }
873
+ const sourceKey = buildRemodexSourceItemKey(turnId, responseItemMessageText(item));
874
+ const occurrence = (occurrencesByBaseKey.get(sourceKey) || 0) + 1;
875
+ occurrencesByBaseKey.set(sourceKey, occurrence);
876
+ if (sourceKey && occurrence === 1) {
877
+ item.remodexSourceItemKey = sourceKey;
878
+ }
879
+ return item;
880
+ }
881
+
503
882
  // Converts `view_image` tool output blobs into a lightweight local image reference.
504
883
  function normalizeToolOutputImageViewItemForHistory(payload, lineNumber, { toolCallsByCallId = new Map() } = {}) {
505
884
  const type = normalizeHistoryItemType(payload.type);
@@ -693,6 +1072,7 @@ function normalizeProgressPlanItemForHistory(payload) {
693
1072
  text: explanation || "Planning...",
694
1073
  explanation: explanation || undefined,
695
1074
  plan,
1075
+ remodexProgressPlan: true,
696
1076
  remodexJsonlProgressPlan: true,
697
1077
  };
698
1078
  }
@@ -953,6 +1333,13 @@ function shouldSkipResponseItemForHistory(payload, skippedCallIds) {
953
1333
  return true;
954
1334
  }
955
1335
 
1336
+ // Injected context (AGENTS.md instructions, environment_context wrappers) is
1337
+ // persisted as user-role response items; Codex UIs hide it at render time and
1338
+ // mobile history must do the same.
1339
+ if (role === "user" && isContextualUserText(responseItemMessageText(payload))) {
1340
+ return true;
1341
+ }
1342
+
956
1343
  return false;
957
1344
  }
958
1345
 
@@ -974,19 +1361,19 @@ function isSubagentNotificationMessage(payload) {
974
1361
  return text.startsWith("<subagent_notification>");
975
1362
  }
976
1363
 
977
- function responseItemMessageText(payload) {
978
- const directText = normalizeString(payload.text) || normalizeString(payload.message);
979
- if (directText) {
980
- return directText;
981
- }
1364
+ // Modern Codex rollouts keep response-item ownership in metadata passthrough.
1365
+ // A rollout can interleave parallel turns, so the process-wide active turn is
1366
+ // only a last resort; using it first moves tools, plans, and prose across turns.
1367
+ function responseItemTurnId(payload) {
1368
+ const metadata = objectValue(payload?.internal_chat_message_metadata_passthrough);
1369
+ return normalizeString(payload?.turn_id)
1370
+ || normalizeString(payload?.turnId)
1371
+ || normalizeString(metadata?.turn_id)
1372
+ || normalizeString(metadata?.turnId);
1373
+ }
982
1374
 
983
- const content = Array.isArray(payload.content) ? payload.content : [];
984
- return content
985
- .map((item) => objectValue(item))
986
- .filter(Boolean)
987
- .map((item) => responseItemContentText(item))
988
- .filter(Boolean)
989
- .join("\n");
1375
+ function responseItemMessageText(payload) {
1376
+ return sharedResponseItemMessageText(payload);
990
1377
  }
991
1378
 
992
1379
  function responseItemContentText(item) {
@@ -1090,7 +1477,10 @@ function normalizeString(value) {
1090
1477
  }
1091
1478
 
1092
1479
  module.exports = {
1480
+ JSONL_OLDER_HANDOFF_CURSOR,
1093
1481
  parseSessionJsonlMetadata,
1094
1482
  parseSessionJsonlTurns,
1483
+ readRecentSessionJsonlTurns,
1484
+ readSessionJsonlMetadataFromFile,
1095
1485
  readThreadTurnsListPageFromSessionJsonl,
1096
1486
  };
@@ -6,12 +6,14 @@
6
6
 
7
7
  const { readLatestContextWindowUsage } = require("./rollout-watch");
8
8
 
9
- function handleThreadContextRequest(rawMessage, sendResponse) {
10
- let parsed;
11
- try {
12
- parsed = JSON.parse(rawMessage);
13
- } catch {
14
- return false;
9
+ function handleThreadContextRequest(rawMessage, sendResponse, parsedMessage = null) {
10
+ let parsed = parsedMessage;
11
+ if (!parsed) {
12
+ try {
13
+ parsed = JSON.parse(rawMessage);
14
+ } catch {
15
+ return false;
16
+ }
15
17
  }
16
18
 
17
19
  const method = typeof parsed?.method === "string" ? parsed.method.trim() : "";