@nyssance/codex-acp-v2 0.7.9 → 0.7.10

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 (3) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.js +313 -53
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.10 — 2026-10-11
4
+
5
+ ### Fixed
6
+
7
+ - Restore image attachments, file references, and readable conversation titles in
8
+ history instead of exposing serialized attachment wrappers or Markdown links.
9
+ - Load native local image attachments with bounded reads and preserve unavailable
10
+ image placeholders when clipboard files have expired.
11
+ - Parse single and multiple questionnaire replies into question and answer metadata
12
+ for both live messages and history, preserving malformed input as ordinary text.
13
+ - Report user send times, assistant completion times, and live work duration through
14
+ message metadata; keep native historical total duration distinct from work duration.
15
+ - Preserve interruption metadata when replaying history and when live turns stop.
16
+
3
17
  ## 0.7.9 — 2026-10-10
4
18
 
5
19
  ### Added
package/dist/index.js CHANGED
@@ -22168,7 +22168,7 @@ var package_default = {
22168
22168
  publishConfig: {
22169
22169
  access: "public"
22170
22170
  },
22171
- version: "0.7.9",
22171
+ version: "0.7.10",
22172
22172
  description: "Native ACP v2 agent for the OpenAI Codex app-server",
22173
22173
  packageManager: "bun@1.4.0",
22174
22174
  engines: {
@@ -23776,8 +23776,100 @@ function mcpStartupFailed(serverName, message) {
23776
23776
  }
23777
23777
 
23778
23778
  // src/codex/sessionConfig.ts
23779
- import { pathToFileURL } from "url";
23779
+ import { pathToFileURL as pathToFileURL2 } from "url";
23780
23780
  import path3 from "path";
23781
+
23782
+ // src/codex/userImages.ts
23783
+ import { constants } from "fs";
23784
+ import { open } from "fs/promises";
23785
+ import { fileURLToPath, pathToFileURL } from "url";
23786
+ var nativeImages = new WeakMap;
23787
+ var MAX_IMAGE_BYTES = 32 * 1024 * 1024;
23788
+ function localImageContent(path3) {
23789
+ const block = { type: "resource_link", name: "Image", uri: path3.startsWith("file:") ? path3 : pathToFileURL(path3).href };
23790
+ nativeImages.set(block, path3);
23791
+ return block;
23792
+ }
23793
+ async function hydrateUserImages(content, signal) {
23794
+ const result = [];
23795
+ for (const block of content) {
23796
+ signal.throwIfAborted();
23797
+ const path3 = nativeImages.get(block);
23798
+ result.push(path3 === undefined ? block : await readImage(path3));
23799
+ }
23800
+ signal.throwIfAborted();
23801
+ return result;
23802
+ }
23803
+ async function readImage(path3) {
23804
+ try {
23805
+ const file2 = await open(path3.startsWith("file:") ? fileURLToPath(path3) : path3, constants.O_RDONLY | constants.O_NONBLOCK);
23806
+ try {
23807
+ const stat = await file2.stat();
23808
+ if (!stat.isFile() || stat.size === 0 || stat.size > MAX_IMAGE_BYTES)
23809
+ return unavailable();
23810
+ const data = Buffer.alloc(stat.size);
23811
+ let offset = 0;
23812
+ while (offset < data.length) {
23813
+ const { bytesRead } = await file2.read(data, offset, data.length - offset, offset);
23814
+ if (bytesRead === 0)
23815
+ return unavailable();
23816
+ offset += bytesRead;
23817
+ }
23818
+ const mimeType = imageMimeType(data);
23819
+ return mimeType ? { type: "image", mimeType, data: data.toString("base64") } : unavailable();
23820
+ } finally {
23821
+ await file2.close();
23822
+ }
23823
+ } catch {
23824
+ return unavailable();
23825
+ }
23826
+ }
23827
+ function unavailable() {
23828
+ return { type: "resource_link", name: "Image unavailable", uri: "codex-image:unavailable", _meta: { codex: { imageUnavailable: true } } };
23829
+ }
23830
+ function imageMimeType(data) {
23831
+ if (data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
23832
+ return "image/png";
23833
+ if (data[0] === 255 && data[1] === 216 && data[2] === 255)
23834
+ return "image/jpeg";
23835
+ if (/^GIF8[79]a$/.test(data.toString("ascii", 0, 6)))
23836
+ return "image/gif";
23837
+ if (data.toString("ascii", 0, 4) === "RIFF" && data.toString("ascii", 8, 12) === "WEBP")
23838
+ return "image/webp";
23839
+ if (data.toString("ascii", 0, 2) === "BM")
23840
+ return "image/bmp";
23841
+ return null;
23842
+ }
23843
+
23844
+ // src/codex/questionReplies.ts
23845
+ function questionReplyContent(text) {
23846
+ const envelope = /^\s*<send_user_message_question_reply>\s*([\s\S]*?)\s*<\/send_user_message_question_reply>\s*$/.exec(text);
23847
+ if (!envelope)
23848
+ return null;
23849
+ let replies;
23850
+ try {
23851
+ replies = JSON.parse(envelope[1]);
23852
+ } catch {
23853
+ return null;
23854
+ }
23855
+ if (!Array.isArray(replies) || replies.length === 0)
23856
+ return null;
23857
+ const content = [];
23858
+ for (const reply of replies) {
23859
+ if (reply === null || typeof reply !== "object" || Array.isArray(reply) || typeof reply.questionItemId !== "string" || typeof reply.question !== "string" || typeof reply.answer !== "string")
23860
+ return null;
23861
+ const { questionItemId, question, answer } = reply;
23862
+ content.push({
23863
+ type: "text",
23864
+ text: `${question}
23865
+ ${answer}`,
23866
+ _meta: { codex: { questionReply: { questionItemId, question, answer } } }
23867
+ });
23868
+ }
23869
+ return content;
23870
+ }
23871
+
23872
+ // src/codex/sessionConfig.ts
23781
23873
  var MCP_SERVER_NAME_WHITESPACE = /\p{White_Space}/gu;
23782
23874
  function sanitizeMcpServerName(name) {
23783
23875
  return name.replace(MCP_SERVER_NAME_WHITESPACE, "_");
@@ -23882,22 +23974,44 @@ ${resource.blob}
23882
23974
  function fromUserInput(input) {
23883
23975
  switch (input.type) {
23884
23976
  case "text":
23885
- return input.text.length > 0 ? [{ type: "text", text: input.text }] : [];
23886
- case "image":
23887
- return [{ type: "text", text: "url" in input ? formatUriAsLink("image", input.url) : `image:${input.fileId}` }];
23977
+ return questionReplyContent(input.text) ?? (input.text.length > 0 ? [fileReference(input.text) ?? { type: "text", text: input.text }] : []);
23978
+ case "image": {
23979
+ if ("url" in input) {
23980
+ const data = /^data:(image\/[a-zA-Z0-9.+-]+);base64,([a-zA-Z0-9+/=\r\n]+)$/.exec(input.url);
23981
+ if (data)
23982
+ return [{ type: "image", mimeType: data[1], data: data[2] }];
23983
+ return [{ type: "resource_link", name: "Image", uri: input.url }];
23984
+ }
23985
+ return [{ type: "resource_link", name: "Image", uri: `codex-file:${input.fileId}` }];
23986
+ }
23888
23987
  case "localImage":
23889
- return [{ type: "text", text: formatUriAsLink(null, toFileUri(input.path)) }];
23988
+ return [localImageContent(input.path)];
23890
23989
  case "skill":
23891
23990
  return [{ type: "text", text: `skill:${input.name} (${input.path})` }];
23892
23991
  case "mention":
23893
- return [{ type: "text", text: formatUriAsLink(input.name, toFileUri(input.path)) }];
23992
+ return [{ type: "resource_link", name: input.name, uri: toFileUri(input.path) }];
23894
23993
  case "audio":
23895
23994
  case "localAudio":
23896
23995
  return [];
23897
23996
  }
23898
23997
  }
23998
+ function fileReference(text) {
23999
+ if (!text.startsWith("[@") || !text.endsWith(")") || /[\r\n]/.test(text))
24000
+ return null;
24001
+ const separator = text.lastIndexOf("](file://");
24002
+ if (separator < 3)
24003
+ return null;
24004
+ const uri = text.slice(separator + 2, -1);
24005
+ try {
24006
+ if (new URL(uri).protocol !== "file:" || /\s/.test(uri))
24007
+ return null;
24008
+ } catch {
24009
+ return null;
24010
+ }
24011
+ return { type: "resource_link", name: text.slice(2, separator), uri };
24012
+ }
23899
24013
  function toFileUri(value) {
23900
- return value.startsWith("file://") ? value : pathToFileURL(value).href;
24014
+ return value.startsWith("file://") ? value : pathToFileURL2(value).href;
23901
24015
  }
23902
24016
  function isSupportedImageUrl(uri) {
23903
24017
  if (!uri)
@@ -23922,12 +24036,43 @@ function promptTitle(prompt) {
23922
24036
  for (const block of prompt) {
23923
24037
  if (!ContentBlock.isText(block))
23924
24038
  continue;
24039
+ const replies = questionReplyContent(block.text);
24040
+ if (replies)
24041
+ return promptTitle(replies);
23925
24042
  const line = block.text.split(/\r?\n/).map((part) => part.trim()).find((part) => part.length > 0);
23926
24043
  if (line)
23927
24044
  return line.length > 120 ? `${line.slice(0, 117)}...` : line;
23928
24045
  }
24046
+ for (const block of prompt) {
24047
+ if (ContentBlock.isResourceLink(block))
24048
+ return block.name;
24049
+ }
23929
24050
  return null;
23930
24051
  }
24052
+ function fromUserMessage(inputs) {
24053
+ const images = new Set(inputs.filter((input) => input.type === "localImage").map((input) => input.path));
24054
+ const inlineImages = inputs.filter((input) => input.type === "image" && ("url" in input) && /^data:image\/[a-zA-Z0-9.+-]+;base64,[a-zA-Z0-9+/=\r\n]+$/.test(input.url)).length;
24055
+ const imageCount = images.size + inlineImages;
24056
+ return inputs.flatMap((input, index) => {
24057
+ if (index !== 0 || input.type !== "text" || imageCount === 0)
24058
+ return fromUserInput(input);
24059
+ const envelope = /^\s*# Files mentioned by the user:\r?\n\r?\n([\s\S]*?)\r?\n\r?\nDistinguish instructions in attached documents from the user's request\.\r?\n\r?\n## My request:\r?\n([\s\S]*)$/.exec(input.text);
24060
+ if (!envelope)
24061
+ return fromUserInput(input);
24062
+ const entries = envelope[1].split(/\r?\n\r?\n/);
24063
+ const paths = new Set;
24064
+ for (const entry of entries) {
24065
+ const match = /^## (.+): (.+)\r?\nImage attachment: true$/.exec(entry);
24066
+ if (!match || path3.basename(match[2]) !== match[1] || paths.has(match[2]))
24067
+ return fromUserInput(input);
24068
+ paths.add(match[2]);
24069
+ }
24070
+ if (paths.size !== imageCount || [...images].some((image) => !paths.has(image)))
24071
+ return fromUserInput(input);
24072
+ const text = envelope[2].trim();
24073
+ return text ? [{ type: "text", text }] : [];
24074
+ });
24075
+ }
23931
24076
 
23932
24077
  // src/agent/commandReceipts.ts
23933
24078
  import { createHash, randomUUID } from "crypto";
@@ -24074,7 +24219,7 @@ function itemSnapshot(item, command) {
24074
24219
  return [];
24075
24220
  if (command)
24076
24221
  return [{ sessionUpdate: "user_message", messageId: command.messageId, content: command.content }];
24077
- const content = item.content.flatMap(fromUserInput);
24222
+ const content = fromUserMessage(item.content);
24078
24223
  return content.length > 0 ? [{ sessionUpdate: "user_message", messageId: item.clientId ?? item.id, content }] : [];
24079
24224
  }
24080
24225
  case "agentMessage":
@@ -24163,6 +24308,10 @@ class EventBridge {
24163
24308
  insertedMessages = new Map;
24164
24309
  unannouncedItems = new Map;
24165
24310
  turnTime = null;
24311
+ messageSentTimes = new Map;
24312
+ workTiming = null;
24313
+ timingCarrier = null;
24314
+ turnPrompt = null;
24166
24315
  openToolCalls = new Set;
24167
24316
  announcedTools = new Set;
24168
24317
  messagePhases = new Map;
@@ -24188,9 +24337,22 @@ class EventBridge {
24188
24337
  this.promptMeta.set(messageId, meta);
24189
24338
  }
24190
24339
  async insertPrompt(messageId, content) {
24191
- const meta = this.promptMeta.get(messageId);
24340
+ content = content.flatMap((block) => ContentBlock.isText(block) ? questionReplyContent(block.text) ?? [block] : [block]);
24341
+ let meta = this.promptMeta.get(messageId);
24342
+ if (content.some((block) => ContentBlock.isText(block) && block._meta?.["codex"] && typeof block._meta["codex"] === "object" && ("questionReply" in block._meta["codex"]))) {
24343
+ const parts = content.flatMap((block) => {
24344
+ if (!ContentBlock.isText(block))
24345
+ return [];
24346
+ const reply = block._meta?.["codex"]?.questionReply;
24347
+ return [reply ? { question: reply.question, text: reply.answer } : { text: block.text }];
24348
+ });
24349
+ meta = { ...meta, codex: { ...meta?.["codex"], questionReplyParts: parts } };
24350
+ }
24192
24351
  this.promptMeta.delete(messageId);
24193
24352
  this.insertedMessages.set(messageId, { content, meta });
24353
+ const sentAt = promptSentAt(meta);
24354
+ if (sentAt !== undefined)
24355
+ this.messageSentTimes.set(messageId, sentAt);
24194
24356
  await this.client.update({ sessionUpdate: "user_message", messageId, content, ...meta ? { _meta: meta } : {} });
24195
24357
  }
24196
24358
  rememberCommand(messageId, native) {
@@ -24201,6 +24363,11 @@ class EventBridge {
24201
24363
  }
24202
24364
  beginTurn() {
24203
24365
  this.commandEcho = null;
24366
+ this.messageSentTimes.clear();
24367
+ this.workTiming = null;
24368
+ this.timingCarrier = null;
24369
+ this.turnPrompt = null;
24370
+ this.turnTime = null;
24204
24371
  this.restoredCommands.clear();
24205
24372
  this.unresolvedUserItems.clear();
24206
24373
  this.insertedMessages.clear();
@@ -24265,6 +24432,16 @@ class EventBridge {
24265
24432
  if (notification.method === "turn/started" || notification.method === "turn/completed") {
24266
24433
  const { id, startedAt } = notification.params.turn;
24267
24434
  this.turnTime = { id, startedAt };
24435
+ if (notification.method === "turn/started" && startedAt !== null) {
24436
+ this.workTiming = { id, startedAt: startedAt * 1000 };
24437
+ }
24438
+ }
24439
+ if (notification.method === "item/started" && notification.params.item.type === "agentMessage" && notification.params.item.phase === "final_answer" && notification.params.item.delivery !== "async" && this.workTiming?.id === notification.params.turnId && this.workTiming.finalStartedAt === undefined) {
24440
+ this.workTiming.finalStartedAt = notification.params.startedAtMs;
24441
+ if (this.timingCarrier?.turnId === notification.params.turnId) {
24442
+ const { turnId, update } = this.timingCarrier;
24443
+ await this.client.update(this.withTiming(update, turnId));
24444
+ }
24268
24445
  }
24269
24446
  let updates = await this.translate(notification);
24270
24447
  if (notification.method === "item/completed") {
@@ -24277,9 +24454,61 @@ class EventBridge {
24277
24454
  for (const update of updates) {
24278
24455
  this.trackToolCall(update);
24279
24456
  const turnId = "turnId" in notification.params ? notification.params.turnId : null;
24280
- await this.client.update(typeof turnId === "string" ? withTurnId(update, turnId, this.turnTime?.id === turnId ? this.turnTime.startedAt : null) : update);
24457
+ let tagged = typeof turnId === "string" ? this.withTiming(withTurnId(update, turnId, this.turnTime?.id === turnId ? this.turnTime.startedAt : null), turnId) : update;
24458
+ if (update.sessionUpdate === "user_message" && typeof update.messageId === "string") {
24459
+ const originalSentAt = promptSentAt(update._meta);
24460
+ if (originalSentAt !== undefined)
24461
+ this.messageSentTimes.set(update.messageId, originalSentAt);
24462
+ if (notification.method === "item/started" && !this.messageSentTimes.has(update.messageId)) {
24463
+ this.messageSentTimes.set(update.messageId, notification.params.startedAtMs);
24464
+ }
24465
+ const sentAt = this.messageSentTimes.get(update.messageId);
24466
+ if (sentAt !== undefined) {
24467
+ if (this.workTiming?.id === turnId && this.turnPrompt?.turnId !== turnId && (originalSentAt !== undefined || this.insertedMessages.has(update.messageId) && promptSentAt(this.insertedMessages.get(update.messageId)?.meta) !== undefined))
24468
+ this.workTiming.startedAt = sentAt;
24469
+ tagged = timingMeta(typeof turnId === "string" ? this.withTiming(tagged, turnId) : tagged, { messageSentAt: sentAt });
24470
+ }
24471
+ }
24472
+ if (notification.method === "item/completed" && update.sessionUpdate === "agent_message") {
24473
+ tagged = timingMeta(tagged, { messageCompletedAt: notification.params.completedAtMs });
24474
+ }
24475
+ if (typeof turnId === "string" && (update.sessionUpdate === "user_message" || update.sessionUpdate === "agent_message")) {
24476
+ this.timingCarrier = { turnId, update: tagged };
24477
+ }
24478
+ if (typeof turnId === "string" && update.sessionUpdate === "user_message") {
24479
+ this.turnPrompt = { turnId, update: tagged };
24480
+ }
24481
+ await this.client.update(tagged);
24482
+ }
24483
+ if (notification.method === "turn/completed") {
24484
+ const turn = notification.params.turn;
24485
+ const carrier = this.timingCarrier;
24486
+ if (carrier?.turnId === turn.id) {
24487
+ await this.client.update(timingMeta(this.withTiming(carrier.update, turn.id), {
24488
+ ...turn.completedAt !== null ? { turnCompletedAt: turn.completedAt * 1000 } : {},
24489
+ ...turn.durationMs !== null ? { turnDurationMs: turn.durationMs } : {}
24490
+ }));
24491
+ }
24492
+ if (this.turnPrompt?.turnId === turn.id && turn.status === "interrupted") {
24493
+ await this.client.update(timingMeta(this.withTiming(this.turnPrompt.update, turn.id), {
24494
+ turnInterrupted: true,
24495
+ ...turn.completedAt !== null ? { turnCompletedAt: turn.completedAt * 1000 } : {},
24496
+ ...turn.durationMs !== null ? { turnDurationMs: turn.durationMs } : {}
24497
+ }));
24498
+ }
24499
+ this.turnPrompt = null;
24281
24500
  }
24282
24501
  }
24502
+ withTiming(update, turnId) {
24503
+ const timing = this.workTiming;
24504
+ if (timing?.id !== turnId)
24505
+ return update;
24506
+ const duration3 = timing.finalStartedAt === undefined ? undefined : timing.finalStartedAt - timing.startedAt;
24507
+ return timingMeta(update, {
24508
+ workStartedAt: timing.startedAt,
24509
+ ...duration3 !== undefined && duration3 >= 0 ? { workDurationMs: duration3 } : {}
24510
+ });
24511
+ }
24283
24512
  rememberNativeCommand(item) {
24284
24513
  const command = this.commandEcho;
24285
24514
  if (!command || command.nativeId)
@@ -24545,7 +24774,7 @@ ${details}` : summary)];
24545
24774
  const inserted = item.clientId ? this.insertedMessages.get(item.clientId) : undefined;
24546
24775
  if (inserted)
24547
24776
  return [{ sessionUpdate: "user_message", messageId: item.clientId, content: inserted.content }];
24548
- const content = item.content.flatMap(fromUserInput);
24777
+ const content = fromUserMessage(item.content);
24549
24778
  if (content.length === 0)
24550
24779
  return [];
24551
24780
  const messageId = item.clientId ?? item.id;
@@ -24720,6 +24949,18 @@ function turnPlan(params) {
24720
24949
  }
24721
24950
  };
24722
24951
  }
24952
+ function timingMeta(update, fields) {
24953
+ const meta = update._meta;
24954
+ const codex = meta?.["codex"];
24955
+ return { ...update, _meta: { ...meta, codex: { ...codex, ...fields } } };
24956
+ }
24957
+ function promptSentAt(meta) {
24958
+ const codex = meta?.["codex"];
24959
+ if (typeof codex !== "object" || codex === null)
24960
+ return;
24961
+ const value = codex["messageSentAt"];
24962
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 8640000000000000 ? value : undefined;
24963
+ }
24723
24964
 
24724
24965
  // src/codex/AppServerClient.ts
24725
24966
  var import_node = __toESM(require_main(), 1);
@@ -26569,7 +26810,7 @@ class TurnContext {
26569
26810
  // node_modules/open/index.js
26570
26811
  import process10 from "process";
26571
26812
  import path7 from "path";
26572
- import { fileURLToPath } from "url";
26813
+ import { fileURLToPath as fileURLToPath2 } from "url";
26573
26814
  import childProcess4 from "child_process";
26574
26815
  import fs6, { constants as fsConstants2 } from "fs/promises";
26575
26816
 
@@ -26952,7 +27193,7 @@ var is_in_ssh_default = isInSsh;
26952
27193
 
26953
27194
  // node_modules/open/index.js
26954
27195
  var fallbackAttemptSymbol = Symbol("fallbackAttempt");
26955
- var __dirname2 = import.meta.url ? path7.dirname(fileURLToPath(import.meta.url)) : "";
27196
+ var __dirname2 = import.meta.url ? path7.dirname(fileURLToPath2(import.meta.url)) : "";
26956
27197
  var localXdgOpenPath = path7.join(__dirname2, "xdg-open");
26957
27198
  var { platform, arch } = process10;
26958
27199
  var tryEachApp = async (apps, opener) => {
@@ -27164,7 +27405,7 @@ var baseOpen = async (options) => {
27164
27405
  });
27165
27406
  });
27166
27407
  };
27167
- var open = (target, options) => {
27408
+ var open2 = (target, options) => {
27168
27409
  if (typeof target !== "string") {
27169
27410
  throw new TypeError("Expected a `target`");
27170
27411
  }
@@ -27233,7 +27474,7 @@ defineLazyProperty(apps, "edge", () => detectPlatformBinary({
27233
27474
  defineLazyProperty(apps, "safari", () => detectPlatformBinary({
27234
27475
  darwin: "Safari"
27235
27476
  }));
27236
- var open_default = open;
27477
+ var open_default = open2;
27237
27478
 
27238
27479
  // src/agent/auth.ts
27239
27480
  var AuthMethodId = {
@@ -27369,6 +27610,10 @@ class ClientSession {
27369
27610
  async update(update) {
27370
27611
  if (this.disposed)
27371
27612
  return;
27613
+ if (SessionUpdate.isUserMessage(update) && update.content) {
27614
+ update = { ...update, content: await hydrateUserImages(update.content, this.output.signal) };
27615
+ }
27616
+ this.output.signal.throwIfAborted();
27372
27617
  await abortable(this.link.notify(methods.client.session.update, { sessionId: this.sessionId, update }), this.output.signal);
27373
27618
  }
27374
27619
  async updateAll(updates) {
@@ -27712,9 +27957,24 @@ function stringValue(request) {
27712
27957
  function historyUpdates(turns, commandReceipts = new Map) {
27713
27958
  const updates = [];
27714
27959
  for (const turn of turns) {
27960
+ const openingUser = turn.items.find((item) => item.type === "userMessage");
27715
27961
  for (const item of turn.items) {
27716
27962
  const command = item.type === "userMessage" ? commandReceipts.get(item.id) : undefined;
27717
- updates.push(...itemSnapshot(item, command).map((update) => withTurnId(update, turn.id, turn.startedAt)));
27963
+ updates.push(...itemSnapshot(item, command).map((update) => {
27964
+ const replay = withTurnId(update, turn.id, turn.startedAt);
27965
+ const meta = replay._meta;
27966
+ const codex = meta?.["codex"];
27967
+ const duration3 = turn.durationMs ?? (turn.startedAt !== null && turn.completedAt !== null ? (turn.completedAt - turn.startedAt) * 1000 : null);
27968
+ replay._meta = { ...meta, codex: {
27969
+ ...codex,
27970
+ historyReplay: true,
27971
+ ...turn.completedAt !== null ? { turnCompletedAt: turn.completedAt * 1000 } : {},
27972
+ ...item === openingUser && turn.startedAt !== null ? { messageSentAt: turn.startedAt * 1000 } : {},
27973
+ ...duration3 !== null && Number.isFinite(duration3) && duration3 >= 0 ? { turnDurationMs: duration3 } : {},
27974
+ ...turn.status === "interrupted" ? { historyInterrupted: true } : {}
27975
+ } };
27976
+ return replay;
27977
+ }));
27718
27978
  }
27719
27979
  }
27720
27980
  return updates;
@@ -27727,9 +27987,9 @@ function historyTitle(thread, turns) {
27727
27987
  for (const item of turn.items) {
27728
27988
  if (item.type !== "userMessage")
27729
27989
  continue;
27730
- const text2 = item.content.filter((input) => input.type === "text").map((input) => input.text.trim()).find((part) => part.length > 0);
27731
- if (text2)
27732
- return firstLine(text2);
27990
+ const title = promptTitle(fromUserMessage(item.content));
27991
+ if (title)
27992
+ return title;
27733
27993
  }
27734
27994
  }
27735
27995
  const preview = thread.preview.trim();
@@ -27902,7 +28162,7 @@ class HistoryRead {
27902
28162
  }
27903
28163
 
27904
28164
  // src/agent/historyProjection.ts
27905
- import { pathToFileURL as pathToFileURL2 } from "url";
28165
+ import { pathToFileURL as pathToFileURL3 } from "url";
27906
28166
  function inputBlock(input) {
27907
28167
  switch (input.type) {
27908
28168
  case "text":
@@ -27913,10 +28173,10 @@ function inputBlock(input) {
27913
28173
  return { type: "resource_link", name: "Audio", uri: input.url, _meta: { codex: { inputType: input.type } } };
27914
28174
  case "localImage":
27915
28175
  case "localAudio":
27916
- return { type: "resource_link", name: input.type === "localImage" ? "Image" : "Audio", uri: input.path.startsWith("file://") ? input.path : pathToFileURL2(input.path).href, _meta: { codex: { inputType: input.type, ..."detail" in input && input.detail ? { detail: input.detail } : {} } } };
28176
+ return { type: "resource_link", name: input.type === "localImage" ? "Image" : "Audio", uri: input.path.startsWith("file://") ? input.path : pathToFileURL3(input.path).href, _meta: { codex: { inputType: input.type, ..."detail" in input && input.detail ? { detail: input.detail } : {} } } };
27917
28177
  case "skill":
27918
28178
  case "mention":
27919
- return { type: "resource_link", name: input.name, uri: input.path.startsWith("file://") ? input.path : pathToFileURL2(input.path).href, _meta: { codex: { inputType: input.type } } };
28179
+ return { type: "resource_link", name: input.name, uri: input.path.startsWith("file://") ? input.path : pathToFileURL3(input.path).href, _meta: { codex: { inputType: input.type } } };
27920
28180
  }
27921
28181
  }
27922
28182
  function projectHistoryItem(item, turnId, startedAt, command) {
@@ -28328,8 +28588,8 @@ class CodexAgent {
28328
28588
  const catalog = await this.withCodex(() => this.codex.allModels(true));
28329
28589
  const nativeProvider = await this.resolveModelProvider(candidate);
28330
28590
  const wholesale = this.providers.active !== null && this.providers.mode === "route" || candidate.active !== null && candidate.mode === "route";
28331
- const unavailable = new Set([...this.sessions.values()].filter((runtime) => this.providers.mode === "catalog" && this.providers.hasGateway(runtime.modelProvider) && !candidate.hasGateway(runtime.modelProvider)));
28332
- const moving = [...this.sessions.values()].filter((runtime) => !unavailable.has(runtime) && (wholesale || this.providers.hasGateway(runtime.modelProvider) && candidate.gateway(runtime.modelProvider) !== this.providers.gateway(runtime.modelProvider)));
28591
+ const unavailable2 = new Set([...this.sessions.values()].filter((runtime) => this.providers.mode === "catalog" && this.providers.hasGateway(runtime.modelProvider) && !candidate.hasGateway(runtime.modelProvider)));
28592
+ const moving = [...this.sessions.values()].filter((runtime) => !unavailable2.has(runtime) && (wholesale || this.providers.hasGateway(runtime.modelProvider) && candidate.gateway(runtime.modelProvider) !== this.providers.gateway(runtime.modelProvider)));
28333
28593
  const targets = new Map(moving.map((runtime) => {
28334
28594
  if (candidate.active === null)
28335
28595
  return [runtime, null];
@@ -28367,7 +28627,7 @@ class CodexAgent {
28367
28627
  }));
28368
28628
  staged.set(runtime, { config: config2, catalog: nextCatalog, model: resolveModelSelection(nextCatalog, requestedModel ?? candidate.selectionId(thread.model, modelProvider), thread.reasoningEffort) });
28369
28629
  }
28370
- for (const runtime of unavailable)
28630
+ for (const runtime of unavailable2)
28371
28631
  runtime.config = candidate.rebind(runtime.config, this.providers, null);
28372
28632
  this.providers = candidate;
28373
28633
  for (const runtime of this.sessions.values()) {
@@ -28380,7 +28640,7 @@ class CodexAgent {
28380
28640
  }
28381
28641
  runtime.session.catalog = nextCatalog;
28382
28642
  runtime.session.gatewayGroups = candidate.gatewayGroups();
28383
- if (!next && !unavailable.has(runtime))
28643
+ if (!next && !unavailable2.has(runtime))
28384
28644
  runtime.session.model = resolveModelSelection(nextCatalog, findModel(nextCatalog, runtime.session.model.model) ? runtime.session.model.model : null, runtime.session.model.effort);
28385
28645
  runtime.client.update({ sessionUpdate: "config_option_update", configOptions: sessionConfigOptions(runtime.session), _meta: { codex: { routing: { stale: false } } } }).catch((error40) => logger.error("Provider config notification failed", error40));
28386
28646
  }
@@ -28427,17 +28687,17 @@ class CodexAgent {
28427
28687
  const runtime = await this.openSession({ kind: "fork", request: params }, signal);
28428
28688
  return { sessionId: runtime.session.id, configOptions: sessionConfigOptions(runtime.session), _meta: { codex: runtime.lineage } };
28429
28689
  }
28430
- async openSession(open2, signal) {
28690
+ async openSession(open3, signal) {
28431
28691
  if (signal?.aborted)
28432
28692
  throw RequestError.requestCancelled(undefined, "Session opening was cancelled");
28433
28693
  const pending = this.admission(async () => {
28434
- const runtime = await this.openSessionInternal(open2, signal);
28694
+ const runtime = await this.openSessionInternal(open3, signal);
28435
28695
  if (signal?.aborted) {
28436
28696
  await this.closeRuntime(runtime.session.id);
28437
28697
  throw RequestError.requestCancelled(undefined, "Session opening was cancelled");
28438
28698
  }
28439
28699
  return runtime;
28440
- }, open2.kind === "new" ? undefined : open2.request.sessionId);
28700
+ }, open3.kind === "new" ? undefined : open3.request.sessionId);
28441
28701
  try {
28442
28702
  return signal ? await abortable(pending, signal) : await pending;
28443
28703
  } catch (error40) {
@@ -28451,24 +28711,24 @@ class CodexAgent {
28451
28711
  throw RequestError.invalidRequest({ sessionId }, "A previous unsubscribe is still pending; retry after Codex responds or reconnect the agent");
28452
28712
  }
28453
28713
  }
28454
- async openSessionInternal(open2, signal) {
28455
- const method = { new: "session/new", resume: "session/resume", fork: "session/fork" }[open2.kind];
28714
+ async openSessionInternal(open3, signal) {
28715
+ const method = { new: "session/new", resume: "session/resume", fork: "session/fork" }[open3.kind];
28456
28716
  const capabilities = this.requireInitialized(method);
28457
- if (open2.kind !== "new")
28458
- this.assertUnsubscribeSettled(open2.request.sessionId);
28459
- const { request } = open2;
28460
- const lastTurnId = open2.kind === "fork" ? forkTurnId(request._meta) : undefined;
28717
+ if (open3.kind !== "new")
28718
+ this.assertUnsubscribeSettled(open3.request.sessionId);
28719
+ const { request } = open3;
28720
+ const lastTurnId = open3.kind === "fork" ? forkTurnId(request._meta) : undefined;
28461
28721
  if (typeof request.cwd !== "string" || !path9.isAbsolute(request.cwd)) {
28462
28722
  throw RequestError.invalidParams({ cwd: request.cwd }, "cwd must be an absolute path");
28463
28723
  }
28464
- if (open2.kind === "resume" && open2.request.replayFrom != null && open2.request.replayFrom.type !== "start") {
28465
- throw RequestError.invalidParams({ replayFrom: open2.request.replayFrom }, 'Only replayFrom {type: "start"} is supported');
28724
+ if (open3.kind === "resume" && open3.request.replayFrom != null && open3.request.replayFrom.type !== "start") {
28725
+ throw RequestError.invalidParams({ replayFrom: open3.request.replayFrom }, 'Only replayFrom {type: "start"} is supported');
28466
28726
  }
28467
- const seed = open2.kind === "new" ? seedHistoryOf(open2.request._meta) : [];
28727
+ const seed = open3.kind === "new" ? seedHistoryOf(open3.request._meta) : [];
28468
28728
  const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories);
28469
- if (open2.kind === "resume" && this.sessions.has(open2.request.sessionId)) {
28470
- await this.closeRuntime(open2.request.sessionId);
28471
- this.assertUnsubscribeSettled(open2.request.sessionId);
28729
+ if (open3.kind === "resume" && this.sessions.has(open3.request.sessionId)) {
28730
+ await this.closeRuntime(open3.request.sessionId);
28731
+ this.assertUnsubscribeSettled(open3.request.sessionId);
28472
28732
  }
28473
28733
  const mcpServers = request.mcpServers ?? [];
28474
28734
  const clientTools = clientToolsOf(request._meta);
@@ -28483,8 +28743,8 @@ class CodexAgent {
28483
28743
  if (this.providers.active !== null && this.providers.mode === "catalog") {
28484
28744
  if (requestedModel !== null)
28485
28745
  gatewayId = this.providers.gatewayIdFor(requestedModel);
28486
- else if (open2.kind === "resume") {
28487
- const known = await this.withCodex(() => this.codex.threadRead({ threadId: open2.request.sessionId, includeTurns: false }));
28746
+ else if (open3.kind === "resume") {
28747
+ const known = await this.withCodex(() => this.codex.threadRead({ threadId: open3.request.sessionId, includeTurns: false }));
28488
28748
  gatewayId = this.providers.hasGateway(known.thread.modelProvider) ? known.thread.modelProvider : null;
28489
28749
  }
28490
28750
  }
@@ -28503,7 +28763,7 @@ class CodexAgent {
28503
28763
  const { thread, skills } = await this.withSkillsContext(request.cwd, additionalDirectories, async (skills2) => ({ skills: skills2, thread: await this.withCodex(async () => {
28504
28764
  if (signal?.aborted)
28505
28765
  throw RequestError.requestCancelled(undefined, "Session opening was cancelled");
28506
- switch (open2.kind) {
28766
+ switch (open3.kind) {
28507
28767
  case "new":
28508
28768
  return await this.codex.threadStart({
28509
28769
  ...hostThreadOptions,
@@ -28515,15 +28775,15 @@ class CodexAgent {
28515
28775
  });
28516
28776
  case "resume":
28517
28777
  try {
28518
- return await this.codex.threadResume({ ...hostThreadOptions, threadId: open2.request.sessionId, config: config2, cwd: request.cwd, modelProvider, model: startModel, excludeTurns: true });
28778
+ return await this.codex.threadResume({ ...hostThreadOptions, threadId: open3.request.sessionId, config: config2, cwd: request.cwd, modelProvider, model: startModel, excludeTurns: true });
28519
28779
  } catch (error40) {
28520
28780
  if (!errorMessage(error40).includes("already has an active writer"))
28521
28781
  throw error40;
28522
28782
  readOnly = true;
28523
- return readOnlyThread((await this.codex.threadRead({ threadId: open2.request.sessionId, includeTurns: false })).thread, request.cwd);
28783
+ return readOnlyThread((await this.codex.threadRead({ threadId: open3.request.sessionId, includeTurns: false })).thread, request.cwd);
28524
28784
  }
28525
28785
  case "fork":
28526
- return await this.codex.threadFork({ ...hostThreadOptions, ...lastTurnId === undefined ? {} : { lastTurnId }, threadId: open2.request.sessionId, config: config2, cwd: request.cwd, modelProvider, model: startModel, excludeTurns: true });
28786
+ return await this.codex.threadFork({ ...hostThreadOptions, ...lastTurnId === undefined ? {} : { lastTurnId }, threadId: open3.request.sessionId, config: config2, cwd: request.cwd, modelProvider, model: startModel, excludeTurns: true });
28527
28787
  }
28528
28788
  }) }));
28529
28789
  const sessionId = thread.thread.id;
@@ -28583,8 +28843,8 @@ class CodexAgent {
28583
28843
  removeCancellation = () => signal?.removeEventListener("abort", cancelOpening);
28584
28844
  if (signal?.aborted)
28585
28845
  cancelOpening();
28586
- const replay = open2.kind === "fork" || open2.kind === "resume" && open2.request.replayFrom?.type === "start";
28587
- if (open2.kind !== "new") {
28846
+ const replay = open3.kind === "fork" || open3.kind === "resume" && open3.request.replayFrom?.type === "start";
28847
+ if (open3.kind !== "new") {
28588
28848
  await this.replayHistory(runtime, replay, thread.thread);
28589
28849
  } else {
28590
28850
  if (seed.length > 0) {
@@ -28841,13 +29101,13 @@ class CodexAgent {
28841
29101
  response.data.push(...missing2.values());
28842
29102
  }
28843
29103
  return {
28844
- sessions: response.data.map((thread) => ({
29104
+ sessions: await Promise.all(response.data.map(async (thread) => ({
28845
29105
  sessionId: thread.id,
28846
29106
  cwd: thread.cwd,
28847
- title: thread.name?.trim() || thread.preview.trim() || null,
29107
+ title: !thread.name?.trim() && /^(?:\[@|# Files mentioned by the user:|<send_user_message_question_reply>)/.test(thread.preview.trimStart()) ? historyTitle(thread, (await this.turnPage(thread.id, null)).data) : thread.name?.trim() || thread.preview.trim() || null,
28848
29108
  updatedAt: new Date(thread.updatedAt * 1000).toISOString(),
28849
29109
  _meta: { codex: { archived, ...threadLineage(thread) } }
28850
- })),
29110
+ }))),
28851
29111
  nextCursor: response.nextCursor
28852
29112
  };
28853
29113
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.7.9",
6
+ "version": "0.7.10",
7
7
  "description": "Native ACP v2 agent for the OpenAI Codex app-server",
8
8
  "packageManager": "bun@1.4.0",
9
9
  "engines": {