codex-mobile-re 0.1.131 → 0.1.133

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.
Files changed (25) hide show
  1. package/dist/assets/{AutomationsPanel-CZ_k1aNL.js → AutomationsPanel-B9A37CNa.js} +1 -1
  2. package/dist/assets/DirectoryHub-D_TdDpHF.js +2 -0
  3. package/dist/assets/{QueuedMessages-DSxRli0y.js → QueuedMessages-CsAVexKb.js} +1 -1
  4. package/dist/assets/{ReviewPane-DO60w4j5.js → ReviewPane-XhRr3646.js} +1 -1
  5. package/dist/assets/{RightFilePreview-B2tx1lOm.js → RightFilePreview-Ch_neGS2.js} +1 -1
  6. package/dist/assets/RightFilesPanel-5fMx5J84.js +1 -0
  7. package/dist/assets/RightGitPanel-irYPFJZZ.js +2 -0
  8. package/dist/assets/{SettingsDialog-D9kB2mzO.js → SettingsDialog-Dg10TF-D.js} +3 -3
  9. package/dist/assets/ThreadConversation-8TMOUqdg.js +25 -0
  10. package/dist/assets/ThreadConversation-Dx1ZKW94.css +1 -0
  11. package/dist/assets/{ThreadPendingRequestPanel-DGbhyPFH.js → ThreadPendingRequestPanel-DorbAtp9.js} +1 -1
  12. package/dist/assets/{ThreadTerminalPanel-DhYhXg8u.js → ThreadTerminalPanel-CKhCiseu.js} +1 -1
  13. package/dist/assets/index-CMrfiiel.js +98 -0
  14. package/dist/assets/index-Dm1iQsik.css +1 -0
  15. package/dist/index.html +2 -2
  16. package/dist-cli/index.js +257 -221
  17. package/dist-cli/index.js.map +1 -1
  18. package/package.json +1 -1
  19. package/dist/assets/DirectoryHub-Yd0fIqxf.js +0 -2
  20. package/dist/assets/RightFilesPanel-r9ijJqw4.js +0 -1
  21. package/dist/assets/RightGitPanel-BxZdreeN.js +0 -2
  22. package/dist/assets/ThreadConversation-CVVohemt.js +0 -39
  23. package/dist/assets/ThreadConversation-D1JHRys4.css +0 -1
  24. package/dist/assets/index-ClI-fbXf.js +0 -83
  25. package/dist/assets/index-DthBwStG.css +0 -1
package/dist/index.html CHANGED
@@ -36,8 +36,8 @@
36
36
  <link rel="preload" as="font" type="font/woff2" href="/fonts/ibm-plex-sans-400.woff2" crossorigin />
37
37
  <link rel="preload" as="font" type="font/woff2" href="/fonts/ibm-plex-mono-400.woff2" crossorigin />
38
38
  <title>Codex Web</title>
39
- <script type="module" crossorigin src="/assets/index-ClI-fbXf.js"></script>
40
- <link rel="stylesheet" crossorigin href="/assets/index-DthBwStG.css">
39
+ <script type="module" crossorigin src="/assets/index-CMrfiiel.js"></script>
40
+ <link rel="stylesheet" crossorigin href="/assets/index-Dm1iQsik.css">
41
41
  </head>
42
42
  <body>
43
43
  <div id="app"></div>
