@makerbi/remodex 1.4.0 → 1.5.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 CHANGED
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "name": "@makerbi/remodex",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Local bridge between Codex and the Remodex mobile app. Run `remodex up` to start.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/AndersonBY/remodex.git",
8
+ "directory": "phodex-bridge"
9
+ },
5
10
  "main": "src/index.js",
6
11
  "bin": {
7
12
  "remodex": "bin/remodex.js"
package/src/bridge.js CHANGED
@@ -24,6 +24,7 @@ const { handleGitRequest } = require("./git-handler");
24
24
  const { handleThreadContextRequest } = require("./thread-context-handler");
25
25
  const { handleWorkspaceRequest } = require("./workspace-handler");
26
26
  const { handleProjectRequest } = require("./project-handler");
27
+ const { handlePetRequest } = require("./pet-handler");
27
28
  const { createNotificationsHandler } = require("./notifications-handler");
28
29
  const { createVoiceHandler, resolveVoiceAuth } = require("./voice-handler");
29
30
  const {
@@ -42,7 +43,6 @@ const { createBridgeSecureTransport } = require("./secure-transport");
42
43
  const { createRolloutLiveMirrorController } = require("./rollout-live-mirror");
43
44
  const {
44
45
  createDesktopIpcActionFollower,
45
- seedConversationStateFromThreadRead,
46
46
  } = require("./desktop-ipc-action-follower");
47
47
  const { version: bridgePackageVersion = "" } = require("../package.json");
48
48
  const {
@@ -66,6 +66,24 @@ const RELAY_HISTORY_IMAGE_REFERENCE_URL = "remodex://history-image-elided";
66
66
  const RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES = 4 * 1024 * 1024;
67
67
  const RELAY_HISTORY_TEXT_TAIL_LIMIT_CHARS = 24_000;
68
68
  const RELAY_HISTORY_RECENT_TURN_TARGET = 40;
69
+ const RELAY_TURNS_LIST_TARGET_BUDGET_MS = 5_500;
70
+ const RELAY_TURNS_LIST_BUDGET_RESERVE_MS = 1_000;
71
+ const RELAY_TURNS_LIST_RESULT_KEYS = ["data", "items", "turns"];
72
+ const RELAY_TURNS_LIST_PAGINATION_RESULT_KEYS = [
73
+ "nextCursor",
74
+ "next_cursor",
75
+ "cursor",
76
+ "hasNextCursor",
77
+ "has_next_cursor",
78
+ "hasNextPage",
79
+ "has_next_page",
80
+ "hasMore",
81
+ "has_more",
82
+ "prevCursor",
83
+ "prev_cursor",
84
+ "previousCursor",
85
+ "previous_cursor",
86
+ ];
69
87
 
70
88
  function buildRelayUserAgentHeader({ version = bridgePackageVersion } = {}) {
71
89
  const normalizedVersion = typeof version === "string" && version.trim()
@@ -168,6 +186,7 @@ function startBridge({
168
186
  const relaySanitizedRequestMethods = new Set([
169
187
  "thread/read",
170
188
  "thread/resume",
189
+ "thread/turns/list",
171
190
  ]);
172
191
  const forwardedRequestMethodTTLms = 2 * 60_000;
173
192
  const pendingAuthLogin = {
@@ -205,7 +224,6 @@ function startBridge({
205
224
  const desktopIpcActionFollower = !config.codexEndpoint
206
225
  ? createDesktopIpcActionFollower({
207
226
  sendApplicationResponse,
208
- readConversationState: readDesktopConversationState,
209
227
  socketPath: config.desktopIpcSocketPath || undefined,
210
228
  })
211
229
  : null;
@@ -554,6 +572,9 @@ function startBridge({
554
572
  if (handleProjectRequest(rawMessage, sendApplicationResponse)) {
555
573
  return;
556
574
  }
575
+ if (handlePetRequest(rawMessage, sendApplicationResponse)) {
576
+ return;
577
+ }
557
578
  if (notificationsHandler.handleNotificationsRequest(rawMessage, sendApplicationResponse)) {
558
579
  return;
559
580
  }
@@ -576,6 +597,9 @@ function startBridge({
576
597
  if (desktopIpcActionFollower?.observeInbound(rawMessage)) {
577
598
  return;
578
599
  }
600
+ if (handleBridgeManagedThreadTurnsListRequest(rawMessage)) {
601
+ return;
602
+ }
579
603
  rememberForwardedRequestMethod(rawMessage);
580
604
  rememberThreadFromMessage("phone", rawMessage);
581
605
  codex.send(rawMessage);
@@ -608,13 +632,33 @@ function startBridge({
608
632
  }));
609
633
  }
610
634
 
611
- // Seeds the desktop IPC follower when it receives patches before a full snapshot.
612
- async function readDesktopConversationState(threadId) {
613
- const result = await sendCodexRequest("thread/read", {
614
- threadId,
615
- includeTurns: true,
616
- });
617
- return seedConversationStateFromThreadRead(result);
635
+ function handleBridgeManagedThreadTurnsListRequest(rawMessage) {
636
+ const request = parseAdaptiveThreadTurnsListRequest(rawMessage);
637
+ if (!request) {
638
+ return false;
639
+ }
640
+
641
+ rememberThreadFromMessage("phone", rawMessage);
642
+ (async () => {
643
+ try {
644
+ const response = await fetchAdaptiveThreadTurnsListForRelay(request, {
645
+ fetchPage: (params) => sendCodexRequest("thread/turns/list", params),
646
+ });
647
+ relaySanitizedResponseMethodsById.set(String(request.id), {
648
+ method: "thread/turns/list",
649
+ createdAt: Date.now(),
650
+ });
651
+ sendApplicationResponse(JSON.stringify(response));
652
+ } catch (error) {
653
+ sendApplicationResponse(createJsonRpcErrorResponse(
654
+ request.id,
655
+ error,
656
+ "thread_turns_list_failed"
657
+ ));
658
+ }
659
+ })();
660
+
661
+ return true;
618
662
  }
619
663
 
620
664
  // ─── Bridge-owned auth snapshot ─────────────────────────────
@@ -1450,81 +1494,242 @@ function normalizeNonEmptyString(value) {
1450
1494
  return typeof value === "string" && value.trim() ? value.trim() : "";
1451
1495
  }
1452
1496
 
1453
- // Shrinks `thread/read` and `thread/resume` snapshots for mobile relay delivery.
1454
- // This elides bulky blobs and replaces oversized older history with a compact marker.
1455
- function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod) {
1456
- if (requestMethod !== "thread/read" && requestMethod !== "thread/resume") {
1457
- return rawMessage;
1497
+ function parseAdaptiveThreadTurnsListRequest(rawMessage) {
1498
+ const parsed = parseBridgeJSON(rawMessage);
1499
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1500
+ return null;
1458
1501
  }
1459
1502
 
1460
- const parsed = parseBridgeJSON(rawMessage);
1461
- const thread = parsed?.result?.thread;
1462
- if (!thread || typeof thread !== "object" || !Array.isArray(thread.turns)) {
1463
- return rawMessage;
1503
+ if (parsed.method !== "thread/turns/list") {
1504
+ return null;
1464
1505
  }
1465
1506
 
1466
- let didSanitize = false;
1467
- const sanitizedTurns = thread.turns.map((turn) => {
1468
- if (!turn || typeof turn !== "object" || !Array.isArray(turn.items)) {
1469
- return turn;
1470
- }
1507
+ if (parsed.id == null) {
1508
+ return null;
1509
+ }
1471
1510
 
1472
- let turnDidChange = false;
1473
- const threadId = normalizeNonEmptyString(thread.id)
1474
- || normalizeNonEmptyString(thread.threadId)
1475
- || normalizeNonEmptyString(thread.thread_id);
1511
+ const params = parsed.params;
1512
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
1513
+ return null;
1514
+ }
1476
1515
 
1477
- const sanitizedItems = turn.items.map((item) => {
1478
- if (!item || typeof item !== "object") {
1479
- return item;
1480
- }
1516
+ if (!Number.isInteger(params.limit) || params.limit <= 0) {
1517
+ return null;
1518
+ }
1481
1519
 
1482
- let itemDidChange = false;
1483
- let sanitizedItem = annotateImageGenerationHistoryItem(item, threadId);
1484
- if (sanitizedItem !== item) {
1485
- itemDidChange = true;
1486
- }
1520
+ return parsed;
1521
+ }
1487
1522
 
1488
- if (Array.isArray(item.content)) {
1489
- const sanitizedContent = item.content.map((contentItem) => {
1490
- const sanitizedEntry = sanitizeInlineHistoryImageContentItem(contentItem);
1491
- if (sanitizedEntry !== contentItem) {
1492
- itemDidChange = true;
1493
- }
1494
- return sanitizedEntry;
1495
- });
1523
+ async function fetchAdaptiveThreadTurnsListForRelay(request, {
1524
+ fetchPage,
1525
+ now = Date.now,
1526
+ targetBudgetMs = RELAY_TURNS_LIST_TARGET_BUDGET_MS,
1527
+ budgetReserveMs = RELAY_TURNS_LIST_BUDGET_RESERVE_MS,
1528
+ rawPageSoftLimitBytes = RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES,
1529
+ payloadSoftLimitBytes = RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES,
1530
+ sanitizeForRelay = sanitizeThreadHistoryImagesForRelay,
1531
+ } = {}) {
1532
+ if (typeof fetchPage !== "function") {
1533
+ throw new Error("fetchPage is required for adaptive turns-list pagination.");
1534
+ }
1535
+
1536
+ const params = request?.params;
1537
+ const requestedLimit = Number.isInteger(params?.limit) && params.limit > 0
1538
+ ? params.limit
1539
+ : 1;
1540
+ const startedAt = now();
1541
+ let nextCursor = params?.cursor;
1542
+ let turnsKey = null;
1543
+ let firstResult = null;
1544
+ let lastResult = null;
1545
+ let combinedTurns = [];
1546
+ let response = null;
1547
+
1548
+ while (combinedTurns.length < requestedLimit) {
1549
+ const remaining = requestedLimit - combinedTurns.length;
1550
+ const pageLimit = selectAdaptiveTurnsListBatchLimit(combinedTurns.length, remaining);
1551
+ const pageParams = buildAdaptiveTurnsListPageParams(params, pageLimit, nextCursor);
1552
+ let page;
1496
1553
 
1497
- if (itemDidChange) {
1498
- sanitizedItem = {
1499
- ...sanitizedItem,
1500
- content: sanitizedContent,
1501
- };
1502
- }
1554
+ try {
1555
+ page = await fetchMeasuredAdaptiveTurnsListPage(fetchPage, pageParams, now);
1556
+ } catch (error) {
1557
+ if (response) {
1558
+ return response;
1503
1559
  }
1560
+ throw error;
1561
+ }
1504
1562
 
1505
- const sanitizedCompactionItem = sanitizeCompactionHistoryItem(sanitizedItem);
1506
- if (sanitizedCompactionItem !== sanitizedItem) {
1507
- sanitizedItem = sanitizedCompactionItem;
1508
- itemDidChange = true;
1563
+ const pageResult = page.result;
1564
+ const pageTurnsKey = findTurnsListResultKey(pageResult);
1565
+ if (!pageTurnsKey) {
1566
+ if (!response) {
1567
+ return {
1568
+ id: request.id,
1569
+ result: pageResult ?? null,
1570
+ };
1509
1571
  }
1572
+ return response;
1573
+ }
1510
1574
 
1511
- if (itemDidChange) {
1512
- turnDidChange = true;
1513
- }
1575
+ if (!turnsKey) {
1576
+ turnsKey = pageTurnsKey;
1577
+ }
1578
+ if (!firstResult) {
1579
+ firstResult = pageResult;
1580
+ }
1581
+ lastResult = pageResult;
1514
1582
 
1515
- return itemDidChange ? sanitizedItem : item;
1516
- });
1583
+ const pageTurns = pageResult[pageTurnsKey];
1584
+ combinedTurns = combinedTurns.concat(pageTurns);
1585
+ response = {
1586
+ id: request.id,
1587
+ result: buildAdaptiveTurnsListResult(firstResult, lastResult, turnsKey, combinedTurns),
1588
+ };
1517
1589
 
1518
- if (!turnDidChange) {
1519
- return turn;
1590
+ nextCursor = readTurnsListNextCursor(pageResult);
1591
+ if (combinedTurns.length >= requestedLimit || !hasRelayCursor(nextCursor) || pageTurns.length === 0) {
1592
+ break;
1520
1593
  }
1521
1594
 
1522
- didSanitize = true;
1523
- return {
1524
- ...turn,
1525
- items: sanitizedItems,
1526
- };
1527
- });
1595
+ const rawPageBytes = jsonByteLength(pageResult);
1596
+ const sanitizedResponseBytes = measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay);
1597
+ const elapsedMs = Math.max(0, now() - startedAt);
1598
+ const remainingBudgetMs = Math.max(0, targetBudgetMs - elapsedMs);
1599
+ if (
1600
+ rawPageBytes >= rawPageSoftLimitBytes
1601
+ || sanitizedResponseBytes >= payloadSoftLimitBytes
1602
+ || page.elapsedMs >= Math.max(0, targetBudgetMs - budgetReserveMs)
1603
+ || remainingBudgetMs <= budgetReserveMs
1604
+ ) {
1605
+ break;
1606
+ }
1607
+ }
1608
+
1609
+ return response ?? {
1610
+ id: request.id,
1611
+ result: {
1612
+ data: [],
1613
+ },
1614
+ };
1615
+ }
1616
+
1617
+ async function fetchMeasuredAdaptiveTurnsListPage(fetchPage, params, now) {
1618
+ const startedAt = now();
1619
+ const result = await fetchPage(params);
1620
+ const elapsedMs = Math.max(0, now() - startedAt);
1621
+ return {
1622
+ result,
1623
+ elapsedMs,
1624
+ };
1625
+ }
1626
+
1627
+ function selectAdaptiveTurnsListBatchLimit(fetchedTurnCount, remainingTurnCount) {
1628
+ if (fetchedTurnCount <= 0) {
1629
+ return Math.min(1, remainingTurnCount);
1630
+ }
1631
+ if (fetchedTurnCount <= 1) {
1632
+ return Math.min(4, remainingTurnCount);
1633
+ }
1634
+ return remainingTurnCount;
1635
+ }
1636
+
1637
+ function buildAdaptiveTurnsListPageParams(baseParams, limit, cursor) {
1638
+ const params = {
1639
+ ...baseParams,
1640
+ limit,
1641
+ };
1642
+ if (hasRelayCursor(cursor)) {
1643
+ params.cursor = cursor;
1644
+ } else {
1645
+ delete params.cursor;
1646
+ }
1647
+ return params;
1648
+ }
1649
+
1650
+ function findTurnsListResultKey(result) {
1651
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
1652
+ return null;
1653
+ }
1654
+ return RELAY_TURNS_LIST_RESULT_KEYS.find((key) => Array.isArray(result[key])) || null;
1655
+ }
1656
+
1657
+ function buildAdaptiveTurnsListResult(firstResult, lastResult, turnsKey, turns) {
1658
+ const result = {
1659
+ ...firstResult,
1660
+ };
1661
+ for (const key of RELAY_TURNS_LIST_RESULT_KEYS) {
1662
+ delete result[key];
1663
+ }
1664
+ result[turnsKey] = turns;
1665
+
1666
+ for (const key of RELAY_TURNS_LIST_PAGINATION_RESULT_KEYS) {
1667
+ if (Object.prototype.hasOwnProperty.call(lastResult, key)) {
1668
+ result[key] = lastResult[key];
1669
+ } else {
1670
+ delete result[key];
1671
+ }
1672
+ }
1673
+
1674
+ return result;
1675
+ }
1676
+
1677
+ function readTurnsListNextCursor(result) {
1678
+ if (!result || typeof result !== "object") {
1679
+ return undefined;
1680
+ }
1681
+ if (hasRelayCursor(result.nextCursor)) {
1682
+ return result.nextCursor;
1683
+ }
1684
+ if (hasRelayCursor(result.next_cursor)) {
1685
+ return result.next_cursor;
1686
+ }
1687
+ return undefined;
1688
+ }
1689
+
1690
+ function hasRelayCursor(cursor) {
1691
+ return cursor !== undefined && cursor !== null && cursor !== "";
1692
+ }
1693
+
1694
+ function jsonByteLength(value) {
1695
+ try {
1696
+ return Buffer.byteLength(JSON.stringify(value), "utf8");
1697
+ } catch {
1698
+ return Number.POSITIVE_INFINITY;
1699
+ }
1700
+ }
1701
+
1702
+ function measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay) {
1703
+ try {
1704
+ const rawResponse = JSON.stringify(response);
1705
+ const sanitizedResponse = sanitizeForRelay(rawResponse, "thread/turns/list");
1706
+ return Buffer.byteLength(sanitizedResponse, "utf8");
1707
+ } catch {
1708
+ return Number.POSITIVE_INFINITY;
1709
+ }
1710
+ }
1711
+
1712
+ // Shrinks thread history snapshots/pages for mobile relay delivery.
1713
+ // This elides bulky blobs and replaces oversized older history with a compact marker.
1714
+ function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod) {
1715
+ if (requestMethod === "thread/turns/list") {
1716
+ return sanitizeThreadTurnsListForRelay(rawMessage);
1717
+ }
1718
+
1719
+ if (requestMethod !== "thread/read" && requestMethod !== "thread/resume") {
1720
+ return rawMessage;
1721
+ }
1722
+
1723
+ const parsed = parseBridgeJSON(rawMessage);
1724
+ const thread = parsed?.result?.thread;
1725
+ if (!thread || typeof thread !== "object" || !Array.isArray(thread.turns)) {
1726
+ return rawMessage;
1727
+ }
1728
+
1729
+ const threadId = normalizeNonEmptyString(thread.id)
1730
+ || normalizeNonEmptyString(thread.threadId)
1731
+ || normalizeNonEmptyString(thread.thread_id);
1732
+ const { turns: sanitizedTurns, didSanitize } = sanitizeRelayHistoryTurns(thread.turns, threadId);
1528
1733
 
1529
1734
  if (!didSanitize) {
1530
1735
  const trimmedPayload = trimThreadPayloadForRelay(parsed, thread);
@@ -1545,6 +1750,108 @@ function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod) {
1545
1750
  return trimThreadPayloadForRelay(parseBridgeJSON(sanitizedPayload), null) ?? sanitizedPayload;
1546
1751
  }
1547
1752
 
1753
+ function sanitizeThreadTurnsListForRelay(rawMessage) {
1754
+ const parsed = parseBridgeJSON(rawMessage);
1755
+ const result = parsed?.result;
1756
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
1757
+ return rawMessage;
1758
+ }
1759
+
1760
+ const turnsKey = ["data", "items", "turns"].find((key) => Array.isArray(result[key]));
1761
+ if (!turnsKey) {
1762
+ return rawMessage;
1763
+ }
1764
+
1765
+ const threadId = normalizeNonEmptyString(result.threadId)
1766
+ || normalizeNonEmptyString(result.thread_id)
1767
+ || normalizeNonEmptyString(result.thread?.id)
1768
+ || normalizeNonEmptyString(result.thread?.threadId)
1769
+ || normalizeNonEmptyString(result.thread?.thread_id);
1770
+ const { turns: sanitizedTurns, didSanitize } = sanitizeRelayHistoryTurns(result[turnsKey], threadId);
1771
+ const sanitizedParsed = didSanitize
1772
+ ? {
1773
+ ...parsed,
1774
+ result: {
1775
+ ...result,
1776
+ [turnsKey]: sanitizedTurns,
1777
+ },
1778
+ }
1779
+ : parsed;
1780
+
1781
+ return trimTurnsListPayloadForRelay(sanitizedParsed, turnsKey, didSanitize ? null : rawMessage);
1782
+ }
1783
+
1784
+ function sanitizeRelayHistoryTurns(turns, threadId = "") {
1785
+ let didSanitize = false;
1786
+ const sanitizedTurns = turns.map((turn) => {
1787
+ const sanitizedTurn = sanitizeRelayHistoryTurn(turn, threadId);
1788
+ if (sanitizedTurn !== turn) {
1789
+ didSanitize = true;
1790
+ }
1791
+ return sanitizedTurn;
1792
+ });
1793
+
1794
+ return { turns: sanitizedTurns, didSanitize };
1795
+ }
1796
+
1797
+ function sanitizeRelayHistoryTurn(turn, threadId = "") {
1798
+ if (!turn || typeof turn !== "object" || !Array.isArray(turn.items)) {
1799
+ return turn;
1800
+ }
1801
+
1802
+ let turnDidChange = false;
1803
+ const turnThreadId = normalizeNonEmptyString(threadId)
1804
+ || normalizeNonEmptyString(turn.threadId)
1805
+ || normalizeNonEmptyString(turn.thread_id);
1806
+ const sanitizedItems = turn.items.map((item) => {
1807
+ if (!item || typeof item !== "object") {
1808
+ return item;
1809
+ }
1810
+
1811
+ let itemDidChange = false;
1812
+ let sanitizedItem = annotateImageGenerationHistoryItem(item, turnThreadId);
1813
+ if (sanitizedItem !== item) {
1814
+ itemDidChange = true;
1815
+ }
1816
+
1817
+ if (Array.isArray(sanitizedItem.content)) {
1818
+ const sanitizedContent = sanitizedItem.content.map((contentItem) => {
1819
+ const sanitizedEntry = sanitizeInlineHistoryImageContentItem(contentItem);
1820
+ if (sanitizedEntry !== contentItem) {
1821
+ itemDidChange = true;
1822
+ }
1823
+ return sanitizedEntry;
1824
+ });
1825
+
1826
+ if (itemDidChange) {
1827
+ sanitizedItem = {
1828
+ ...sanitizedItem,
1829
+ content: sanitizedContent,
1830
+ };
1831
+ }
1832
+ }
1833
+
1834
+ const sanitizedCompactionItem = sanitizeCompactionHistoryItem(sanitizedItem);
1835
+ if (sanitizedCompactionItem !== sanitizedItem) {
1836
+ sanitizedItem = sanitizedCompactionItem;
1837
+ itemDidChange = true;
1838
+ }
1839
+
1840
+ if (itemDidChange) {
1841
+ turnDidChange = true;
1842
+ }
1843
+
1844
+ return itemDidChange ? sanitizedItem : item;
1845
+ });
1846
+
1847
+ return turnDidChange
1848
+ ? {
1849
+ ...turn,
1850
+ items: sanitizedItems,
1851
+ }
1852
+ : turn;
1853
+ }
1854
+
1548
1855
  // Annotates live image-generation notifications so the phone can render a local-file
1549
1856
  // preview and does not receive the bulky inline base64 result over the relay.
1550
1857
  function sanitizeLiveGeneratedImageMessageForRelay(rawMessage) {
@@ -1947,6 +2254,55 @@ function trimThreadPayloadForRelay(parsed, explicitThread = undefined) {
1947
2254
  return encodeRelayThreadPayload(parsed, candidateThread);
1948
2255
  }
1949
2256
 
2257
+ function trimTurnsListPayloadForRelay(parsed, turnsKey, originalRawMessage = null) {
2258
+ const result = parsed?.result;
2259
+ const turns = result?.[turnsKey];
2260
+ if (!parsed || !result || !Array.isArray(turns)) {
2261
+ return originalRawMessage ?? JSON.stringify(parsed);
2262
+ }
2263
+
2264
+ const encoded = JSON.stringify(parsed);
2265
+ if (Buffer.byteLength(encoded, "utf8") <= RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES) {
2266
+ return originalRawMessage ?? encoded;
2267
+ }
2268
+
2269
+ let fallbackCompactedPayload = null;
2270
+ for (const maxChars of [
2271
+ RELAY_HISTORY_TEXT_TAIL_LIMIT_CHARS,
2272
+ Math.floor(RELAY_HISTORY_TEXT_TAIL_LIMIT_CHARS / 4),
2273
+ 1_000,
2274
+ 0,
2275
+ ]) {
2276
+ const compactedTurns = turns.map((turn) => compactTurnsListTurnForRelay(turn, maxChars));
2277
+ const compactedPayload = JSON.stringify({
2278
+ ...parsed,
2279
+ result: {
2280
+ ...result,
2281
+ [turnsKey]: compactedTurns,
2282
+ remodexPageCompactedForRelay: true,
2283
+ },
2284
+ });
2285
+ fallbackCompactedPayload = compactedPayload;
2286
+ if (Buffer.byteLength(compactedPayload, "utf8") <= RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES) {
2287
+ return compactedPayload;
2288
+ }
2289
+ }
2290
+
2291
+ return fallbackCompactedPayload ?? (originalRawMessage ?? encoded);
2292
+ }
2293
+
2294
+ function compactTurnsListTurnForRelay(turn, maxChars) {
2295
+ if (!turn || typeof turn !== "object" || !Array.isArray(turn.items)) {
2296
+ return turn;
2297
+ }
2298
+
2299
+ return {
2300
+ ...turn,
2301
+ items: turn.items.map((item) => compactHistoryItemForRelay(item, maxChars)),
2302
+ remodexPageCompactedForRelay: true,
2303
+ };
2304
+ }
2305
+
1950
2306
  function buildRelayHistoryCompactedThread(thread, turns, omittedTurnCount, keptTurnCount) {
1951
2307
  return {
1952
2308
  ...thread,
@@ -2074,7 +2430,7 @@ function compactHistoryItemForRelay(item, maxChars) {
2074
2430
  itemId: typeof item?.itemId === "string" ? item.itemId : undefined,
2075
2431
  relayPayloadTruncated: true,
2076
2432
  };
2077
- const tailText = firstRelayTextTail(item, maxChars);
2433
+ const tailText = maxChars > 0 ? firstRelayTextTail(item, maxChars) : "";
2078
2434
  if (tailText) {
2079
2435
  compactItem.text = tailText;
2080
2436
  }
@@ -2179,6 +2535,7 @@ module.exports = {
2179
2535
  buildRelayAccessTokenHeaders,
2180
2536
  buildRelayUserAgentHeader,
2181
2537
  createMacOSBridgeWakeAssertion,
2538
+ fetchAdaptiveThreadTurnsListForRelay,
2182
2539
  hasRelayConnectionGoneStale,
2183
2540
  isTerminalRelayCloseCode,
2184
2541
  persistBridgePreferences,