replicas-engine 0.1.654 → 0.1.656

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/README.md CHANGED
@@ -28,6 +28,7 @@ Chats:
28
28
  - `GET /chats/:chatId`
29
29
  - `DELETE /chats/:chatId`
30
30
  - `GET /chats/:chatId/history`
31
+ - `GET /chats/:chatId/history-page?limit=<count>` (`limit` required)
31
32
  - `GET /chats/:chatId/slash-commands`
32
33
  - `POST /chats/:chatId/messages`
33
34
  - `POST /chats/:chatId/interrupt`
@@ -758,7 +758,7 @@ var sendChatMessageRequestSchema = z3.object({
758
758
  ]).optional()
759
759
  }).passthrough();
760
760
  function isChatMessageSender(value) {
761
- return isRecord(value) && typeof value.senderUserId === "string" && typeof value.senderEmail === "string" && typeof value.recordedAt === "string";
761
+ return isRecord(value) && (value.messageId === void 0 || typeof value.messageId === "string") && typeof value.senderUserId === "string" && typeof value.senderEmail === "string" && typeof value.recordedAt === "string";
762
762
  }
763
763
  function normalizeCodexAspTranscriptStatus(status, failed = false) {
764
764
  if (failed || status === "failed" || status === "declined") return "failed";
@@ -881,6 +881,27 @@ function applyCodexAspTranscriptDelta(current, delta) {
881
881
  turns
882
882
  };
883
883
  }