package/dist-cli/index.js CHANGED
@@ -5077,6 +5077,210 @@ async function handleSkillsRoutes(req, res, url, context) {
5077
5077
  // src/server/telegramThreadBridge.ts
5078
5078
  import { basename as basename2 } from "path";
5079
5079
 
5080
+ // src/server/bridge/threadTurnPage.ts
5081
+ var ThreadTurnPageUnsupportedError = class extends Error {
5082
+ constructor(cause) {
5083
+ super(`thread/turns/list is not supported by this app-server: ${getErrorMessage(cause, "unknown error")}`);
5084
+ this.name = "ThreadTurnPageUnsupportedError";
5085
+ }
5086
+ };
5087
+ var TURN_LIST_UNSUPPORTED_PATTERN = /-32601|not supported|not implemented|unknown variant|method not found|unknown method/i;
5088
+ function isTurnListUnsupportedError(error) {
5089
+ return TURN_LIST_UNSUPPORTED_PATTERN.test(getErrorMessage(error, ""));
5090
+ }
5091
+ var TURN_ID_PAGE_SIZE = 1e4;
5092
+ var TURN_ID_MAX_PAGES = 50;
5093
+ var TURN_LIST_SERVER_PAGE_CLAMP = 100;
5094
+ var ThreadTurnPageCursorChain = class {
5095
+ constructor(options = {}) {
5096
+ this.cursorByThreadId = /* @__PURE__ */ new Map();
5097
+ this.maxThreads = options.maxThreads ?? 64;
5098
+ this.maxTurnsPerThread = options.maxTurnsPerThread ?? 256;
5099
+ }
5100
+ /** Record that `olderCursor` reaches the turns just before `oldestTurnId`. */
5101
+ record(threadId, oldestTurnId, olderCursor) {
5102
+ if (!threadId || !oldestTurnId) return;
5103
+ const cursor = readNonEmptyString(olderCursor);
5104
+ if (!cursor) return;
5105
+ let perThread = this.cursorByThreadId.get(threadId);
5106
+ if (!perThread) {
5107
+ perThread = /* @__PURE__ */ new Map();
5108
+ this.cursorByThreadId.set(threadId, perThread);
5109
+ if (this.cursorByThreadId.size > this.maxThreads) {
5110
+ const oldestThreadId = this.cursorByThreadId.keys().next().value;
5111
+ if (typeof oldestThreadId === "string") this.cursorByThreadId.delete(oldestThreadId);
5112
+ }
5113
+ }
5114
+ perThread.delete(oldestTurnId);
5115
+ perThread.set(oldestTurnId, cursor);
5116
+ if (perThread.size > this.maxTurnsPerThread) {
5117
+ const oldestTurnId2 = perThread.keys().next().value;
5118
+ if (typeof oldestTurnId2 === "string") perThread.delete(oldestTurnId2);
5119
+ }
5120
+ }
5121
+ lookup(threadId, turnId) {
5122
+ if (!threadId || !turnId) return null;
5123
+ const perThread = this.cursorByThreadId.get(threadId);
5124
+ if (!perThread) return null;
5125
+ const cursor = perThread.get(turnId);
5126
+ if (cursor === void 0) return null;
5127
+ perThread.delete(turnId);
5128
+ perThread.set(turnId, cursor);
5129
+ return cursor;
5130
+ }
5131
+ clear(threadId) {
5132
+ if (threadId) this.cursorByThreadId.delete(threadId);
5133
+ else this.cursorByThreadId.clear();
5134
+ }
5135
+ /**
5136
+ * Restore a persisted snapshot (round-136). Entries go in through `record`,
5137
+ * so the caps and the least-recently-recorded eviction order apply to restored
5138
+ * anchors exactly as they do to fresh ones -- the caller therefore feeds the
5139
+ * snapshot oldest-first, and a snapshot older than the caps degrades to "the
5140
+ * newest entries survive" rather than to a rejected file.
5141
+ */
5142
+ hydrate(state) {
5143
+ if (!state) return;
5144
+ for (const [threadId, perThread] of Object.entries(state)) {
5145
+ for (const [turnId, cursor] of Object.entries(perThread ?? {})) {
5146
+ this.record(threadId, turnId, cursor);
5147
+ }
5148
+ }
5149
+ }
5150
+ /** What is currently known, oldest registration first (the persist order). */
5151
+ snapshot() {
5152
+ const state = {};
5153
+ for (const [threadId, perThread] of this.cursorByThreadId) {
5154
+ const entries = {};
5155
+ for (const [turnId, cursor] of perThread) entries[turnId] = cursor;
5156
+ if (Object.keys(entries).length > 0) state[threadId] = entries;
5157
+ }
5158
+ return state;
5159
+ }
5160
+ };
5161
+ async function readThreadTurnIds(rpc, threadId) {
5162
+ if (!threadId) return null;
5163
+ const descending = [];
5164
+ let cursor = "";
5165
+ for (let page = 0; page < TURN_ID_MAX_PAGES; page += 1) {
5166
+ const params = {
5167
+ threadId,
5168
+ limit: TURN_ID_PAGE_SIZE,
5169
+ sortDirection: "desc",
5170
+ itemsView: "notLoaded"
5171
+ };
5172
+ if (cursor) params.cursor = cursor;
5173
+ let result;
5174
+ try {
5175
+ result = await rpc("thread/turns/list", params);
5176
+ } catch (error) {
5177
+ if (isTurnListUnsupportedError(error)) throw new ThreadTurnPageUnsupportedError(error);
5178
+ return null;
5179
+ }
5180
+ const record = asRecord(result);
5181
+ if (!record || !Array.isArray(record.data)) return null;
5182
+ for (const turn of record.data) {
5183
+ const turnId = readNonEmptyString(asRecord(turn)?.id);
5184
+ if (!turnId) return null;
5185
+ descending.push(turnId);
5186
+ }
5187
+ const nextCursor = readNonEmptyString(record.nextCursor);
5188
+ if (!nextCursor) return descending.reverse();
5189
+ cursor = nextCursor;
5190
+ }
5191
+ return null;
5192
+ }
5193
+ async function buildThreadTurnPage(rpc, threadId, turns, startTurnIndex, hasMoreOlder) {
5194
+ let metadata;
5195
+ try {
5196
+ metadata = await rpc("thread/read", { threadId, includeTurns: false });
5197
+ } catch {
5198
+ return null;
5199
+ }
5200
+ const record = asRecord(metadata);
5201
+ const thread = asRecord(record?.thread);
5202
+ if (!record || !thread) return null;
5203
+ return {
5204
+ result: { ...record, thread: { ...thread, turns } },
5205
+ startTurnIndex,
5206
+ hasMoreOlder
5207
+ };
5208
+ }
5209
+ async function readBoundedThreadTurnPage(deps, threadId, beforeTurnId, limit) {
5210
+ if (!threadId || !beforeTurnId) return null;
5211
+ const pageSize = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1;
5212
+ const ids = await readThreadTurnIds(deps.rpc, threadId);
5213
+ if (!ids) return null;
5214
+ const beforeIndex = ids.indexOf(beforeTurnId);
5215
+ if (beforeIndex < 0) {
5216
+ return await buildThreadTurnPage(deps.rpc, threadId, [], 0, false);
5217
+ }
5218
+ const startTurnIndex = Math.max(0, beforeIndex - pageSize);
5219
+ const wanted = beforeIndex - startTurnIndex;
5220
+ let turns = [];
5221
+ let boundaryTurnId = "";
5222
+ let boundaryCursor = null;
5223
+ if (wanted > 0) {
5224
+ const cursor = await resolveChainCursor(deps, threadId, ids, beforeIndex);
5225
+ if (!cursor) return null;
5226
+ let page;
5227
+ try {
5228
+ page = await deps.rpc("thread/turns/list", {
5229
+ threadId,
5230
+ cursor,
5231
+ sortDirection: "desc",
5232
+ limit: wanted,
5233
+ itemsView: "full"
5234
+ });
5235
+ } catch (error) {
5236
+ if (isTurnListUnsupportedError(error)) throw new ThreadTurnPageUnsupportedError(error);
5237
+ return null;
5238
+ }
5239
+ const record = asRecord(page);
5240
+ const data = Array.isArray(record?.data) ? record.data : null;
5241
+ if (!data || data.length !== wanted) return null;
5242
+ const ascending = [...data].reverse();
5243
+ if (readNonEmptyString(asRecord(ascending[0])?.id) !== ids[startTurnIndex]) return null;
5244
+ if (wanted > 1 && readNonEmptyString(asRecord(ascending[wanted - 1])?.id) !== ids[beforeIndex - 1]) {
5245
+ return null;
5246
+ }
5247
+ turns = ascending;
5248
+ boundaryTurnId = ids[startTurnIndex];
5249
+ boundaryCursor = record?.nextCursor ?? null;
5250
+ }
5251
+ const built = await buildThreadTurnPage(deps.rpc, threadId, turns, startTurnIndex, startTurnIndex > 0);
5252
+ if (!built) return null;
5253
+ if (boundaryTurnId) deps.chain.record(threadId, boundaryTurnId, boundaryCursor);
5254
+ return built;
5255
+ }
5256
+ async function resolveChainCursor(deps, threadId, ids, beforeIndex) {
5257
+ const anchorTurnId = ids[beforeIndex];
5258
+ if (!anchorTurnId) return null;
5259
+ const known = deps.chain.lookup(threadId, anchorTurnId);
5260
+ if (known) return known;
5261
+ const span = ids.length - beforeIndex;
5262
+ if (span < 1 || span > TURN_LIST_SERVER_PAGE_CLAMP) return null;
5263
+ let page;
5264
+ try {
5265
+ page = await deps.rpc("thread/turns/list", {
5266
+ threadId,
5267
+ limit: span,
5268
+ sortDirection: "desc",
5269
+ itemsView: "notLoaded"
5270
+ });
5271
+ } catch (error) {
5272
+ if (isTurnListUnsupportedError(error)) throw new ThreadTurnPageUnsupportedError(error);
5273
+ return null;
5274
+ }
5275
+ const record = asRecord(page);
5276
+ const data = Array.isArray(record?.data) ? record.data : null;
5277
+ const oldestTurnId = data ? readNonEmptyString(asRecord(data[data.length - 1])?.id) : "";
5278
+ const cursor = readNonEmptyString(record?.nextCursor);
5279
+ if (!data || oldestTurnId !== anchorTurnId || !cursor) return null;
5280
+ deps.chain.record(threadId, anchorTurnId, cursor);
5281
+ return cursor;
5282
+ }
5283
+
5080
5284
  // src/server/bridge/threadResumeTurnPage.ts
5081
5285
  var TURN_COUNT_PAGE_SIZE = 1e4;
5082
5286
  var TURN_COUNT_MAX_PAGES = 50;
@@ -5132,6 +5336,35 @@ async function readThreadTurnCount(rpc, threadId) {
5132
5336
  }
5133
5337
  return null;
5134
5338
  }
5339
+ var TERMINAL_TURN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "declined", "interrupted", "error"]);
5340
+ function readTurnStatus(turn) {
5341
+ const record = asRecord(turn);
5342
+ const raw = record?.status;
5343
+ const text = readNonEmptyString(raw) || readNonEmptyString(asRecord(raw)?.type);
5344
+ return text.toLowerCase();
5345
+ }
5346
+ function dropTurnsMissingFromIndex(pageData, indexTurnIds) {
5347
+ const known = new Set(indexTurnIds);
5348
+ return pageData.filter((turn) => {
5349
+ const turnId = readNonEmptyString(asRecord(turn)?.id);
5350
+ if (!turnId || known.has(turnId)) return true;
5351
+ return !TERMINAL_TURN_STATUSES.has(readTurnStatus(turn));
5352
+ });
5353
+ }
5354
+ async function readAuthoritativeTurnIds(rpc, threadId) {
5355
+ if (!threadId) return null;
5356
+ try {
5357
+ return await readThreadTurnIds(rpc, threadId);
5358
+ } catch {
5359
+ return null;
5360
+ }
5361
+ }
5362
+ function withResumePageTurns(result, turns) {
5363
+ const record = asRecord(result);
5364
+ const page = asRecord(record?.initialTurnsPage);
5365
+ if (!record || !page) return result;
5366
+ return { ...record, initialTurnsPage: { ...page, data: turns } };
5367
+ }
5135
5368
  function promoteResumeTurnPage(result, totalTurnCount) {
5136
5369
  const record = asRecord(result);
5137
5370
  const thread = asRecord(record?.thread);
@@ -5154,28 +5387,35 @@ async function resumeThreadWithTurnPage(deps, originalParams) {
5154
5387
  if (!isThreadResumeTurnPageSupported(result)) {
5155
5388
  return await deps.sendResume(originalParams);
5156
5389
  }
5157
- const pageData = readPageData(result) ?? [];
5390
+ const threadId = readNonEmptyString(asRecord(asRecord(result)?.thread)?.id);
5158
5391
  const page = asRecord(asRecord(result)?.initialTurnsPage);
5392
+ const pageData = readPageData(result) ?? [];
5393
+ const indexTurnIds = await readAuthoritativeTurnIds(deps.rpc, threadId);
5159
5394
  if (pageData.length === 0) {
5160
- const threadId = readNonEmptyString(asRecord(asRecord(result)?.thread)?.id);
5161
- const turnCount = await readThreadTurnCount(deps.rpc, threadId);
5162
- if (turnCount === null || turnCount > 0) return await deps.sendResume(originalParams);
5395
+ if (indexTurnIds === null || indexTurnIds.length > 0) return await deps.sendResume(originalParams);
5163
5396
  }
5164
- const oldestTurnId = readNonEmptyString(asRecord(pageData[pageData.length - 1])?.id);
5165
- if (oldestTurnId && deps.onTurnPageBoundary) {
5166
- deps.onTurnPageBoundary(
5167
- readNonEmptyString(asRecord(asRecord(result)?.thread)?.id),
5168
- oldestTurnId,
5169
- readNonEmptyString(page?.nextCursor) || null
5170
- );
5397
+ let promoted = result;
5398
+ let pageTurns = pageData;
5399
+ if (pageData.length > 0 && indexTurnIds !== null && indexTurnIds.length > 0) {
5400
+ const kept = dropTurnsMissingFromIndex(pageData, indexTurnIds);
5401
+ if (kept.length !== pageData.length) {
5402
+ const oldestKeptTurnId = readNonEmptyString(asRecord(kept[kept.length - 1])?.id);
5403
+ const anchored = kept.length > 0 && oldestKeptTurnId !== "" && indexTurnIds[indexTurnIds.length - kept.length] === oldestKeptTurnId;
5404
+ if (anchored) {
5405
+ promoted = withResumePageTurns(result, kept);
5406
+ pageTurns = kept;
5407
+ }
5408
+ }
5171
5409
  }
5172
- const needsCount = pageData.length > 0 && readNonEmptyString(page?.nextCursor) !== "";
5173
- let totalTurnCount = pageData.length;
5174
- if (needsCount) {
5175
- const threadId = readNonEmptyString(asRecord(asRecord(result)?.thread)?.id);
5176
- totalTurnCount = await readThreadTurnCount(deps.rpc, threadId);
5410
+ const oldestTurnId = readNonEmptyString(asRecord(pageTurns[pageTurns.length - 1])?.id);
5411
+ if (oldestTurnId && deps.onTurnPageBoundary) {
5412
+ deps.onTurnPageBoundary(threadId, oldestTurnId, readNonEmptyString(page?.nextCursor) || null);
5177
5413
  }
5178
- return promoteResumeTurnPage(result, totalTurnCount);
5414
+ const needsCount = pageTurns.length > 0 && readNonEmptyString(page?.nextCursor) !== "";
5415
+ let totalTurnCount = pageTurns.length;
5416
+ if (indexTurnIds !== null) totalTurnCount = indexTurnIds.length;
5417
+ else if (needsCount) totalTurnCount = await readThreadTurnCount(deps.rpc, threadId);
5418
+ return promoteResumeTurnPage(promoted, totalTurnCount);
5179
5419
  }
5180
5420
 
5181
5421
  // src/server/bridge/threadReadTurnPage.ts
@@ -12006,210 +12246,6 @@ async function runRpcResponsePipeline(deps, method, rpcResult) {
12006
12246
  return method === "thread/list" ? overlayExternalSessionOnThreadList(externalSessionTracker, dedupedResult) : THREAD_METHODS_WITH_TURNS.has(method) ? overlayExternalSessionOnThreadResult(externalSessionTracker, dedupedResult) : dedupedResult;
12007
12247
  }
12008
12248
 
12009
- // src/server/bridge/threadTurnPage.ts
12010
- var ThreadTurnPageUnsupportedError = class extends Error {
12011
- constructor(cause) {
12012
- super(`thread/turns/list is not supported by this app-server: ${getErrorMessage(cause, "unknown error")}`);
12013
- this.name = "ThreadTurnPageUnsupportedError";
12014
- }
12015
- };
12016
- var TURN_LIST_UNSUPPORTED_PATTERN = /-32601|not supported|not implemented|unknown variant|method not found|unknown method/i;
12017
- function isTurnListUnsupportedError(error) {
12018
- return TURN_LIST_UNSUPPORTED_PATTERN.test(getErrorMessage(error, ""));
12019
- }
12020
- var TURN_ID_PAGE_SIZE = 1e4;
12021
- var TURN_ID_MAX_PAGES = 50;
12022
- var TURN_LIST_SERVER_PAGE_CLAMP = 100;
12023
- var ThreadTurnPageCursorChain = class {
12024
- constructor(options = {}) {
12025
- this.cursorByThreadId = /* @__PURE__ */ new Map();
12026
- this.maxThreads = options.maxThreads ?? 64;
12027
- this.maxTurnsPerThread = options.maxTurnsPerThread ?? 256;
12028
- }
12029
- /** Record that `olderCursor` reaches the turns just before `oldestTurnId`. */
12030
- record(threadId, oldestTurnId, olderCursor) {
12031
- if (!threadId || !oldestTurnId) return;
12032
- const cursor = readNonEmptyString(olderCursor);
12033
- if (!cursor) return;
12034
- let perThread = this.cursorByThreadId.get(threadId);
12035
- if (!perThread) {
12036
- perThread = /* @__PURE__ */ new Map();
12037
- this.cursorByThreadId.set(threadId, perThread);
12038
- if (this.cursorByThreadId.size > this.maxThreads) {
12039
- const oldestThreadId = this.cursorByThreadId.keys().next().value;
12040
- if (typeof oldestThreadId === "string") this.cursorByThreadId.delete(oldestThreadId);
12041
- }
12042
- }
12043
- perThread.delete(oldestTurnId);
12044
- perThread.set(oldestTurnId, cursor);
12045
- if (perThread.size > this.maxTurnsPerThread) {
12046
- const oldestTurnId2 = perThread.keys().next().value;
12047
- if (typeof oldestTurnId2 === "string") perThread.delete(oldestTurnId2);
12048
- }
12049
- }
12050
- lookup(threadId, turnId) {
12051
- if (!threadId || !turnId) return null;
12052
- const perThread = this.cursorByThreadId.get(threadId);
12053
- if (!perThread) return null;
12054
- const cursor = perThread.get(turnId);
12055
- if (cursor === void 0) return null;
12056
- perThread.delete(turnId);
12057
- perThread.set(turnId, cursor);
12058
- return cursor;
12059
- }
12060
- clear(threadId) {
12061
- if (threadId) this.cursorByThreadId.delete(threadId);
12062
- else this.cursorByThreadId.clear();
12063
- }
12064
- /**
12065
- * Restore a persisted snapshot (round-136). Entries go in through `record`,
12066
- * so the caps and the least-recently-recorded eviction order apply to restored
12067
- * anchors exactly as they do to fresh ones -- the caller therefore feeds the
12068
- * snapshot oldest-first, and a snapshot older than the caps degrades to "the
12069
- * newest entries survive" rather than to a rejected file.
12070
- */
12071
- hydrate(state) {
12072
- if (!state) return;
12073
- for (const [threadId, perThread] of Object.entries(state)) {
12074
- for (const [turnId, cursor] of Object.entries(perThread ?? {})) {
12075
- this.record(threadId, turnId, cursor);
12076
- }
12077
- }
12078
- }
12079
- /** What is currently known, oldest registration first (the persist order). */
12080
- snapshot() {
12081
- const state = {};
12082
- for (const [threadId, perThread] of this.cursorByThreadId) {
12083
- const entries = {};
12084
- for (const [turnId, cursor] of perThread) entries[turnId] = cursor;
12085
- if (Object.keys(entries).length > 0) state[threadId] = entries;
12086
- }
12087
- return state;
12088
- }
12089
- };
12090
- async function readThreadTurnIds(rpc, threadId) {
12091
- if (!threadId) return null;
12092
- const descending = [];
12093
- let cursor = "";
12094
- for (let page = 0; page < TURN_ID_MAX_PAGES; page += 1) {
12095
- const params = {
12096
- threadId,
12097
- limit: TURN_ID_PAGE_SIZE,
12098
- sortDirection: "desc",
12099
- itemsView: "notLoaded"
12100
- };
12101
- if (cursor) params.cursor = cursor;
12102
- let result;
12103
- try {
12104
- result = await rpc("thread/turns/list", params);
12105
- } catch (error) {
12106
- if (isTurnListUnsupportedError(error)) throw new ThreadTurnPageUnsupportedError(error);
12107
- return null;
12108
- }
12109
- const record = asRecord(result);
12110
- if (!record || !Array.isArray(record.data)) return null;
12111
- for (const turn of record.data) {
12112
- const turnId = readNonEmptyString(asRecord(turn)?.id);
12113
- if (!turnId) return null;
12114
- descending.push(turnId);
12115
- }
12116
- const nextCursor = readNonEmptyString(record.nextCursor);
12117
- if (!nextCursor) return descending.reverse();
12118
- cursor = nextCursor;
12119
- }
12120
- return null;
12121
- }
12122
- async function buildThreadTurnPage(rpc, threadId, turns, startTurnIndex, hasMoreOlder) {
12123
- let metadata;
12124
- try {
12125
- metadata = await rpc("thread/read", { threadId, includeTurns: false });
12126
- } catch {
12127
- return null;
12128
- }
12129
- const record = asRecord(metadata);
12130
- const thread = asRecord(record?.thread);
12131
- if (!record || !thread) return null;
12132
- return {
12133
- result: { ...record, thread: { ...thread, turns } },
12134
- startTurnIndex,
12135
- hasMoreOlder
12136
- };
12137
- }
12138
- async function readBoundedThreadTurnPage(deps, threadId, beforeTurnId, limit) {
12139
- if (!threadId || !beforeTurnId) return null;
12140
- const pageSize = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1;
12141
- const ids = await readThreadTurnIds(deps.rpc, threadId);
12142
- if (!ids) return null;
12143
- const beforeIndex = ids.indexOf(beforeTurnId);
12144
- if (beforeIndex < 0) {
12145
- return await buildThreadTurnPage(deps.rpc, threadId, [], 0, false);
12146
- }
12147
- const startTurnIndex = Math.max(0, beforeIndex - pageSize);
12148
- const wanted = beforeIndex - startTurnIndex;
12149
- let turns = [];
12150
- let boundaryTurnId = "";
12151
- let boundaryCursor = null;
12152
- if (wanted > 0) {
12153
- const cursor = await resolveChainCursor(deps, threadId, ids, beforeIndex);
12154
- if (!cursor) return null;
12155
- let page;
12156
- try {
12157
- page = await deps.rpc("thread/turns/list", {
12158
- threadId,
12159
- cursor,
12160
- sortDirection: "desc",
12161
- limit: wanted,
12162
- itemsView: "full"
12163
- });
12164
- } catch (error) {
12165
- if (isTurnListUnsupportedError(error)) throw new ThreadTurnPageUnsupportedError(error);
12166
- return null;
12167
- }
12168
- const record = asRecord(page);
12169
- const data = Array.isArray(record?.data) ? record.data : null;
12170
- if (!data || data.length !== wanted) return null;
12171
- const ascending = [...data].reverse();
12172
- if (readNonEmptyString(asRecord(ascending[0])?.id) !== ids[startTurnIndex]) return null;
12173
- if (wanted > 1 && readNonEmptyString(asRecord(ascending[wanted - 1])?.id) !== ids[beforeIndex - 1]) {
12174
- return null;
12175
- }
12176
- turns = ascending;
12177
- boundaryTurnId = ids[startTurnIndex];
12178
- boundaryCursor = record?.nextCursor ?? null;
12179
- }
12180
- const built = await buildThreadTurnPage(deps.rpc, threadId, turns, startTurnIndex, startTurnIndex > 0);
12181
- if (!built) return null;
12182
- if (boundaryTurnId) deps.chain.record(threadId, boundaryTurnId, boundaryCursor);
12183
- return built;
12184
- }
12185
- async function resolveChainCursor(deps, threadId, ids, beforeIndex) {
12186
- const anchorTurnId = ids[beforeIndex];
12187
- if (!anchorTurnId) return null;
12188
- const known = deps.chain.lookup(threadId, anchorTurnId);
12189
- if (known) return known;
12190
- const span = ids.length - beforeIndex;
12191
- if (span < 1 || span > TURN_LIST_SERVER_PAGE_CLAMP) return null;
12192
- let page;
12193
- try {
12194
- page = await deps.rpc("thread/turns/list", {
12195
- threadId,
12196
- limit: span,
12197
- sortDirection: "desc",
12198
- itemsView: "notLoaded"
12199
- });
12200
- } catch (error) {
12201
- if (isTurnListUnsupportedError(error)) throw new ThreadTurnPageUnsupportedError(error);
12202
- return null;
12203
- }
12204
- const record = asRecord(page);
12205
- const data = Array.isArray(record?.data) ? record.data : null;
12206
- const oldestTurnId = data ? readNonEmptyString(asRecord(data[data.length - 1])?.id) : "";
12207
- const cursor = readNonEmptyString(record?.nextCursor);
12208
- if (!data || oldestTurnId !== anchorTurnId || !cursor) return null;
12209
- deps.chain.record(threadId, anchorTurnId, cursor);
12210
- return cursor;
12211
- }
12212
-
12213
12249
  // src/server/bridge/rollbackTurnContext.ts
12214
12250
  async function readRollbackTurnContext(appServer, threadId) {
12215
12251
  if (!threadId) return null;