884
+ function isLatestChatHistoryPage(params) {
885
+ return params.beforeCursor === void 0 && params.beforeEvent === void 0 && params.beforeTurn === void 0;
886
+ }
887
+ function getChatHistoryPageSenders(senders, params) {
888
+ return isLatestChatHistoryPage(params) ? senders : senders?.filter((sender) => sender.messageId);
889
+ }
890
+ function parseChatHistoryCursor(value) {
891
+ if (value === void 0) return void 0;
892
+ try {
893
+ const parsed = JSON.parse(value);
894
+ if (!isRecord(parsed) || parsed.mode !== "range" && parsed.mode !== "legacy") return void 0;
895
+ const valid = (part) => part === null || typeof part === "string";
896
+ if (!valid(parsed.events) || !valid(parsed.turns)) return void 0;
897
+ return { mode: parsed.mode, events: parsed.events, turns: parsed.turns };
898
+ } catch {
899
+ return void 0;
900
+ }
901
+ }
902
+ function createChatHistoryCursor(cursor, mode = "range") {
903
+ return cursor.events === null && cursor.turns === null ? null : JSON.stringify({ mode, ...cursor });
904
+ }
884
905
  function chatHistoryPageParamsFromQuery(query) {
885
906
  const parse = (value) => {
886
907
  if (value === void 0) return void 0;
@@ -890,7 +911,8 @@ function chatHistoryPageParamsFromQuery(query) {
890
911
  return {
891
912
  limit: parse(query.limit),
892
913
  beforeEvent: parse(query.beforeEvent),
893
- beforeTurn: parse(query.beforeTurn)
914
+ beforeTurn: parse(query.beforeTurn),
915
+ beforeCursor: query.beforeCursor || void 0
894
916
  };
895
917
  }
896
918
  function getChatHistoryPageWindow(totalItems, limit, before) {
@@ -5723,6 +5745,105 @@ var EMAIL_KEYS = [
5723
5745
  var MANUAL_EMAIL_TEMPLATE_KEYS = ["product_update"];
5724
5746
  var EMAIL_TEMPLATE_KEYS = [...EMAIL_KEYS, ...MANUAL_EMAIL_TEMPLATE_KEYS];
5725
5747
 
5748
+ // ../shared/src/chat-transcript-pages.ts
5749
+ var decoder = new TextDecoder("utf-8", { fatal: true });
5750
+ var MAX_SOURCE_PAGE_BYTES = 4 * 1024 * 1024;
5751
+ var SOURCE_RANGE_BYTES = 256 * 1024;
5752
+ async function readExactRange(readRange, startByte, endByte, error) {
5753
+ const bytes = await readRange(startByte, endByte);
5754
+ if (bytes.length !== endByte - startByte) throw new Error(error);
5755
+ return bytes;
5756
+ }
5757
+ async function readJsonlPage(args) {
5758
+ let endByte = args.size;
5759
+ if (args.before === null) endByte = 0;
5760
+ else if (args.before !== void 0) {
5761
+ endByte = /^(0|[1-9]\d*)$/.test(args.before) ? Number(args.before) : Number.NaN;
5762
+ }
5763
+ if (!Number.isSafeInteger(endByte) || endByte < 0 || endByte > args.size || !Number.isSafeInteger(args.limit) || args.limit < 1) {
5764
+ throw new RangeError("Invalid JSONL page");
5765
+ }
5766
+ let startByte = endByte;
5767
+ let bytes = new Uint8Array();
5768
+ let items = [];
5769
+ while (startByte > 0 && items.length < args.limit && (endByte - startByte < MAX_SOURCE_PAGE_BYTES || bytes.indexOf(10) === -1)) {
5770
+ const nextStartByte = Math.max(0, startByte - SOURCE_RANGE_BYTES);
5771
+ const chunk = await readExactRange(
5772
+ args.readRange,
5773
+ nextStartByte,
5774
+ startByte,
5775
+ "Incomplete JSONL page read"
5776
+ );
5777
+ const combined = new Uint8Array(chunk.length + bytes.length);
5778
+ combined.set(chunk);
5779
+ combined.set(bytes, chunk.length);
5780
+ bytes = combined;
5781
+ startByte = nextStartByte;
5782
+ items = [];
5783
+ let lineStart = 0;
5784
+ for (let lineEnd = bytes.indexOf(10); lineEnd !== -1; lineEnd = bytes.indexOf(10, lineStart)) {
5785
+ if (!(startByte > 0 && lineStart === 0)) {
5786
+ try {
5787
+ const item = args.parse(JSON.parse(decoder.decode(bytes.subarray(lineStart, lineEnd))));
5788
+ if (item !== void 0) items.push({ item, startByte: startByte + lineStart });
5789
+ } catch {
5790
+ }
5791
+ }
5792
+ lineStart = lineEnd + 1;
5793
+ }
5794
+ }
5795
+ const selected = items.slice(-args.limit);
5796
+ const firstCompleteLine = bytes.indexOf(10) + 1;
5797
+ let before = 0;
5798
+ if (selected.length === args.limit) before = selected[0].startByte;
5799
+ else if (startByte > 0) before = startByte + firstCompleteLine;
5800
+ return {
5801
+ items: selected.map(({ item }) => item),
5802
+ before: before > 0 ? String(before) : null
5803
+ };
5804
+ }
5805
+ function parseChatTranscriptHistoryIndex(value) {
5806
+ if (!isRecord(value) || value.version !== 2 || !isNonNegativeInteger(value.sourceSize)) return void 0;
5807
+ if (value.codexAspTranscript === void 0) {
5808
+ return { version: 2, sourceSize: value.sourceSize };
5809
+ }
5810
+ if (!isRecord(value.codexAspTranscript) || typeof value.codexAspTranscript.threadId !== "string" || typeof value.codexAspTranscript.updatedAt !== "string" || !isNonNegativeInteger(value.codexAspTranscript.size)) return void 0;
5811
+ return {
5812
+ version: 2,
5813
+ sourceSize: value.sourceSize,
5814
+ codexAspTranscript: {
5815
+ threadId: value.codexAspTranscript.threadId,
5816
+ updatedAt: value.codexAspTranscript.updatedAt,
5817
+ size: value.codexAspTranscript.size
5818
+ }
5819
+ };
5820
+ }
5821
+ function getChatTranscriptHistorySize(index) {
5822
+ return index.codexAspTranscript?.size ?? 0;
5823
+ }
5824
+ function createChatTranscriptPages(sourceBytes) {
5825
+ const history = parseAgentEventJsonlWithCodexAspTranscript(new TextDecoder("utf-8", { fatal: true }).decode(sourceBytes));
5826
+ const bytes = new TextEncoder().encode(history.transcript?.turns.length ? `${history.transcript.turns.map((turn) => JSON.stringify(turn)).join("\n")}
5827
+ ` : "");
5828
+ return {
5829
+ bytes,
5830
+ index: {
5831
+ version: 2,
5832
+ sourceSize: sourceBytes.byteLength,
5833
+ ...history.transcript ? {
5834
+ codexAspTranscript: {
5835
+ threadId: history.transcript.threadId,
5836
+ updatedAt: history.transcript.updatedAt,
5837
+ size: bytes.byteLength
5838
+ }
5839
+ } : {}
5840
+ }
5841
+ };
5842
+ }
5843
+ function isNonNegativeInteger(value) {
5844
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
5845
+ }
5846
+
5726
5847
  // ../shared/src/object-store/types.ts
5727
5848
  var MEDIA_KIND = {
5728
5849
  IMAGE: "image",
@@ -5737,8 +5858,9 @@ function normalizeChatMessageSenders(value) {
5737
5858
  if (!Array.isArray(value)) return void 0;
5738
5859
  const senders = value.flatMap((sender) => {
5739
5860
  if (!isChatMessageSender(sender)) return [];
5740
- const { senderUserId, senderEmail, senderDisplayName, senderAvatarUrl, recordedAt } = sender;
5861
+ const { messageId, senderUserId, senderEmail, senderDisplayName, senderAvatarUrl, recordedAt } = sender;
5741
5862
  return [{
5863
+ ...typeof messageId === "string" ? { messageId } : {},
5742
5864
  senderUserId,
5743
5865
  senderEmail,
5744
5866
  ...typeof senderDisplayName === "string" ? { senderDisplayName } : {},
@@ -5760,6 +5882,7 @@ function normalizeChatTranscriptMetadata(value) {
5760
5882
  parentChatId,
5761
5883
  deletedAt,
5762
5884
  senders,
5885
+ historyIndex,
5763
5886
  captureId,
5764
5887
  captureReason,
5765
5888
  sha256
@@ -5774,6 +5897,8 @@ function normalizeChatTranscriptMetadata(value) {
5774
5897
  if (typeof deletedAt === "string" || deletedAt === null) metadata.deletedAt = deletedAt;
5775
5898
  const normalizedSenders = normalizeChatMessageSenders(senders);
5776
5899
  if (normalizedSenders) metadata.senders = normalizedSenders;
5900
+ const parsedHistoryIndex = parseChatTranscriptHistoryIndex(historyIndex);
5901
+ if (parsedHistoryIndex) metadata.historyIndex = parsedHistoryIndex;
5777
5902
  if (typeof captureId === "string") metadata.captureId = captureId;
5778
5903
  if (captureReason === "workspace_sleep") metadata.captureReason = captureReason;
5779
5904
  if (typeof sha256 === "string") metadata.sha256 = sha256;
@@ -5823,9 +5948,6 @@ var memoryGenerationManifestSchema = z6.object({
5823
5948
  rolloutSources: z6.record(z6.string(), z6.string()).optional()
5824
5949
  });
5825
5950
 
5826
- // ../shared/src/chat-transcript-pages.ts
5827
- var decoder = new TextDecoder("utf-8", { fatal: true });
5828
-
5829
5951
  // ../shared/src/skill-registry.ts
5830
5952
  var SKILL_REGISTRY_MANIFEST_VERSION = 1;
5831
5953
  function isSkillRegistryManifest(value) {
@@ -6100,7 +6222,7 @@ var DEFAULT_CODEX_ARGS = [
6100
6222
  var MIN_CODEX_CLI_VERSION = "0.144.6";
6101
6223
  var CODEX_UPGRADE_TIMEOUT_MS = 12e4;
6102
6224
  var codexCliVersionEnsured = null;
6103
- var ENGINE_PACKAGE_VERSION = "0.1.654";
6225
+ var ENGINE_PACKAGE_VERSION = "0.1.656";
6104
6226
  var INITIALIZE_METHOD = "initialize";
6105
6227
  var INITIALIZED_NOTIFICATION = "initialized";
6106
6228
  var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
@@ -6434,8 +6556,11 @@ export {
6434
6556
  isCodexAspTranscript,
6435
6557
  isCodexAspTranscriptDelta,
6436
6558
  applyCodexAspTranscriptDelta,
6559
+ isLatestChatHistoryPage,
6560
+ getChatHistoryPageSenders,
6561
+ parseChatHistoryCursor,
6562
+ createChatHistoryCursor,
6437
6563
  chatHistoryPageParamsFromQuery,
6438
- getChatHistoryPageWindow,
6439
6564
  paginateChatHistory,
6440
6565
  CODEX_AUTH_ENV_KEYS,
6441
6566
  codexAuthEnvFromResponse,
@@ -6464,6 +6589,10 @@ export {
6464
6589
  parseLatestCodexAspTranscriptTurn,
6465
6590
  parseAgentEvents,
6466
6591
  parseDisplayMessages,
6592
+ isAgentBackendEvent,
6593
+ readJsonlPage,
6594
+ getChatTranscriptHistorySize,
6595
+ createChatTranscriptPages,
6467
6596
  parseChatTranscriptArtifact,
6468
6597
  MEMORY_ROOT,
6469
6598
  MEMORY_SUMMARY_FILENAME,
@@ -7,7 +7,7 @@ import {
7
7
  headlessAgentRequestSchema,
8
8
  putPresignedFile,
9
9
  recoverCompletedTurn
10
- } from "./chunk-DDFRCI5Q.js";
10
+ } from "./chunk-KCMDTZ5H.js";
11
11
 
12
12
  // src/headless-agent.ts
13
13
  import { createHash } from "crypto";
package/dist/src/index.js CHANGED
@@ -104,7 +104,9 @@ import {
104
104
  coerceBackgroundTaskPayload,
105
105
  coerceClaudeResultPayload,
106
106
  createAcceptedUserMessageEvent,
107
+ createChatHistoryCursor,
107
108
  createChatRequestSchema,
109
+ createChatTranscriptPages,
108
110
  createErrorResult,
109
111
  createProviderSlashCommand,
110
112
  createSuccessResult,
@@ -119,7 +121,8 @@ import {
119
121
  extractToolResultText,
120
122
  findGitCommitSignals,
121
123
  findPrMergeSignals,
122
- getChatHistoryPageWindow,
124
+ getChatHistoryPageSenders,
125
+ getChatTranscriptHistorySize,
123
126
  getClaudeModelContextWindow,
124
127
  getCodexAspTurnResponse,
125
128
  getDeepseekAssistantMessageText,
@@ -132,6 +135,7 @@ import {
132
135
  gitIdentityConfigCommands,
133
136
  hasChatStarted,
134
137
  imageContentToUserMessageImages,
138
+ isAgentBackendEvent,
135
139
  isAgentChatMcpActivityRecord,
136
140
  isAgentChatSkillActivityRecord,
137
141
  isAgentChatTurnActivityRecord,
@@ -143,6 +147,7 @@ import {
143
147
  isCodexAuthError,
144
148
  isDefaultChat,
145
149
  isGitHubUrl,
150
+ isLatestChatHistoryPage,
146
151
  isRecord,
147
152
  isSkillRegistryManifest,
148
153
  isTerminalBackgroundTaskStatus,
@@ -158,6 +163,7 @@ import {
158
163
  paginateEngineLogContent,
159
164
  parseAgentEventJsonlWithCodexAspTranscript,
160
165
  parseAgentEvents,
166
+ parseChatHistoryCursor,
161
167
  parseChatTranscriptArtifact,
162
168
  parseCodeHostPrUrl,
163
169
  parseDisplayMessages,
@@ -169,6 +175,7 @@ import {
169
175
  percentage,
170
176
  putPresignedFile,
171
177
  raceWithTimeout,
178
+ readJsonlPage,
172
179
  readReplicasRuntimeEnv,
173
180
  recoverCompletedTurn,
174
181
  resolveWarmHookConfig,
@@ -176,7 +183,7 @@ import {
176
183
  serializeCanvasContentResponse,
177
184
  shellQuotePosix,
178
185
  stripAgentDiagnosticErrors
179
- } from "./chunk-DDFRCI5Q.js";
186
+ } from "./chunk-KCMDTZ5H.js";
180
187
 
181
188
  // src/index.ts
182
189
  import { serve } from "@hono/node-server";
@@ -4147,11 +4154,11 @@ var CodexHistoryFile = class {
4147
4154
  }
4148
4155
  filePath;
4149
4156
  writeLock = new AsyncLock();
4150
- eventLineIndex = null;
4151
4157
  /** Best-effort ordered append; failures must not disrupt the turn. */
4152
4158
  append(event) {
4153
4159
  void this.writeLock.run(
4154
- () => appendFile2(this.filePath, JSON.stringify(event) + "\n", "utf-8").catch((error) => {
4160
+ () => appendFile2(this.filePath, `${JSON.stringify(event)}
4161
+ `).catch((error) => {
4155
4162
  console.error("[CodexHistoryFile] Failed to append event:", error);
4156
4163
  })
4157
4164
  );
@@ -4172,85 +4179,51 @@ var CodexHistoryFile = class {
4172
4179
  }
4173
4180
  async loadEventsPage(page) {
4174
4181
  if (page.limit === void 0) {
4175
- const history2 = await this.load();
4176
- return { events: history2.events, eventsStartIndex: 0, totalEvents: history2.events.length };
4182
+ const history = await this.load();
4183
+ return { events: history.events };
4184
+ }
4185
+ const cursor = parseChatHistoryCursor(page.beforeCursor);
4186
+ if (page.beforeCursor !== void 0 && cursor?.mode !== "range") throw new RangeError("Invalid chat history cursor");
4187
+ if (cursor?.events === null) {
4188
+ return {
4189
+ events: [],
4190
+ beforeCursor: createChatHistoryCursor({
4191
+ events: null,
4192
+ turns: cursor.turns
4193
+ })
4194
+ };
4177
4195
  }
4178
4196
  let file;
4179
4197
  try {
4180
4198
  file = await open(this.filePath, "r");
4181
4199
  } catch (error) {
4182
- if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) {
4183
- console.error("[CodexHistoryFile] Failed to open history file:", error);
4200
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
4201
+ return { events: [], beforeCursor: null };
4184
4202
  }
4185
- return { events: [], eventsStartIndex: 0, totalEvents: 0 };
4203
+ throw error;
4186
4204
  }
4187
4205
  try {
4188
- const stats = await file.stat();
4189
- let index = this.eventLineIndex;
4190
- if (!index || index.size !== stats.size || index.mtimeMs !== stats.mtimeMs) {
4191
- const starts = stats.size > 0 ? [0] : [];
4192
- const buffer = Buffer.allocUnsafe(64 * 1024);
4193
- let position = 0;
4194
- while (position < stats.size) {
4195
- const { bytesRead: bytesRead2 } = await file.read(
4196
- buffer,
4197
- 0,
4198
- Math.min(buffer.length, stats.size - position),
4199
- position
4200
- );
4201
- if (bytesRead2 === 0) break;
4202
- for (let offset = 0; offset < bytesRead2; offset += 1) {
4203
- const next = position + offset + 1;
4204
- if (buffer[offset] === 10 && next < stats.size) starts.push(next);
4205
- }
4206
- position += bytesRead2;
4207
- }
4208
- index = { size: stats.size, mtimeMs: stats.mtimeMs, starts };
4209
- this.eventLineIndex = index;
4210
- }
4211
- const totalEvents2 = index.starts.length;
4212
- const { startIndex: eventsStartIndex2, endIndex: eventsEnd2 } = getChatHistoryPageWindow(
4213
- totalEvents2,
4214
- page.limit,
4215
- page.beforeEvent
4216
- );
4217
- const startByte = index.starts[eventsStartIndex2] ?? index.size;
4218
- const endByte = index.starts[eventsEnd2] ?? index.size;
4219
- const content = Buffer.allocUnsafe(endByte - startByte);
4220
- let bytesRead = 0;
4221
- while (bytesRead < content.length) {
4222
- const result = await file.read(
4223
- content,
4224
- bytesRead,
4225
- content.length - bytesRead,
4226
- startByte + bytesRead
4227
- );
4228
- if (result.bytesRead === 0) break;
4229
- bytesRead += result.bytesRead;
4230
- }
4231
- if (bytesRead === content.length) {
4232
- const parsed = parseAgentEventJsonlWithCodexAspTranscript(content.toString("utf-8"));
4233
- if (!parsed.transcript && parsed.events.length === eventsEnd2 - eventsStartIndex2) {
4234
- return { events: parsed.events, eventsStartIndex: eventsStartIndex2, totalEvents: totalEvents2 };
4235
- }
4236
- }
4237
- } catch (error) {
4238
- console.error("[CodexHistoryFile] Failed to load history page:", error);
4206
+ const result = await readJsonlPage({
4207
+ size: (await file.stat()).size,
4208
+ limit: page.limit,
4209
+ before: cursor?.events,
4210
+ readRange: async (startByte, endByte) => {
4211
+ const bytes = Buffer.allocUnsafe(endByte - startByte);
4212
+ const { bytesRead } = await file.read(bytes, 0, bytes.length, startByte);
4213
+ return bytes.subarray(0, bytesRead);
4214
+ },
4215
+ parse: (value) => isAgentBackendEvent(value) && value.type !== CODEX_ASP_TRANSCRIPT_UPDATED_EVENT_TYPE ? value : void 0
4216
+ });
4217
+ return {
4218
+ events: result.items,
4219
+ beforeCursor: createChatHistoryCursor({
4220
+ events: result.before,
4221
+ turns: cursor?.turns ?? null
4222
+ })
4223
+ };
4239
4224
  } finally {
4240
4225
  await file.close();
4241
4226
  }
4242
- const history = await this.load();
4243
- const totalEvents = history.events.length;
4244
- const { startIndex: eventsStartIndex, endIndex: eventsEnd } = getChatHistoryPageWindow(
4245
- totalEvents,
4246
- page.limit,
4247
- page.beforeEvent
4248
- );
4249
- return {
4250
- events: history.events.slice(eventsStartIndex, eventsEnd),
4251
- eventsStartIndex,
4252
- totalEvents
4253
- };
4254
4227
  }
4255
4228
  };
4256
4229
 
@@ -5721,6 +5694,7 @@ var DEFAULT_MODEL = DEFAULT_CODEX_MODEL;
5721
5694
  var THREAD_START_METHOD = "thread/start";
5722
5695
  var THREAD_RESUME_METHOD = "thread/resume";
5723
5696
  var THREAD_READ_METHOD = "thread/read";
5697
+ var THREAD_TURNS_LIST_METHOD = "thread/turns/list";
5724
5698
  var THREAD_GOAL_SET_METHOD = "thread/goal/set";
5725
5699
  var THREAD_GOAL_GET_METHOD = "thread/goal/get";
5726
5700
  var THREAD_GOAL_CLEAR_METHOD = "thread/goal/clear";
@@ -6049,8 +6023,11 @@ function formatTurnFailure(turn) {
6049
6023
  return parts.join(" ");
6050
6024
  }
6051
6025
  function threadToAspTranscript(thread) {
6026
+ return turnsToAspTranscript(thread.id, timestampFromSeconds(thread.updatedAt), thread.turns);
6027
+ }
6028
+ function turnsToAspTranscript(threadId, updatedAt, sourceTurns) {
6052
6029
  let sequence = 0;
6053
- const turns = thread.turns.map((turn, index) => ({ turn, index })).sort((a, b) => (a.turn.startedAt ?? a.turn.completedAt ?? Number.MAX_SAFE_INTEGER) - (b.turn.startedAt ?? b.turn.completedAt ?? Number.MAX_SAFE_INTEGER) || a.index - b.index).map(({ turn }) => {
6030
+ const turns = sourceTurns.map((turn, index) => ({ turn, index })).sort((a, b) => (a.turn.startedAt ?? a.turn.completedAt ?? Number.MAX_SAFE_INTEGER) - (b.turn.startedAt ?? b.turn.completedAt ?? Number.MAX_SAFE_INTEGER) || a.index - b.index).map(({ turn }) => {
6054
6031
  const startedAt = timestampFromSeconds(turn.startedAt);
6055
6032
  const completedAt = turn.completedAt === null ? null : timestampFromSeconds(turn.completedAt);
6056
6033
  const itemTimestamp = completedAt ?? startedAt;
@@ -6080,8 +6057,8 @@ function threadToAspTranscript(thread) {
6080
6057
  };
6081
6058
  });
6082
6059
  return {
6083
- threadId: thread.id,
6084
- updatedAt: timestampFromSeconds(thread.updatedAt),
6060
+ threadId,
6061
+ updatedAt,
6085
6062
  turns
6086
6063
  };
6087
6064
  }
@@ -6403,7 +6380,7 @@ async function readCodexAspThreadHistory(threadId) {
6403
6380
  }
6404
6381
  }
6405
6382
  for (const entry of entries) {
6406
- if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
6383
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl") || entry.name.endsWith(".pages.jsonl")) continue;
6407
6384
  const history = await new CodexHistoryFile(join19(CODEX_HISTORY_DIR, entry.name)).load();
6408
6385
  const transcript = history.transcriptsByThreadId.get(threadId);
6409
6386
  if (transcript) {
@@ -6452,7 +6429,6 @@ var CodexAspManager = class extends CodingAgentManager {
6452
6429
  activeTurnId = null;
6453
6430
  threadAttached = false;
6454
6431
  historyFile;
6455
- historyEvents = [];
6456
6432
  codexAspTranscript = null;
6457
6433
  lastEmittedTranscripts = /* @__PURE__ */ new Map();
6458
6434
  codexAspSequence = 0;
@@ -6474,9 +6450,12 @@ var CodexAspManager = class extends CodingAgentManager {
6474
6450
  this.initializeManager(this.processMessageInternal.bind(this));
6475
6451
  }
6476
6452
  async initialize() {
6453
+ if (this.initialSessionId) {
6454
+ this.currentThreadId = this.initialSessionId;
6455
+ return;
6456
+ }
6477
6457
  const replayed = await this.historyFile?.load();
6478
6458
  if (replayed) {
6479
- this.historyEvents.push(...replayed.events);
6480
6459
  if (replayed.transcript) {
6481
6460
  this.mergeTranscriptSnapshot(replayed.transcript);
6482
6461
  }
@@ -6565,51 +6544,87 @@ var CodexAspManager = class extends CodingAgentManager {
6565
6544
  return true;
6566
6545
  }
6567
6546
  async getHistory(page = {}) {
6568
- if (!this.currentThreadId) {
6569
- return paginateChatHistory({ thread_id: null, events: [], goal: null }, page);
6570
- }
6571
- if (this.codexAspTranscript?.threadId === this.currentThreadId) {
6572
- if (page.beforeEvent === void 0 && page.beforeTurn === void 0) {
6573
- try {
6574
- const host = await getCodexAspHost();
6575
- await this.refreshThreadGoal(host, this.currentThreadId);
6576
- } catch {
6577
- }
6578
- }
6579
- return paginateChatHistory({
6580
- thread_id: this.currentThreadId,
6581
- events: [...this.historyEvents],
6582
- codexAspTranscript: this.codexAspTranscript,
6583
- goal: this.currentGoal
6584
- }, page);
6585
- }
6586
- try {
6547
+ await this.historyFile?.flush();
6548
+ if (page.limit === void 0) {
6549
+ const local = async () => {
6550
+ const history = await this.historyFile?.load();
6551
+ return {
6552
+ thread_id: this.currentThreadId,
6553
+ events: history?.events ?? [],
6554
+ codexAspTranscript: this.currentThreadId ? history?.transcriptsByThreadId.get(this.currentThreadId) ?? null : null,
6555
+ goal: this.currentGoal
6556
+ };
6557
+ };
6558
+ if (!this.currentThreadId) return local();
6559
+ return this.readFullNativeHistory().catch(local);
6560
+ }
6561
+ const events = await this.historyFile?.loadEventsPage(page) ?? { events: [], beforeCursor: null };
6562
+ if (!this.currentThreadId) return { thread_id: null, ...events, goal: this.currentGoal };
6563
+ const cursor = parseChatHistoryCursor(page.beforeCursor);
6564
+ if (page.beforeCursor !== void 0 && cursor?.mode !== "range") throw new RangeError("Invalid chat history cursor");
6565
+ let transcript = null;
6566
+ let turnsBefore = null;
6567
+ if (cursor?.turns !== null) {
6587
6568
  const host = await getCodexAspHost();
6588
- const [response] = await Promise.all([
6589
- host.client.request(
6590
- THREAD_READ_METHOD,
6591
- { threadId: this.currentThreadId, includeTurns: true }
6592
- ),
6593
- this.refreshThreadGoal(host, this.currentThreadId)
6594
- ]);
6595
- const transcript = this.mergeTranscriptSnapshot(threadToAspTranscript(response.thread));
6596
- if (transcript) {
6597
- transcript.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
6598
- }
6599
- return paginateChatHistory({
6600
- thread_id: this.currentThreadId,
6601
- events: [...this.historyEvents],
6602
- codexAspTranscript: transcript,
6603
- goal: this.currentGoal
6604
- }, page);
6605
- } catch {
6569
+ if (page.beforeCursor === void 0) await this.refreshThreadGoal(host, this.currentThreadId);
6570
+ const turns = await this.readNativeTurnsPage(page.limit, cursor?.turns, host);
6571
+ transcript = turns.transcript;
6572
+ turnsBefore = turns.before;
6606
6573
  }
6607
- return paginateChatHistory({
6574
+ const eventCursor = parseChatHistoryCursor(events.beforeCursor ?? void 0);
6575
+ return {
6576
+ thread_id: this.currentThreadId,
6577
+ ...events,
6578
+ codexAspTranscript: transcript,
6579
+ goal: this.currentGoal,
6580
+ beforeCursor: createChatHistoryCursor({
6581
+ events: eventCursor?.events ?? null,
6582
+ turns: turnsBefore
6583
+ })
6584
+ };
6585
+ }
6586
+ async readFullNativeHistory() {
6587
+ if (!this.currentThreadId) {
6588
+ return { thread_id: null, events: [], goal: this.currentGoal };
6589
+ }
6590
+ const host = await getCodexAspHost();
6591
+ const [response, history] = await Promise.all([
6592
+ host.client.request(
6593
+ THREAD_READ_METHOD,
6594
+ { threadId: this.currentThreadId, includeTurns: true }
6595
+ ),
6596
+ this.historyFile?.load()
6597
+ ]);
6598
+ const transcript = this.mergeTranscriptSnapshot(threadToAspTranscript(response.thread));
6599
+ return {
6608
6600
  thread_id: this.currentThreadId,
6609
- events: [...this.historyEvents],
6610
- codexAspTranscript: this.codexAspTranscript,
6601
+ events: history?.events ?? [],
6602
+ codexAspTranscript: transcript,
6611
6603
  goal: this.currentGoal
6612
- }, page);
6604
+ };
6605
+ }
6606
+ async readNativeTurnsPage(limit, cursor, host) {
6607
+ if (!this.currentThreadId) throw new Error("A thread is required for native turn pagination");
6608
+ const response = await host.client.request(
6609
+ THREAD_TURNS_LIST_METHOD,
6610
+ {
6611
+ threadId: this.currentThreadId,
6612
+ cursor: cursor ?? null,
6613
+ limit,
6614
+ sortDirection: "desc",
6615
+ itemsView: "full"
6616
+ }
6617
+ );
6618
+ const transcript = turnsToAspTranscript(
6619
+ this.currentThreadId,
6620
+ (/* @__PURE__ */ new Date()).toISOString(),
6621
+ response.data
6622
+ );
6623
+ if (cursor === void 0) {
6624
+ this.codexAspTranscript = transcript;
6625
+ this.syncTranscriptSequence(transcript);
6626
+ }
6627
+ return { transcript, before: response.nextCursor };
6613
6628
  }
6614
6629
  getGoal() {
6615
6630
  return this.currentGoal;
@@ -7538,7 +7553,6 @@ var CodexAspManager = class extends CodingAgentManager {
7538
7553
  return event;
7539
7554
  }
7540
7555
  trackHistoryEvent(event) {
7541
- this.historyEvents.push(event);
7542
7556
  this.historyFile?.append(event);
7543
7557
  }
7544
7558
  emitTranscriptUpdated(threadId, options = {}) {
@@ -10800,7 +10814,7 @@ async function reconcileCanvasItems(filenames) {
10800
10814
  // src/services/upload-chat-transcripts.ts
10801
10815
  import { createReadStream } from "fs";
10802
10816
  import { createHash as createHash2 } from "crypto";
10803
- import { readdir as readdir8, readFile as readFile14, stat as stat4 } from "fs/promises";
10817
+ import { readFile as readFile14, readdir as readdir8, stat as stat4 } from "fs/promises";
10804
10818
  import { basename as basename2, join as join27 } from "path";
10805
10819
 
10806
10820
  // src/services/chat/chat-senders.ts
@@ -10859,6 +10873,7 @@ async function flushAllChatTranscripts(chatsById = /* @__PURE__ */ new Map(), ca
10859
10873
  async function uploadChatTranscript(chatId, filePath, chat, capture) {
10860
10874
  const { size } = await stat4(filePath);
10861
10875
  if (size === 0) return null;
10876
+ const historyPages = createChatTranscriptPages(await readFile14(filePath));
10862
10877
  const metadata = chat ? {
10863
10878
  provider: chat.provider,
10864
10879
  credential: ENGINE_ENV.REPLICAS_AGENT_CREDENTIALS[chat.provider],
@@ -10882,6 +10897,7 @@ async function uploadChatTranscript(chatId, filePath, chat, capture) {
10882
10897
  } catch (error) {
10883
10898
  if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) throw error;
10884
10899
  }
10900
+ if (historyPages.index.codexAspTranscript) metadata.historyIndex = historyPages.index;
10885
10901
  const uploadRequest = { chatId, size, metadata };
10886
10902
  const prepareResponse = await monolithRequest("/v1/engine/chat-transcripts/upload-url", {
10887
10903
  body: uploadRequest
@@ -10890,11 +10906,25 @@ async function uploadChatTranscript(chatId, filePath, chat, capture) {
10890
10906
  throw new Error(`prepare failed: ${prepareResponse.status} ${await prepareResponse.text()}`);
10891
10907
  }
10892
10908
  const prepareBody = await prepareResponse.json();
10893
- if (!isRecord(prepareBody) || typeof prepareBody.uploadUrl !== "string" && prepareBody.uploadUrl !== null) {
10909
+ if (!isRecord(prepareBody) || typeof prepareBody.uploadUrl !== "string" && prepareBody.uploadUrl !== null || prepareBody.historyPagesUploadUrl !== void 0 && typeof prepareBody.historyPagesUploadUrl !== "string") {
10894
10910
  throw new Error("prepare failed: invalid response");
10895
10911
  }
10896
10912
  if (prepareBody.uploadUrl === null) return null;
10897
- await putPresignedFile(prepareBody.uploadUrl, filePath, size, "application/x-ndjson");
10913
+ const uploads = [putPresignedFile(prepareBody.uploadUrl, filePath, size, "application/x-ndjson")];
10914
+ const historyPagesSize = getChatTranscriptHistorySize(historyPages.index);
10915
+ if (historyPagesSize > 0) {
10916
+ if (typeof prepareBody.historyPagesUploadUrl !== "string") {
10917
+ throw new Error("prepare failed: missing history pages upload URL");
10918
+ }
10919
+ uploads.push(fetch(prepareBody.historyPagesUploadUrl, {
10920
+ method: "PUT",
10921
+ headers: { "content-type": "application/x-ndjson" },
10922
+ body: new Uint8Array(historyPages.bytes).buffer
10923
+ }).then((response) => {
10924
+ if (!response.ok) throw new Error(`history pages upload failed: ${response.status}`);
10925
+ }));
10926
+ }
10927
+ await Promise.all(uploads);
10898
10928
  const finalizeResponse = await monolithRequest("/v1/engine/chat-transcripts/finalize", {
10899
10929
  body: uploadRequest
10900
10930
  });
@@ -11310,10 +11340,11 @@ var ChatService = class {
11310
11340
  }
11311
11341
  chat.persisted.acceptedSendResponses = Object.fromEntries(chat.acceptedSendResponses);
11312
11342
  }
11343
+ const submittedAt = request.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString();
11313
11344
  const acceptedEvent = createAcceptedUserMessageEvent(
11314
11345
  request.message,
11315
11346
  result.messageId,
11316
- request.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
11347
+ submittedAt,
11317
11348
  request.images
11318
11349
  );
11319
11350
  if (!chat.pendingMessageIds.includes(result.messageId)) {
@@ -11329,11 +11360,12 @@ var ChatService = class {
11329
11360
  let recordedSender;
11330
11361
  if (request.senderUserId && request.senderEmail) {
11331
11362
  recordedSender = {
11363
+ messageId: result.messageId,
11332
11364
  senderUserId: request.senderUserId,
11333
11365
  senderEmail: request.senderEmail,
11334
11366
  ...request.senderDisplayName ? { senderDisplayName: request.senderDisplayName } : {},
11335
11367
  ...request.senderAvatarUrl ? { senderAvatarUrl: request.senderAvatarUrl } : {},
11336
- recordedAt: request.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString()
11368
+ recordedAt: submittedAt
11337
11369
  };
11338
11370
  await this.appendSender(chatId, recordedSender);
11339
11371
  }
@@ -11362,15 +11394,11 @@ var ChatService = class {
11362
11394
  }
11363
11395
  }
11364
11396
  async readSenders(chatId) {
11365
- try {
11366
- return parseChatMessageSendersJsonl(await readFile15(chatMessageSendersFilePath(chatId), "utf-8"));
11367
- } catch (error) {
11368
- if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
11369
- return [];
11370
- }
11397
+ return readFile15(chatMessageSendersFilePath(chatId), "utf-8").then(parseChatMessageSendersJsonl).catch((error) => {
11398
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return [];
11371
11399
  console.error("[ChatService] Failed to read sender records:", error);
11372
11400
  return [];
11373
- }
11401
+ });
11374
11402
  }
11375
11403
  async interrupt(chatId) {
11376
11404
  const chat = this.requireChat(chatId);
@@ -11592,7 +11620,12 @@ var ChatService = class {
11592
11620
  return descendants;
11593
11621
  }
11594
11622
  async deleteHistoryFile(persisted) {
11595
- await rm2(join29(HISTORY_DIR_BY_PROVIDER[persisted.provider], `${persisted.id}.jsonl`), { force: true });
11623
+ const historyPath = join29(HISTORY_DIR_BY_PROVIDER[persisted.provider], `${persisted.id}.jsonl`);
11624
+ await Promise.all([
11625
+ historyPath,
11626
+ `${historyPath}.pages.jsonl`,
11627
+ `${historyPath}.pages.index.json`
11628
+ ].map((path6) => rm2(path6, { force: true })));
11596
11629
  await rm2(chatMessageSendersFilePath(persisted.id), { force: true });
11597
11630
  }
11598
11631
  async getChatHistory(chatId, page = {}) {
@@ -11606,10 +11639,23 @@ var ChatService = class {
11606
11639
  chat.acceptedUserEvents.delete(messageId);
11607
11640
  }
11608
11641
  }
11609
- const isLatestPage = page.beforeEvent === void 0 && page.beforeTurn === void 0;
11642
+ const isLatestPage = isLatestChatHistoryPage(page);
11610
11643
  const queuedMessageIds = new Set(chat.provider.getQueue().map((message) => message.id));
11611
11644
  const acceptedEvents = (isLatestPage ? [...chat.acceptedUserEvents.entries()] : []).filter(([messageId]) => messageId === chat.activeMessageId || !chat.hasActiveTurn && chat.pendingMessageIds[0] === messageId && !queuedMessageIds.has(messageId)).map(([, acceptedEvent]) => acceptedEvent).filter((acceptedEvent) => !acceptedEventInCodexTranscript(acceptedEvent, history.codexAspTranscript) && !history.events.some((event) => isSameAcceptedUserEvent(event, acceptedEvent)));
11612
11645
  const events = [...history.events, ...acceptedEvents].sort((a, b) => getEventTimestampMs(a) - getEventTimestampMs(b));
11646
+ if (page.limit !== void 0 && "beforeCursor" in history) {
11647
+ const cursor = parseChatHistoryCursor(history.beforeCursor ?? void 0);
11648
+ return {
11649
+ ...history,
11650
+ events,
11651
+ goal: history.goal ?? chat.provider.getGoal?.() ?? null,
11652
+ senders: getChatHistoryPageSenders(senders, page),
11653
+ beforeCursor: createChatHistoryCursor({
11654
+ events: cursor?.events ?? null,
11655
+ turns: cursor?.turns ?? null
11656
+ })
11657
+ };
11658
+ }
11613
11659
  if (history.eventsStartIndex !== void 0) {
11614
11660
  return {
11615
11661
  ...history,
@@ -12940,6 +12986,22 @@ function jsonError(message, details) {
12940
12986
  }
12941
12987
  function createV1Routes(deps) {
12942
12988
  const app2 = new Hono();
12989
+ const loadChatHistory = async (c, chatId, page, failureMessage, unexpectedStatus, legacy = false) => {
12990
+ try {
12991
+ const history = await deps.chatService.getChatHistory(chatId, legacy && page.limit !== void 0 ? {} : page);
12992
+ if (!legacy) return c.json(history);
12993
+ return c.json(paginateChatHistory({
12994
+ ...history,
12995
+ senders: getChatHistoryPageSenders(history.senders, page)
12996
+ }, page));
12997
+ } catch (error) {
12998
+ const status = error instanceof ChatNotFoundError ? 404 : unexpectedStatus;
12999
+ return c.json(jsonError(
13000
+ failureMessage,
13001
+ error instanceof Error ? error.message : "Unknown error"
13002
+ ), status);
13003
+ }
13004
+ };
12943
13005
  app2.get("/events", async () => {
12944
13006
  const encoder = new TextEncoder();
12945
13007
  let unsubscribe = null;
@@ -13059,19 +13121,23 @@ function createV1Routes(deps) {
13059
13121
  return c.json(jsonError("Failed to restore chat", details), 500);
13060
13122
  }
13061
13123
  });
13062
- app2.get("/chats/:chatId/history", async (c) => {
13063
- try {
13064
- const history = await deps.chatService.getChatHistory(
13065
- c.req.param("chatId"),
13066
- chatHistoryPageParamsFromQuery(c.req.query())
13067
- );
13068
- return c.json(history);
13069
- } catch (error) {
13070
- if (error instanceof ChatNotFoundError) {
13071
- return c.json(jsonError("Failed to load chat history", error.message), 404);
13072
- }
13073
- return c.json(jsonError("Failed to load chat history", error instanceof Error ? error.message : "Unknown error"), 404);
13124
+ app2.get("/chats/:chatId/history", (c) => loadChatHistory(
13125
+ c,
13126
+ c.req.param("chatId"),
13127
+ chatHistoryPageParamsFromQuery(c.req.query()),
13128
+ "Failed to load chat history",
13129
+ 404,
13130
+ true
13131
+ ));
13132
+ app2.get("/chats/:chatId/history-page", async (c) => {
13133
+ const page = chatHistoryPageParamsFromQuery(c.req.query());
13134
+ if (page.limit === void 0) {
13135
+ return c.json(jsonError("Failed to load chat history page", "limit is required"), 400);
13136
+ }
13137
+ if (page.beforeCursor !== void 0 && !parseChatHistoryCursor(page.beforeCursor)) {
13138
+ return c.json(jsonError("Failed to load chat history page", "invalid cursor"), 400);
13074
13139
  }
13140
+ return loadChatHistory(c, c.req.param("chatId"), page, "Failed to load chat history page", 500);
13075
13141
  });
13076
13142
  app2.get("/chats/:chatId/slash-commands", async (c) => {
13077
13143
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.654",
3
+ "version": "0.1.656",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -38,8 +38,8 @@
38
38
  "@anthropic-ai/claude-agent-sdk": "0.3.219",
39
39
  "@connectrpc/connect-node": "1.7.0",
40
40
  "@cursor/sdk": "1.0.19",
41
- "@deepseek-ai/dsh": "0.1.0-rc.6",
42
- "@deepseek-ai/dsh-host-apiproxy": "0.1.0-rc.6",
41
+ "@deepseek-ai/dsh": "0.1.0-rc.7",
42
+ "@deepseek-ai/dsh-host-apiproxy": "0.1.0-rc.7",
43
43
  "@earendil-works/pi-ai": "0.79.10",
44
44
  "@earendil-works/pi-coding-agent": "0.79.0",
45
45
  "@hono/node-server": "^1.19.5",
@@ -52,7 +52,7 @@
52
52
  "zod": "^4.0.0"
53
53
  },
54
54
  "optionalDependencies": {
55
- "node-pty": "1.2.0-beta.14"
55
+ "node-pty": "1.2.0-beta.15"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@replicas/codex-asp-types": "file:../codex-asp-types",