remote-codex 0.11.54 → 0.11.55

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.
@@ -8934,13 +8934,16 @@ function deferLargeHistoryItemDetails(turn, deferredDetails) {
8934
8934
  };
8935
8935
  }
8936
8936
  function visibleRuntimeTurnItems(items) {
8937
- const hasFinalAgentMessage = items.some(
8937
+ const nonEmptyItems = items.filter(
8938
+ (item) => !((item.kind === "agentMessage" || item.kind === "reasoning") && item.text.trim().length === 0)
8939
+ );
8940
+ const hasFinalAgentMessage = nonEmptyItems.some(
8938
8941
  (item) => item.kind === "agentMessage" && !isTransientAgentHistoryItem(item)
8939
8942
  );
8940
8943
  if (!hasFinalAgentMessage) {
8941
- return items;
8944
+ return nonEmptyItems;
8942
8945
  }
8943
- return items.filter(
8946
+ return nonEmptyItems.filter(
8944
8947
  (item) => !(item.kind === "agentMessage" && isTransientAgentHistoryItem(item))
8945
8948
  );
8946
8949
  }
@@ -21500,6 +21503,50 @@ function reasoningOptions(configOptions) {
21500
21503
  defaultEffort: normalizeAcpEffort(option.currentValue)
21501
21504
  };
21502
21505
  }
21506
+ function legacyModelsFromResponse(response) {
21507
+ if (!response || typeof response !== "object") {
21508
+ return null;
21509
+ }
21510
+ const models = response.models;
21511
+ if (!models || typeof models !== "object") {
21512
+ return null;
21513
+ }
21514
+ const availableModels = models.availableModels;
21515
+ return Array.isArray(availableModels) ? models : null;
21516
+ }
21517
+ function legacyModelOptions(models) {
21518
+ const currentModelId = models.currentModelId ?? null;
21519
+ return (models.availableModels ?? []).flatMap((model, index) => {
21520
+ if (!model.modelId) {
21521
+ return [];
21522
+ }
21523
+ const efforts = (model._meta?.reasoningEfforts ?? []).flatMap((entry) => {
21524
+ const reasoningEffort = normalizeAcpEffort(entry.value ?? entry.id);
21525
+ return reasoningEffort ? [{
21526
+ reasoningEffort,
21527
+ description: entry.description ?? entry.label ?? ""
21528
+ }] : [];
21529
+ });
21530
+ const declaredDefault = model._meta?.reasoningEfforts?.find(
21531
+ (entry) => entry.default
21532
+ );
21533
+ return [{
21534
+ id: model.modelId,
21535
+ model: model.modelId,
21536
+ displayName: model.name ?? model.modelId,
21537
+ description: model.description ?? "",
21538
+ isDefault: model.modelId === currentModelId || !currentModelId && index === 0,
21539
+ hidden: false,
21540
+ supportedReasoningEfforts: efforts,
21541
+ defaultReasoningEffort: normalizeAcpEffort(model._meta?.reasoningEffort) ?? normalizeAcpEffort(declaredDefault?.value ?? declaredDefault?.id),
21542
+ selectionKind: "model"
21543
+ }];
21544
+ });
21545
+ }
21546
+ function selectedLegacyModel(state) {
21547
+ const models = state.legacyModels?.availableModels ?? [];
21548
+ return models.find((model) => model.modelId === state.model) ?? models[0] ?? null;
21549
+ }
21503
21550
  function permissionOptions(params) {
21504
21551
  return params.options.map((option) => ({
21505
21552
  id: option.optionId,
@@ -21792,7 +21839,32 @@ var AcpRuntimeAdapter = class extends EventEmitter8 {
21792
21839
  async inspectModelOptions(cwd) {
21793
21840
  const sessionCapabilities = this.initializeResponse?.agentCapabilities?.sessionCapabilities;
21794
21841
  if (!sessionCapabilities?.delete) {
21795
- return this.listModels();
21842
+ const normalizedCwd = path16.resolve(cwd);
21843
+ const loadedSession = [...this.sessions.values()].find(
21844
+ (session) => session.cwd === normalizedCwd && (session.configOptions.length > 0 || session.legacyModels !== null)
21845
+ );
21846
+ if (!loadedSession) {
21847
+ return this.listModels();
21848
+ }
21849
+ if (loadedSession.legacyModels) {
21850
+ return legacyModelOptions(loadedSession.legacyModels);
21851
+ }
21852
+ const reasoning = reasoningOptions(loadedSession.configOptions);
21853
+ const supportsPerformanceMode2 = loadedSession.configOptions.some(
21854
+ (option) => option.id === "fast-mode" || option.category === "model_config"
21855
+ );
21856
+ return [{
21857
+ id: "default",
21858
+ model: "default",
21859
+ displayName: "Agent default",
21860
+ description: "Use the model configured by the ACP agent.",
21861
+ isDefault: true,
21862
+ hidden: false,
21863
+ supportsPerformanceMode: supportsPerformanceMode2,
21864
+ supportedReasoningEfforts: reasoning.efforts,
21865
+ defaultReasoningEffort: reasoning.defaultEffort,
21866
+ selectionKind: "model"
21867
+ }];
21796
21868
  }
21797
21869
  const context = await this.requireContext();
21798
21870
  const response = await context.request(methods.agent.session.new, {
@@ -21806,6 +21878,10 @@ var AcpRuntimeAdapter = class extends EventEmitter8 {
21806
21878
  (option) => option.id === "fast-mode" || option.category === "model_config"
21807
21879
  );
21808
21880
  try {
21881
+ const legacyModels = legacyModelsFromResponse(response);
21882
+ if (legacyModels) {
21883
+ return legacyModelOptions(legacyModels);
21884
+ }
21809
21885
  const modelOption = configOptionByCategory(configOptions, "model");
21810
21886
  if (!modelOption || modelOption.type !== "select") {
21811
21887
  const reasoning = reasoningOptions(configOptions);
@@ -21991,6 +22067,7 @@ var AcpRuntimeAdapter = class extends EventEmitter8 {
21991
22067
  }
21992
22068
  forked.modes = response.modes ?? forked.modes;
21993
22069
  forked.configOptions = response.configOptions ?? forked.configOptions;
22070
+ forked.legacyModels = legacyModelsFromResponse(response) ?? forked.legacyModels;
21994
22071
  this.syncStateFromConfigOptions(forked);
21995
22072
  return sessionDetail(forked);
21996
22073
  }
@@ -22052,7 +22129,8 @@ var AcpRuntimeAdapter = class extends EventEmitter8 {
22052
22129
  cwd: path16.resolve(input.cwd),
22053
22130
  mcpServers: [],
22054
22131
  _meta: {
22055
- yoloMode: input.approvalMode === "yolo"
22132
+ yoloMode: input.approvalMode === "yolo",
22133
+ ...input.reasoningEffort ? { reasoningEffort: input.reasoningEffort } : {}
22056
22134
  }
22057
22135
  });
22058
22136
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -22070,6 +22148,7 @@ var AcpRuntimeAdapter = class extends EventEmitter8 {
22070
22148
  activeMapper: null,
22071
22149
  modes: response.modes ?? null,
22072
22150
  configOptions: response.configOptions ?? [],
22151
+ legacyModels: legacyModelsFromResponse(response),
22073
22152
  availableCommands: [],
22074
22153
  hydrationCoverage: null,
22075
22154
  goal: null
@@ -22285,6 +22364,7 @@ var AcpRuntimeAdapter = class extends EventEmitter8 {
22285
22364
  activeMapper: null,
22286
22365
  modes: null,
22287
22366
  configOptions: [],
22367
+ legacyModels: null,
22288
22368
  availableCommands: [],
22289
22369
  hydrationCoverage: null,
22290
22370
  goal: null
@@ -22303,6 +22383,7 @@ var AcpRuntimeAdapter = class extends EventEmitter8 {
22303
22383
  });
22304
22384
  state.modes = response.modes ?? null;
22305
22385
  state.configOptions = response.configOptions ?? [];
22386
+ state.legacyModels = legacyModelsFromResponse(response);
22306
22387
  state.turns = hydrator.complete();
22307
22388
  state.hydrationCoverage = hydrator.coverage();
22308
22389
  this.syncStateFromConfigOptions(state);
@@ -22317,6 +22398,7 @@ var AcpRuntimeAdapter = class extends EventEmitter8 {
22317
22398
  });
22318
22399
  state.modes = response.modes ?? null;
22319
22400
  state.configOptions = response.configOptions ?? [];
22401
+ state.legacyModels = legacyModelsFromResponse(response);
22320
22402
  this.syncStateFromConfigOptions(state);
22321
22403
  } else {
22322
22404
  throw new Error("ACP agent does not support session/load or session/resume.");
@@ -22338,7 +22420,7 @@ var AcpRuntimeAdapter = class extends EventEmitter8 {
22338
22420
  if (model && model !== "default") {
22339
22421
  state.model = await this.setConfigOption(state, "model", model);
22340
22422
  }
22341
- if (reasoningEffort) {
22423
+ if (reasoningEffort && normalizeAcpEffort(reasoningEffort) !== state.reasoningEffort) {
22342
22424
  const applied = await this.setConfigOption(
22343
22425
  state,
22344
22426
  "thought_level",
@@ -22368,17 +22450,39 @@ var AcpRuntimeAdapter = class extends EventEmitter8 {
22368
22450
  }
22369
22451
  }
22370
22452
  async setConfigOption(state, category, value) {
22371
- const option = state.configOptions.find(
22372
- (candidate) => candidate.category === category || candidate.id.toLowerCase().includes(category === "model" ? "model" : "thought")
22373
- );
22453
+ const option = configOptionByCategory(state.configOptions, category);
22374
22454
  if ((!option || option.type !== "select") && category === "model") {
22375
22455
  const context2 = await this.requireContext();
22376
22456
  await context2.request("session/set_model", {
22377
22457
  sessionId: state.providerSessionId,
22378
22458
  modelId: value
22379
22459
  });
22460
+ if (state.legacyModels) {
22461
+ state.legacyModels.currentModelId = value;
22462
+ }
22380
22463
  return value;
22381
22464
  }
22465
+ if ((!option || option.type !== "select") && category === "thought_level") {
22466
+ const legacyModel = selectedLegacyModel(state);
22467
+ const selected2 = legacyModel?._meta?.reasoningEfforts?.find(
22468
+ (entry) => normalizeAcpEffort(entry.value ?? entry.id) === normalizeAcpEffort(value)
22469
+ );
22470
+ const modeId = selected2?.value ?? selected2?.id;
22471
+ if (modeId) {
22472
+ const context2 = await this.requireContext();
22473
+ const response2 = await context2.request(methods.agent.session.load, {
22474
+ sessionId: state.providerSessionId,
22475
+ cwd: state.cwd,
22476
+ mcpServers: [],
22477
+ _meta: { reasoningEffort: modeId }
22478
+ });
22479
+ state.modes = response2.modes ?? state.modes;
22480
+ state.configOptions = response2.configOptions ?? state.configOptions;
22481
+ state.legacyModels = legacyModelsFromResponse(response2) ?? state.legacyModels;
22482
+ this.syncStateFromConfigOptions(state);
22483
+ return state.reasoningEffort ?? modeId;
22484
+ }
22485
+ }
22382
22486
  if (!option || option.type !== "select") {
22383
22487
  throw new AgentRuntimeError(
22384
22488
  `ACP agent does not expose a ${category} config option.`,
@@ -22421,6 +22525,16 @@ var AcpRuntimeAdapter = class extends EventEmitter8 {
22421
22525
  if (thoughtOption?.type === "select") {
22422
22526
  state.reasoningEffort = normalizeAcpEffort(thoughtOption.currentValue);
22423
22527
  }
22528
+ if (!modelOption && state.legacyModels) {
22529
+ state.model = state.legacyModels.currentModelId ?? state.legacyModels.availableModels?.[0]?.modelId ?? null;
22530
+ }
22531
+ if (!thoughtOption) {
22532
+ const legacyModel = selectedLegacyModel(state);
22533
+ const declaredDefault = legacyModel?._meta?.reasoningEfforts?.find(
22534
+ (entry) => entry.default
22535
+ );
22536
+ state.reasoningEffort = normalizeAcpEffort(legacyModel?._meta?.reasoningEffort) ?? normalizeAcpEffort(declaredDefault?.value ?? declaredDefault?.id);
22537
+ }
22424
22538
  }
22425
22539
  updateCapabilitiesFromConfigOptions(configOptions) {
22426
22540
  this.capabilities.management.models ||= Boolean(
@@ -23832,7 +23946,13 @@ var AcpCatalogRuntimeAdapter = class extends EventEmitter9 {
23832
23946
  if (models.length === 1 && models[0]?.model === "default") {
23833
23947
  const commandModels = await this.catalog.listCommandModels(agentId);
23834
23948
  if (commandModels.length > 0) {
23835
- models = commandModels;
23949
+ const discoveredDefaults = models[0];
23950
+ models = commandModels.map((model) => ({
23951
+ ...model,
23952
+ supportsPerformanceMode: model.supportsPerformanceMode ?? discoveredDefaults.supportsPerformanceMode ?? false,
23953
+ supportedReasoningEfforts: model.supportedReasoningEfforts.length > 0 ? model.supportedReasoningEfforts : discoveredDefaults.supportedReasoningEfforts,
23954
+ defaultReasoningEffort: model.defaultReasoningEffort ?? discoveredDefaults.defaultReasoningEffort
23955
+ }));
23836
23956
  }
23837
23957
  }
23838
23958
  this.modelCache.set(cacheKey, { at: Date.now(), models });
@@ -23933,6 +24053,7 @@ var AcpCatalogRuntimeAdapter = class extends EventEmitter9 {
23933
24053
  const probedDefaultModel = this.modelCache.get(`${agentId}\0${input.cwd}`)?.models.find((model) => model.isDefault)?.model ?? null;
23934
24054
  const resolvedModel = response.model && response.model !== "default" ? response.model : input.model !== "default" ? input.model : probedDefaultModel ?? input.model;
23935
24055
  this.refreshAgentSessionCapabilities(agentId, agent);
24056
+ this.modelCache.clear();
23936
24057
  const session = scopedSession(agentId, response.session);
23937
24058
  return {
23938
24059
  ...response,
@@ -23958,6 +24079,7 @@ var AcpCatalogRuntimeAdapter = class extends EventEmitter9 {
23958
24079
  throw error;
23959
24080
  }
23960
24081
  this.refreshAgentSessionCapabilities(owner.agentId, agent);
24082
+ this.modelCache.clear();
23961
24083
  const session = scopedSession(owner.agentId, response.session);
23962
24084
  return {
23963
24085
  ...response,
@@ -25906,6 +26028,19 @@ function mergeHistoryItemsBySequence(items, missingItems) {
25906
26028
  return sortTurnItemsByRecordedSequence(mergedItems);
25907
26029
  }
25908
26030
  function shouldAppendPersistedMissingItem(turn, item) {
26031
+ if (item.kind === "userMessage" || item.kind === "reasoning") {
26032
+ const persistedText = item.text.replace(/\s+/g, " ").trim();
26033
+ const duplicate = turn.items.some((turnItem) => {
26034
+ if (turnItem.kind !== item.kind) {
26035
+ return false;
26036
+ }
26037
+ const turnText = turnItem.text.replace(/\s+/g, " ").trim();
26038
+ return turnText === persistedText || Math.min(turnText.length, persistedText.length) >= 8 && (turnText.includes(persistedText) || persistedText.includes(turnText));
26039
+ });
26040
+ if (duplicate) {
26041
+ return false;
26042
+ }
26043
+ }
25909
26044
  if (item.kind !== "agentMessage") {
25910
26045
  return true;
25911
26046
  }
@@ -25940,6 +26075,7 @@ function mergePersistedHistoryItemsIntoTurns(turns, persistedItemsByTurnId, defe
25940
26075
  }
25941
26076
  const persistedItemWithTranscriptOrder = {
25942
26077
  ...persistedItem,
26078
+ ...turn.status === "completed" && persistedItem.status === "running" ? { status: "completed" } : {},
25943
26079
  transcriptOrder: transcriptIndex
25944
26080
  };
25945
26081
  if (shouldUsePersistedUserMessageText(item, persistedItemWithTranscriptOrder)) {
@@ -26023,6 +26159,7 @@ var ThreadLiveStateStore = class {
26023
26159
  threadLivePlans = /* @__PURE__ */ new Map();
26024
26160
  threadLiveItems = /* @__PURE__ */ new Map();
26025
26161
  threadTurnItemOrder = /* @__PURE__ */ new Map();
26162
+ threadTurnItemCreatedAt = /* @__PURE__ */ new Map();
26026
26163
  threadNextTurnItemSequence = /* @__PURE__ */ new Map();
26027
26164
  threadMaterializedAgentMessageCounts = /* @__PURE__ */ new Map();
26028
26165
  threadAgentMessageOrderingHints = /* @__PURE__ */ new Map();
@@ -26092,11 +26229,13 @@ var ThreadLiveStateStore = class {
26092
26229
  }
26093
26230
  resetRecordedTurnItemOrder(localThreadId, turnId) {
26094
26231
  this.threadTurnItemOrder.get(localThreadId)?.delete(turnId);
26232
+ this.threadTurnItemCreatedAt.get(localThreadId)?.delete(turnId);
26095
26233
  this.threadNextTurnItemSequence.get(localThreadId)?.delete(turnId);
26096
26234
  this.threadAgentMessageOrderingHints.get(localThreadId)?.delete(turnId);
26097
26235
  }
26098
26236
  clearRecordedTurnItemOrders(localThreadId) {
26099
26237
  this.threadTurnItemOrder.delete(localThreadId);
26238
+ this.threadTurnItemCreatedAt.delete(localThreadId);
26100
26239
  this.threadNextTurnItemSequence.delete(localThreadId);
26101
26240
  this.threadAgentMessageOrderingHints.delete(localThreadId);
26102
26241
  }
@@ -26125,6 +26264,24 @@ var ThreadLiveStateStore = class {
26125
26264
  turnOrder.set(itemId, sequence);
26126
26265
  return sequence;
26127
26266
  }
26267
+ recordTurnItemCreatedAt(localThreadId, turnId, itemId, createdAt) {
26268
+ let threadTimestamps = this.threadTurnItemCreatedAt.get(localThreadId);
26269
+ if (!threadTimestamps) {
26270
+ threadTimestamps = /* @__PURE__ */ new Map();
26271
+ this.threadTurnItemCreatedAt.set(localThreadId, threadTimestamps);
26272
+ }
26273
+ let turnTimestamps = threadTimestamps.get(turnId);
26274
+ if (!turnTimestamps) {
26275
+ turnTimestamps = /* @__PURE__ */ new Map();
26276
+ threadTimestamps.set(turnId, turnTimestamps);
26277
+ }
26278
+ const existing = turnTimestamps.get(itemId);
26279
+ if (existing) {
26280
+ return existing;
26281
+ }
26282
+ turnTimestamps.set(itemId, createdAt);
26283
+ return createdAt;
26284
+ }
26128
26285
  turnItemOrderSnapshot(localThreadId) {
26129
26286
  return this.threadTurnItemOrder.get(localThreadId) ?? /* @__PURE__ */ new Map();
26130
26287
  }
@@ -26243,7 +26400,19 @@ var ThreadLiveStateStore = class {
26243
26400
  appendLiveAgentMessageDelta(input) {
26244
26401
  const current = this.threadLiveItems.get(input.localThreadId);
26245
26402
  const currentItems = current?.turnId === input.turnId ? current.items : [];
26246
- const existing = currentItems.find((entry) => entry.id === input.itemId);
26403
+ const currentExisting = currentItems.find(
26404
+ (entry) => entry.id === input.itemId
26405
+ );
26406
+ const existing = currentExisting ?? (() => {
26407
+ const hint = this.threadAgentMessageOrderingHints.get(input.localThreadId)?.get(input.turnId)?.get(input.itemId);
26408
+ return hint ? {
26409
+ id: hint.id,
26410
+ kind: "agentMessage",
26411
+ text: hint.text,
26412
+ sequence: hint.sequence,
26413
+ ...hint.createdAt ? { createdAt: hint.createdAt } : {}
26414
+ } : void 0;
26415
+ })();
26247
26416
  const nextItem = existing?.kind === "agentMessage" ? {
26248
26417
  ...existing,
26249
26418
  text: `${existing.text}${input.delta}`,
@@ -26264,7 +26433,7 @@ var ThreadLiveStateStore = class {
26264
26433
  this.setLiveItems(input.localThreadId, {
26265
26434
  turnId: input.turnId,
26266
26435
  items: sortHistoryItemsBySequence(
26267
- existing ? currentItems.map(
26436
+ currentExisting ? currentItems.map(
26268
26437
  (entry) => entry.id === input.itemId ? nextItem : entry
26269
26438
  ) : [...currentItems, nextItem]
26270
26439
  ),
@@ -26974,8 +27143,17 @@ var ThreadRuntimeEventProjector = class {
26974
27143
  event.item.id
26975
27144
  );
26976
27145
  const eventTimestamp = (/* @__PURE__ */ new Date()).toISOString();
27146
+ const stableCreatedAt = liveState.recordTurnItemCreatedAt(
27147
+ record2.id,
27148
+ displayTurnId,
27149
+ event.item.id,
27150
+ event.item.createdAt ?? eventTimestamp
27151
+ );
26977
27152
  const orderedLiveItem = {
26978
- ...withHistoryItemCreatedAt2(event.item, eventTimestamp),
27153
+ ...withHistoryItemCreatedAt2(
27154
+ event.item,
27155
+ stableCreatedAt
27156
+ ),
26979
27157
  sequence
26980
27158
  };
26981
27159
  const transportLiveItem = deferHistoryItemDetailForTransport(orderedLiveItem);
@@ -27027,7 +27205,12 @@ var ThreadRuntimeEventProjector = class {
27027
27205
  displayTurnId,
27028
27206
  event.itemId
27029
27207
  );
27030
- const createdAt = (/* @__PURE__ */ new Date()).toISOString();
27208
+ const createdAt = liveState.recordTurnItemCreatedAt(
27209
+ record2.id,
27210
+ displayTurnId,
27211
+ event.itemId,
27212
+ (/* @__PURE__ */ new Date()).toISOString()
27213
+ );
27031
27214
  callbacks.appendLiveAgentMessageDelta({
27032
27215
  localThreadId: record2.id,
27033
27216
  turnId: displayTurnId,
@@ -27922,7 +28105,7 @@ function conversationMatchScore(turn, persistedItems) {
27922
28105
  if (!remoteUser || !persistedUser) {
27923
28106
  return 0;
27924
28107
  }
27925
- const userScore = remoteUser === persistedUser ? 2 : Math.min(remoteUser.length, persistedUser.length) >= 16 && (remoteUser.includes(persistedUser) || persistedUser.includes(remoteUser)) ? 1 : 0;
28108
+ const userScore = remoteUser === persistedUser ? 2 : Math.min(remoteUser.length, persistedUser.length) >= 8 && (remoteUser.includes(persistedUser) || persistedUser.includes(remoteUser)) ? 1 : 0;
27926
28109
  if (userScore === 0) {
27927
28110
  return 0;
27928
28111
  }
@@ -27954,7 +28137,7 @@ function historyItemMatchScore(remote, persisted) {
27954
28137
  if (remoteText === persistedText) {
27955
28138
  return 50;
27956
28139
  }
27957
- if ((remote.kind === "agentMessage" || remote.kind === "reasoning") && (remoteText.includes(persistedText) || persistedText.includes(remoteText))) {
28140
+ if ((remote.kind === "userMessage" || remote.kind === "agentMessage" || remote.kind === "reasoning") && Math.min(remoteText.length, persistedText.length) >= 8 && (remoteText.includes(persistedText) || persistedText.includes(remoteText))) {
27958
28141
  return 40;
27959
28142
  }
27960
28143
  return 0;
@@ -27966,7 +28149,7 @@ function alignHydratedItems(remoteItems, persistedItems, turnId) {
27966
28149
  persisted,
27967
28150
  index,
27968
28151
  score: historyItemMatchScore(remote, persisted)
27969
- })).filter((candidate) => candidate.score > 0).sort((left, right) => right.score - left.score || left.index - right.index)[0];
28152
+ })).filter((candidate) => candidate.score > 0).sort((left, right) => Number(left.persisted.id.startsWith("acp-hydrated:")) - Number(right.persisted.id.startsWith("acp-hydrated:")) || right.score - left.score || left.index - right.index)[0];
27970
28153
  if (!match) {
27971
28154
  return remote.sourceTurnId ? { ...remote, sourceTurnId: turnId } : remote;
27972
28155
  }
@@ -27974,6 +28157,12 @@ function alignHydratedItems(remoteItems, persistedItems, turnId) {
27974
28157
  return {
27975
28158
  ...remote,
27976
28159
  id: match.persisted.id,
28160
+ ...match.persisted.createdAt ? { createdAt: match.persisted.createdAt } : {},
28161
+ ...match.persisted.sequence !== void 0 ? { sequence: match.persisted.sequence } : {},
28162
+ ...remote.kind === "userMessage" ? {
28163
+ text: match.persisted.text,
28164
+ ...match.persisted.detailText !== void 0 ? { detailText: match.persisted.detailText } : {}
28165
+ } : {},
27977
28166
  ...remote.sourceTurnId || remote.kind === "agentMessage" ? { sourceTurnId: turnId } : {}
27978
28167
  };
27979
28168
  });
@@ -27993,14 +28182,16 @@ function alignHydratedAcpTurnsWithPersistedHistory(turns, persistedItemsByTurnId
27993
28182
  items,
27994
28183
  index,
27995
28184
  score: conversationMatchScore(turn, items)
27996
- })).filter((candidate) => candidate.score > 0).sort((left, right) => right.score - left.score || left.index - right.index)[0];
28185
+ })).filter((candidate) => candidate.score > 0).sort((left, right) => Number(left.turnId.startsWith("acp-hydrated:")) - Number(right.turnId.startsWith("acp-hydrated:")) || right.score - left.score || left.index - right.index)[0];
27997
28186
  if (!match) {
27998
28187
  return turn;
27999
28188
  }
28000
28189
  used.add(match.turnId);
28190
+ const persistedStartedAt = earliestItemCreatedAt(match.items);
28001
28191
  return {
28002
28192
  ...turn,
28003
28193
  providerTurnId: match.turnId,
28194
+ ...persistedStartedAt ? { startedAt: persistedStartedAt } : {},
28004
28195
  items: alignHydratedItems(turn.items, match.items, match.turnId)
28005
28196
  };
28006
28197
  });
@@ -28015,6 +28206,20 @@ function appendPersistedTurnsIfMissing(turns, persistedItemsByTurnId, metadataBy
28015
28206
  if (existingTurnIds.has(turnId)) {
28016
28207
  continue;
28017
28208
  }
28209
+ if (turns.some(
28210
+ (turn) => conversationMatchScore(
28211
+ {
28212
+ providerTurnId: turn.id,
28213
+ startedAt: turn.startedAt,
28214
+ status: turn.status,
28215
+ error: turn.error ? { message: turn.error } : null,
28216
+ items: turn.items
28217
+ },
28218
+ items
28219
+ ) > 0
28220
+ )) {
28221
+ continue;
28222
+ }
28018
28223
  const status = persistedAcpTurnStatus(items);
28019
28224
  const error = status === "failed" ? persistedTurnError(items) : null;
28020
28225
  missingTurns.push({
@@ -29120,8 +29325,13 @@ var ThreadSessionCoordinator = class {
29120
29325
  return { status: "bootstrap_unavailable", error };
29121
29326
  }
29122
29327
  const effectiveModel = input.resumeInput.model ?? input.currentModel ?? response.model ?? null;
29328
+ const resumedModelRecords = await this.listSessionModels({
29329
+ provider: input.provider,
29330
+ agentId: input.agentId,
29331
+ workspacePath: input.workspacePath
29332
+ }).catch(() => modelRecords);
29123
29333
  const resumedReasoning = this.providerRuntime.normalizeReasoningForModel(
29124
- modelRecords,
29334
+ resumedModelRecords,
29125
29335
  effectiveModel,
29126
29336
  normalizeReasoningEffort2(input.currentReasoningEffort) ?? normalizeReasoningEffort2(response.reasoningEffort)
29127
29337
  );
@@ -29468,6 +29678,9 @@ var ThreadHistoryPersistenceCoordinator = class {
29468
29678
  }
29469
29679
  }
29470
29680
  persistHydratedTurns(localThreadId, turns) {
29681
+ const existingItemsByTurnId = this.listPersistedHistoryItemsByTurnId(
29682
+ localThreadId
29683
+ );
29471
29684
  for (const turn of turns) {
29472
29685
  if (turn.startedAt) {
29473
29686
  upsertThreadTurnMetadata(this.db, {
@@ -29480,14 +29693,15 @@ var ThreadHistoryPersistenceCoordinator = class {
29480
29693
  if (!shouldPersistRuntimeFinalHistoryItem(item)) {
29481
29694
  return;
29482
29695
  }
29483
- const createdAt = item.createdAt ?? turn.startedAt;
29696
+ const existingItem = existingItemsByTurnId.get(turn.providerTurnId)?.find((candidate) => candidate.id === item.id);
29697
+ const createdAt = existingItem?.createdAt ?? item.createdAt ?? turn.startedAt;
29484
29698
  this.persistProjectedHistoryItem(
29485
29699
  localThreadId,
29486
29700
  turn.providerTurnId,
29487
29701
  {
29488
29702
  ...item,
29489
29703
  ...createdAt ? { createdAt } : {},
29490
- sequence: item.sequence ?? index,
29704
+ sequence: existingItem?.sequence ?? item.sequence ?? index,
29491
29705
  ...item.kind === "agentMessage" && !item.sourceTurnId ? { sourceTurnId: turn.providerTurnId } : {}
29492
29706
  }
29493
29707
  );
@@ -31420,6 +31634,30 @@ function canUseRuntimePagedTurns(cachedDetail, enrichedTurns, options) {
31420
31634
  }
31421
31635
  return cachedDetail.totalTurnCount > enrichedTurns.length;
31422
31636
  }
31637
+ function latestTurnItemTimestamp(turn) {
31638
+ return turn.items.map((item) => item.createdAt).filter((value) => Boolean(value)).sort().at(-1) ?? turn.startedAt ?? null;
31639
+ }
31640
+ function localImportThreadStatusPatch(record2, turns) {
31641
+ const latestTurn = turns.at(-1);
31642
+ if (!latestTurn) {
31643
+ return null;
31644
+ }
31645
+ const status = latestTurn.status === "inProgress" ? "running" : latestTurn.status === "completed" ? "idle" : latestTurn.status;
31646
+ const providerTurnId = latestTurn.status === "inProgress" ? latestTurn.id : null;
31647
+ const lastError = latestTurn.status === "failed" ? latestTurn.error : null;
31648
+ const lastTurnStartedAt = latestTurn.startedAt ?? record2.lastTurnStartedAt ?? null;
31649
+ const lastTurnCompletedAt = latestTurn.status === "inProgress" ? null : latestTurnItemTimestamp(latestTurn) ?? record2.lastTurnCompletedAt ?? null;
31650
+ if (record2.status === status && record2.providerTurnId === providerTurnId && record2.lastError === lastError && record2.lastTurnStartedAt === lastTurnStartedAt && record2.lastTurnCompletedAt === lastTurnCompletedAt) {
31651
+ return null;
31652
+ }
31653
+ return {
31654
+ status,
31655
+ providerTurnId,
31656
+ lastError,
31657
+ lastTurnStartedAt,
31658
+ lastTurnCompletedAt
31659
+ };
31660
+ }
31423
31661
  var ThreadService = class {
31424
31662
  constructor(db, agentRuntimes, eventBus, localSessionStore, workspaceRoot, providerManagement, pluginService, config) {
31425
31663
  this.db = db;
@@ -31786,6 +32024,9 @@ var ThreadService = class {
31786
32024
  if (!local) {
31787
32025
  continue;
31788
32026
  }
32027
+ if (local.source === "local_codex_import" && local.isConnected === false) {
32028
+ continue;
32029
+ }
31789
32030
  updateThreadRecord(
31790
32031
  this.db,
31791
32032
  local.id,
@@ -31861,6 +32102,12 @@ var ThreadService = class {
31861
32102
  turnMetadataById,
31862
32103
  options
31863
32104
  });
32105
+ if (record2.source === "local_codex_import" && record2.isConnected === false) {
32106
+ const statusPatch = localImportThreadStatusPatch(record2, cachedDetail.turns);
32107
+ if (statusPatch) {
32108
+ updateThreadRecord(this.db, record2.id, statusPatch);
32109
+ }
32110
+ }
31864
32111
  const updated = getThreadRecordById(this.db, record2.id);
31865
32112
  const enrichedTurns = this.pluginService?.enrichTurnsWithArtifacts({
31866
32113
  threadId: updated.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-codex",
3
- "version": "0.11.54",
3
+ "version": "0.11.55",
4
4
  "description": "Local web supervisor for Codex workspaces and threads.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -168,6 +168,62 @@ describe('AcpCatalogRuntimeAdapter', () => {
168
168
  expect(forked.agentId).toBe('fixture-agent');
169
169
  }, 15_000);
170
170
 
171
+ it('refreshes command models with reasoning options from a close-only session', async () => {
172
+ const fixture = path.resolve('src/test/fixtures/fake-acp-agent.mjs');
173
+ vi.stubEnv('REMOTE_CODEX_FAKE_ACP_NO_SESSION_DELETE', '1');
174
+ const runtime = new AcpCatalogRuntimeAdapter({
175
+ catalog: new AcpAgentCatalog({
176
+ definitions: [{
177
+ id: 'grok-fixture',
178
+ displayName: 'Grok fixture',
179
+ description: 'Close-only ACP fixture',
180
+ transport: 'native',
181
+ baseCommand: process.execPath,
182
+ baseProbeCommand: `"${process.execPath}" --version`,
183
+ serverCommand: `"${process.execPath}" "${fixture}"`,
184
+ serverProbeCommand: `"${process.execPath}" --version`,
185
+ installCommand: null,
186
+ modelListCommand: `"${process.execPath}" -e "console.log('Default model: fixture-model\\nAvailable models:\\n * fixture-model (default)\\n - fixture-fast')"`,
187
+ }],
188
+ }),
189
+ startupTimeoutMs: 5_000,
190
+ });
191
+ runtimes.push(runtime);
192
+ await runtime.start();
193
+
194
+ expect(await runtime.listModelsForAgent('grok-fixture', process.cwd())).toEqual(
195
+ expect.arrayContaining([
196
+ expect.objectContaining({
197
+ model: 'fixture-model',
198
+ supportedReasoningEfforts: [],
199
+ }),
200
+ ]),
201
+ );
202
+
203
+ await runtime.startSession({
204
+ cwd: process.cwd(),
205
+ agentId: 'grok-fixture',
206
+ model: 'fixture-model',
207
+ reasoningEffort: 'high',
208
+ approvalMode: 'yolo',
209
+ });
210
+
211
+ expect(await runtime.listModelsForAgent('grok-fixture', process.cwd())).toEqual(
212
+ expect.arrayContaining([
213
+ expect.objectContaining({
214
+ model: 'fixture-model',
215
+ supportedReasoningEfforts: [
216
+ expect.objectContaining({ reasoningEffort: 'low' }),
217
+ expect.objectContaining({ reasoningEffort: 'medium' }),
218
+ expect.objectContaining({ reasoningEffort: 'high' }),
219
+ ],
220
+ defaultReasoningEffort: 'high',
221
+ }),
222
+ expect.objectContaining({ model: 'fixture-fast' }),
223
+ ]),
224
+ );
225
+ });
226
+
171
227
  it('delegates Codex child controls while preserving scoped goal identity', async () => {
172
228
  const fixture = path.resolve('src/test/fixtures/fake-acp-agent.mjs');
173
229
  vi.stubEnv('REMOTE_CODEX_FAKE_ACP_AGENT_KIND', 'codex');
@@ -255,7 +255,21 @@ export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRunti
255
255
  if (models.length === 1 && models[0]?.model === 'default') {
256
256
  const commandModels = await this.catalog.listCommandModels(agentId);
257
257
  if (commandModels.length > 0) {
258
- models = commandModels;
258
+ const discoveredDefaults = models[0];
259
+ models = commandModels.map((model) => ({
260
+ ...model,
261
+ supportsPerformanceMode:
262
+ model.supportsPerformanceMode ??
263
+ discoveredDefaults.supportsPerformanceMode ??
264
+ false,
265
+ supportedReasoningEfforts:
266
+ model.supportedReasoningEfforts.length > 0
267
+ ? model.supportedReasoningEfforts
268
+ : discoveredDefaults.supportedReasoningEfforts,
269
+ defaultReasoningEffort:
270
+ model.defaultReasoningEffort ??
271
+ discoveredDefaults.defaultReasoningEffort,
272
+ }));
259
273
  }
260
274
  }
261
275
  this.modelCache.set(cacheKey, { at: Date.now(), models });
@@ -381,6 +395,7 @@ export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRunti
381
395
  ? input.model
382
396
  : probedDefaultModel ?? input.model;
383
397
  this.refreshAgentSessionCapabilities(agentId, agent);
398
+ this.modelCache.clear();
384
399
  const session = scopedSession(agentId, response.session);
385
400
  return {
386
401
  ...response,
@@ -407,6 +422,7 @@ export class AcpCatalogRuntimeAdapter extends EventEmitter implements AgentRunti
407
422
  throw error;
408
423
  }
409
424
  this.refreshAgentSessionCapabilities(owner.agentId, agent);
425
+ this.modelCache.clear();
410
426
  const session = scopedSession(owner.agentId, response.session);
411
427
  return {
412
428
  ...response,
@@ -350,6 +350,112 @@ describe('AcpRuntimeAdapter', () => {
350
350
  await expect(adapter.listSessions()).resolves.toEqual([]);
351
351
  });
352
352
 
353
+ it('reads reasoning options from a loaded close-only agent session', async () => {
354
+ const adapter = new AcpRuntimeAdapter({
355
+ command: `"${process.execPath}" "${fixture}"`,
356
+ env: { REMOTE_CODEX_FAKE_ACP_NO_SESSION_DELETE: '1' },
357
+ startupTimeoutMs: 5_000,
358
+ });
359
+ adapters.push(adapter);
360
+ await adapter.start();
361
+
362
+ const started = await adapter.startSession({
363
+ cwd: process.cwd(),
364
+ model: 'fixture-model',
365
+ reasoningEffort: 'high',
366
+ approvalMode: 'yolo',
367
+ });
368
+
369
+ await expect(adapter.inspectModelOptions(process.cwd())).resolves.toEqual([
370
+ expect.objectContaining({
371
+ model: 'default',
372
+ supportedReasoningEfforts: [
373
+ expect.objectContaining({ reasoningEffort: 'low' }),
374
+ expect.objectContaining({ reasoningEffort: 'medium' }),
375
+ expect.objectContaining({ reasoningEffort: 'high' }),
376
+ ],
377
+ defaultReasoningEffort: 'high',
378
+ }),
379
+ ]);
380
+ await expect(adapter.listSessions()).resolves.toEqual([
381
+ expect.objectContaining({ providerSessionId: started.providerSessionId }),
382
+ ]);
383
+ });
384
+
385
+ it('maps and writes Grok-style legacy model reasoning metadata', async () => {
386
+ const adapter = new AcpRuntimeAdapter({
387
+ command: `"${process.execPath}" "${fixture}"`,
388
+ env: {
389
+ REMOTE_CODEX_FAKE_ACP_NO_SESSION_DELETE: '1',
390
+ REMOTE_CODEX_FAKE_ACP_LEGACY_MODELS: '1',
391
+ REMOTE_CODEX_FAKE_ACP_SKIP_PERMISSION: '1',
392
+ },
393
+ startupTimeoutMs: 5_000,
394
+ });
395
+ adapters.push(adapter);
396
+ await adapter.start();
397
+
398
+ const started = await adapter.startSession({
399
+ cwd: process.cwd(),
400
+ model: 'fixture-fast',
401
+ reasoningEffort: 'low',
402
+ approvalMode: 'yolo',
403
+ });
404
+
405
+ expect(started).toMatchObject({
406
+ model: 'fixture-fast',
407
+ reasoningEffort: 'low',
408
+ rawSession: {
409
+ models: {
410
+ availableModels: [
411
+ expect.objectContaining({
412
+ _meta: expect.objectContaining({ reasoningEffort: 'low' }),
413
+ }),
414
+ expect.anything(),
415
+ ],
416
+ },
417
+ },
418
+ });
419
+ await expect(adapter.inspectModelOptions(process.cwd())).resolves.toEqual([
420
+ expect.objectContaining({
421
+ model: 'fixture-model',
422
+ supportedReasoningEfforts: [
423
+ expect.objectContaining({ reasoningEffort: 'high' }),
424
+ expect.objectContaining({ reasoningEffort: 'medium' }),
425
+ expect.objectContaining({ reasoningEffort: 'low' }),
426
+ ],
427
+ }),
428
+ expect.objectContaining({
429
+ model: 'fixture-fast',
430
+ defaultReasoningEffort: 'low',
431
+ supportedReasoningEfforts: [
432
+ expect.objectContaining({ reasoningEffort: 'high' }),
433
+ expect.objectContaining({ reasoningEffort: 'low' }),
434
+ ],
435
+ }),
436
+ ]);
437
+
438
+ const completed = new Promise<void>((resolve) => {
439
+ adapter.on('event', (event: AgentRuntimeEvent) => {
440
+ if (event.type === 'turn.completed') resolve();
441
+ });
442
+ });
443
+ await adapter.startTurn({
444
+ providerSessionId: started.providerSessionId,
445
+ prompt: 'Verify changed effort.',
446
+ reasoningEffort: 'high',
447
+ });
448
+ await completed;
449
+ await expect(adapter.inspectModelOptions(process.cwd())).resolves.toEqual(
450
+ expect.arrayContaining([
451
+ expect.objectContaining({
452
+ model: 'fixture-fast',
453
+ defaultReasoningEffort: 'high',
454
+ }),
455
+ ]),
456
+ );
457
+ });
458
+
353
459
  it('deletes temporary model-probe sessions', async () => {
354
460
  const adapter = new AcpRuntimeAdapter({
355
461
  command: `"${process.execPath}" "${fixture}"`,
@@ -79,11 +79,33 @@ interface AcpSessionState {
79
79
  activeMapper: AcpTurnItemMapper | null;
80
80
  modes: acp.SessionModeState | null;
81
81
  configOptions: acp.SessionConfigOption[];
82
+ legacyModels: AcpLegacyModelState | null;
82
83
  availableCommands: acp.AvailableCommand[];
83
84
  hydrationCoverage: AgentSessionHistoryCoverage | null;
84
85
  goal: AgentGoal | null;
85
86
  }
86
87
 
88
+ interface AcpLegacyModelState {
89
+ currentModelId?: string;
90
+ availableModels?: AcpLegacyModelInfo[];
91
+ }
92
+
93
+ interface AcpLegacyModelInfo {
94
+ modelId: string;
95
+ name?: string;
96
+ description?: string;
97
+ _meta?: {
98
+ reasoningEffort?: string;
99
+ reasoningEfforts?: Array<{
100
+ id?: string;
101
+ value?: string;
102
+ label?: string;
103
+ description?: string;
104
+ default?: boolean;
105
+ }>;
106
+ };
107
+ }
108
+
87
109
  interface PendingPermission {
88
110
  params: acp.RequestPermissionRequest;
89
111
  resolve: (response: acp.RequestPermissionResponse) => void;
@@ -239,6 +261,60 @@ function reasoningOptions(configOptions: acp.SessionConfigOption[]) {
239
261
  };
240
262
  }
241
263
 
264
+ function legacyModelsFromResponse(response: unknown): AcpLegacyModelState | null {
265
+ if (!response || typeof response !== 'object') {
266
+ return null;
267
+ }
268
+ const models = (response as { models?: unknown }).models;
269
+ if (!models || typeof models !== 'object') {
270
+ return null;
271
+ }
272
+ const availableModels = (models as { availableModels?: unknown }).availableModels;
273
+ return Array.isArray(availableModels)
274
+ ? models as AcpLegacyModelState
275
+ : null;
276
+ }
277
+
278
+ function legacyModelOptions(models: AcpLegacyModelState): AgentModel[] {
279
+ const currentModelId = models.currentModelId ?? null;
280
+ return (models.availableModels ?? []).flatMap((model, index) => {
281
+ if (!model.modelId) {
282
+ return [];
283
+ }
284
+ const efforts = (model._meta?.reasoningEfforts ?? []).flatMap((entry) => {
285
+ const reasoningEffort = normalizeAcpEffort(entry.value ?? entry.id);
286
+ return reasoningEffort
287
+ ? [{
288
+ reasoningEffort,
289
+ description: entry.description ?? entry.label ?? '',
290
+ }]
291
+ : [];
292
+ });
293
+ const declaredDefault = model._meta?.reasoningEfforts?.find(
294
+ (entry) => entry.default,
295
+ );
296
+ return [{
297
+ id: model.modelId,
298
+ model: model.modelId,
299
+ displayName: model.name ?? model.modelId,
300
+ description: model.description ?? '',
301
+ isDefault:
302
+ model.modelId === currentModelId || (!currentModelId && index === 0),
303
+ hidden: false,
304
+ supportedReasoningEfforts: efforts,
305
+ defaultReasoningEffort:
306
+ normalizeAcpEffort(model._meta?.reasoningEffort) ??
307
+ normalizeAcpEffort(declaredDefault?.value ?? declaredDefault?.id),
308
+ selectionKind: 'model',
309
+ }];
310
+ });
311
+ }
312
+
313
+ function selectedLegacyModel(state: AcpSessionState) {
314
+ const models = state.legacyModels?.availableModels ?? [];
315
+ return models.find((model) => model.modelId === state.model) ?? models[0] ?? null;
316
+ }
317
+
242
318
  function permissionOptions(params: acp.RequestPermissionRequest) {
243
319
  return params.options.map((option) => ({
244
320
  id: option.optionId,
@@ -578,7 +654,34 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
578
654
  const sessionCapabilities =
579
655
  this.initializeResponse?.agentCapabilities?.sessionCapabilities;
580
656
  if (!sessionCapabilities?.delete) {
581
- return this.listModels();
657
+ const normalizedCwd = path.resolve(cwd);
658
+ const loadedSession = [...this.sessions.values()].find(
659
+ (session) =>
660
+ session.cwd === normalizedCwd &&
661
+ (session.configOptions.length > 0 || session.legacyModels !== null),
662
+ );
663
+ if (!loadedSession) {
664
+ return this.listModels();
665
+ }
666
+ if (loadedSession.legacyModels) {
667
+ return legacyModelOptions(loadedSession.legacyModels);
668
+ }
669
+ const reasoning = reasoningOptions(loadedSession.configOptions);
670
+ const supportsPerformanceMode = loadedSession.configOptions.some(
671
+ (option) => option.id === 'fast-mode' || option.category === 'model_config',
672
+ );
673
+ return [{
674
+ id: 'default',
675
+ model: 'default',
676
+ displayName: 'Agent default',
677
+ description: 'Use the model configured by the ACP agent.',
678
+ isDefault: true,
679
+ hidden: false,
680
+ supportsPerformanceMode,
681
+ supportedReasoningEfforts: reasoning.efforts,
682
+ defaultReasoningEffort: reasoning.defaultEffort,
683
+ selectionKind: 'model',
684
+ }];
582
685
  }
583
686
  const context = await this.requireContext();
584
687
  const response = await context.request(acp.methods.agent.session.new, {
@@ -592,6 +695,10 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
592
695
  (option) => option.id === 'fast-mode' || option.category === 'model_config',
593
696
  );
594
697
  try {
698
+ const legacyModels = legacyModelsFromResponse(response);
699
+ if (legacyModels) {
700
+ return legacyModelOptions(legacyModels);
701
+ }
595
702
  const modelOption = configOptionByCategory(configOptions, 'model');
596
703
  if (!modelOption || modelOption.type !== 'select') {
597
704
  const reasoning = reasoningOptions(configOptions);
@@ -792,6 +899,7 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
792
899
  }
793
900
  forked.modes = response.modes ?? forked.modes;
794
901
  forked.configOptions = response.configOptions ?? forked.configOptions;
902
+ forked.legacyModels = legacyModelsFromResponse(response) ?? forked.legacyModels;
795
903
  this.syncStateFromConfigOptions(forked);
796
904
  return sessionDetail(forked);
797
905
  }
@@ -880,6 +988,9 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
880
988
  mcpServers: [],
881
989
  _meta: {
882
990
  yoloMode: input.approvalMode === 'yolo',
991
+ ...(input.reasoningEffort
992
+ ? { reasoningEffort: input.reasoningEffort }
993
+ : {}),
883
994
  },
884
995
  });
885
996
  const now = new Date().toISOString();
@@ -897,6 +1008,7 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
897
1008
  activeMapper: null,
898
1009
  modes: response.modes ?? null,
899
1010
  configOptions: response.configOptions ?? [],
1011
+ legacyModels: legacyModelsFromResponse(response),
900
1012
  availableCommands: [],
901
1013
  hydrationCoverage: null,
902
1014
  goal: null,
@@ -1134,6 +1246,7 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
1134
1246
  activeMapper: null,
1135
1247
  modes: null,
1136
1248
  configOptions: [],
1249
+ legacyModels: null,
1137
1250
  availableCommands: [],
1138
1251
  hydrationCoverage: null,
1139
1252
  goal: null,
@@ -1152,6 +1265,7 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
1152
1265
  });
1153
1266
  state.modes = response.modes ?? null;
1154
1267
  state.configOptions = response.configOptions ?? [];
1268
+ state.legacyModels = legacyModelsFromResponse(response);
1155
1269
  state.turns = hydrator.complete();
1156
1270
  state.hydrationCoverage = hydrator.coverage();
1157
1271
  this.syncStateFromConfigOptions(state);
@@ -1166,6 +1280,7 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
1166
1280
  });
1167
1281
  state.modes = response.modes ?? null;
1168
1282
  state.configOptions = response.configOptions ?? [];
1283
+ state.legacyModels = legacyModelsFromResponse(response);
1169
1284
  this.syncStateFromConfigOptions(state);
1170
1285
  } else {
1171
1286
  throw new Error('ACP agent does not support session/load or session/resume.');
@@ -1195,7 +1310,10 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
1195
1310
  if (model && model !== 'default') {
1196
1311
  state.model = await this.setConfigOption(state, 'model', model);
1197
1312
  }
1198
- if (reasoningEffort) {
1313
+ if (
1314
+ reasoningEffort &&
1315
+ normalizeAcpEffort(reasoningEffort) !== state.reasoningEffort
1316
+ ) {
1199
1317
  const applied = await this.setConfigOption(
1200
1318
  state,
1201
1319
  'thought_level',
@@ -1239,17 +1357,42 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
1239
1357
  category: 'model' | 'thought_level',
1240
1358
  value: string,
1241
1359
  ) {
1242
- const option = state.configOptions.find((candidate) =>
1243
- candidate.category === category || candidate.id.toLowerCase().includes(category === 'model' ? 'model' : 'thought'),
1244
- );
1360
+ const option = configOptionByCategory(state.configOptions, category);
1245
1361
  if ((!option || option.type !== 'select') && category === 'model') {
1246
1362
  const context = await this.requireContext();
1247
1363
  await context.request('session/set_model', {
1248
1364
  sessionId: state.providerSessionId,
1249
1365
  modelId: value,
1250
1366
  });
1367
+ if (state.legacyModels) {
1368
+ state.legacyModels.currentModelId = value;
1369
+ }
1251
1370
  return value;
1252
1371
  }
1372
+ if ((!option || option.type !== 'select') && category === 'thought_level') {
1373
+ const legacyModel = selectedLegacyModel(state);
1374
+ const selected = legacyModel?._meta?.reasoningEfforts?.find(
1375
+ (entry) =>
1376
+ normalizeAcpEffort(entry.value ?? entry.id) ===
1377
+ normalizeAcpEffort(value),
1378
+ );
1379
+ const modeId = selected?.value ?? selected?.id;
1380
+ if (modeId) {
1381
+ const context = await this.requireContext();
1382
+ const response = await context.request(acp.methods.agent.session.load, {
1383
+ sessionId: state.providerSessionId,
1384
+ cwd: state.cwd,
1385
+ mcpServers: [],
1386
+ _meta: { reasoningEffort: modeId },
1387
+ });
1388
+ state.modes = response.modes ?? state.modes;
1389
+ state.configOptions = response.configOptions ?? state.configOptions;
1390
+ state.legacyModels =
1391
+ legacyModelsFromResponse(response) ?? state.legacyModels;
1392
+ this.syncStateFromConfigOptions(state);
1393
+ return state.reasoningEffort ?? modeId;
1394
+ }
1395
+ }
1253
1396
  if (!option || option.type !== 'select') {
1254
1397
  throw new AgentRuntimeError(
1255
1398
  `ACP agent does not expose a ${category} config option.`,
@@ -1293,6 +1436,20 @@ export class AcpRuntimeAdapter extends EventEmitter implements AgentRuntime {
1293
1436
  if (thoughtOption?.type === 'select') {
1294
1437
  state.reasoningEffort = normalizeAcpEffort(thoughtOption.currentValue);
1295
1438
  }
1439
+ if (!modelOption && state.legacyModels) {
1440
+ state.model = state.legacyModels.currentModelId ??
1441
+ state.legacyModels.availableModels?.[0]?.modelId ??
1442
+ null;
1443
+ }
1444
+ if (!thoughtOption) {
1445
+ const legacyModel = selectedLegacyModel(state);
1446
+ const declaredDefault = legacyModel?._meta?.reasoningEfforts?.find(
1447
+ (entry) => entry.default,
1448
+ );
1449
+ state.reasoningEffort =
1450
+ normalizeAcpEffort(legacyModel?._meta?.reasoningEffort) ??
1451
+ normalizeAcpEffort(declaredDefault?.value ?? declaredDefault?.id);
1452
+ }
1296
1453
  }
1297
1454
 
1298
1455
  private updateCapabilitiesFromConfigOptions(
@@ -25,6 +25,7 @@ const agentKind = process.env.REMOTE_CODEX_FAKE_ACP_AGENT_KIND ?? 'fixture';
25
25
  const supportsFork = process.env.REMOTE_CODEX_FAKE_ACP_FORK === '1';
26
26
  const noSessionCleanup = process.env.REMOTE_CODEX_FAKE_ACP_NO_SESSION_CLEANUP === '1';
27
27
  const noSessionDelete = process.env.REMOTE_CODEX_FAKE_ACP_NO_SESSION_DELETE === '1';
28
+ const legacyModels = process.env.REMOTE_CODEX_FAKE_ACP_LEGACY_MODELS === '1';
28
29
  const goalVersion = process.env.REMOTE_CODEX_FAKE_ACP_GOAL_VERSION ?? '1';
29
30
  const goalActions = (process.env.REMOTE_CODEX_FAKE_ACP_GOAL_ACTIONS ?? 'get,set,clear')
30
31
  .split(',')
@@ -90,6 +91,38 @@ function modes(session) {
90
91
  }
91
92
 
92
93
  function responseState(session) {
94
+ if (legacyModels) {
95
+ return {
96
+ models: {
97
+ currentModelId: session.config.model,
98
+ availableModels: [
99
+ {
100
+ modelId: 'fixture-model',
101
+ name: 'Fixture model',
102
+ _meta: {
103
+ reasoningEffort: session.config.thought,
104
+ reasoningEfforts: [
105
+ { id: 'high', value: 'high', label: 'High', default: true },
106
+ { id: 'medium', value: 'medium', label: 'Medium' },
107
+ { id: 'low', value: 'low', label: 'Low' },
108
+ ],
109
+ },
110
+ },
111
+ {
112
+ modelId: 'fixture-fast',
113
+ name: 'Fixture fast model',
114
+ _meta: {
115
+ reasoningEffort: session.config.thought,
116
+ reasoningEfforts: [
117
+ { id: 'high', value: 'high', label: 'High', default: true },
118
+ { id: 'low', value: 'low', label: 'Low' },
119
+ ],
120
+ },
121
+ },
122
+ ],
123
+ },
124
+ };
125
+ }
93
126
  return {
94
127
  modes: modes(session),
95
128
  configOptions: configOptions(session),
@@ -290,13 +323,20 @@ acp.agent({ name: 'remote-codex-fake-acp-agent' })
290
323
  }))
291
324
  .onRequest(acp.methods.agent.session.new, async (context) => {
292
325
  const now = new Date().toISOString();
326
+ const config = defaultConfig();
327
+ if (
328
+ legacyModels &&
329
+ typeof context.params._meta?.reasoningEffort === 'string'
330
+ ) {
331
+ config.thought = context.params._meta.reasoningEffort;
332
+ }
293
333
  const session = {
294
334
  sessionId: randomUUID(),
295
335
  cwd: path.resolve(context.params.cwd),
296
336
  title: 'Fixture session',
297
337
  createdAt: now,
298
338
  updatedAt: now,
299
- config: defaultConfig(),
339
+ config,
300
340
  turns: [],
301
341
  pendingPrompt: null,
302
342
  };
@@ -315,6 +355,12 @@ acp.agent({ name: 'remote-codex-fake-acp-agent' })
315
355
  }))
316
356
  .onRequest(acp.methods.agent.session.load, async (context) => {
317
357
  const session = requireSession(context.params.sessionId);
358
+ if (
359
+ legacyModels &&
360
+ typeof context.params._meta?.reasoningEffort === 'string'
361
+ ) {
362
+ session.config.thought = context.params._meta.reasoningEffort;
363
+ }
318
364
  await replaySession(context.client, session);
319
365
  return responseState(session);
320
366
  })
@@ -348,7 +394,15 @@ acp.agent({ name: 'remote-codex-fake-acp-agent' })
348
394
  })
349
395
  .onRequest(acp.methods.agent.session.setMode, async (context) => {
350
396
  const session = requireSession(context.params.sessionId);
351
- session.config.mode = context.params.modeId;
397
+ if (legacyModels) session.config.thought = context.params.modeId;
398
+ else session.config.mode = context.params.modeId;
399
+ session.updatedAt = new Date().toISOString();
400
+ await saveState();
401
+ return {};
402
+ })
403
+ .onRequest('session/set_model', (params) => params, async (context) => {
404
+ const session = requireSession(context.params.sessionId);
405
+ session.config.model = context.params.modelId;
352
406
  session.updatedAt = new Date().toISOString();
353
407
  await saveState();
354
408
  return {};
@@ -55,6 +55,27 @@ describe('codex history item persistence policy', () => {
55
55
  ).toEqual([{ ...finalMessage, transcriptOrder: 0 }]);
56
56
  });
57
57
 
58
+ it('omits empty assistant and reasoning rows from runtime history', () => {
59
+ const finalMessage: AgentHistoryItem = {
60
+ id: 'agent-final-1',
61
+ kind: 'agentMessage',
62
+ text: 'Final answer',
63
+ };
64
+
65
+ expect(
66
+ agentTurnToThreadTurnDto({
67
+ providerTurnId: 'turn-1',
68
+ status: 'completed',
69
+ error: null,
70
+ items: [
71
+ { id: 'reasoning-empty', kind: 'reasoning', text: ' \n' },
72
+ { id: 'agent-empty', kind: 'agentMessage', text: '' },
73
+ finalMessage,
74
+ ],
75
+ }).items,
76
+ ).toEqual([{ ...finalMessage, transcriptOrder: 0 }]);
77
+ });
78
+
58
79
  it('maps Codex collab agent tool calls to dedicated agent tool call items', () => {
59
80
  const turn = codexTurnToAgentTurn({
60
81
  id: 'turn-1',
@@ -541,14 +541,21 @@ export function shouldPersistRuntimeFinalHistoryItem(
541
541
  }
542
542
 
543
543
  function visibleRuntimeTurnItems(items: ThreadHistoryItemDto[]) {
544
- const hasFinalAgentMessage = items.some(
544
+ const nonEmptyItems = items.filter(
545
+ (item) =>
546
+ !(
547
+ (item.kind === 'agentMessage' || item.kind === 'reasoning') &&
548
+ item.text.trim().length === 0
549
+ ),
550
+ );
551
+ const hasFinalAgentMessage = nonEmptyItems.some(
545
552
  (item) => item.kind === 'agentMessage' && !isTransientAgentHistoryItem(item),
546
553
  );
547
554
  if (!hasFinalAgentMessage) {
548
- return items;
555
+ return nonEmptyItems;
549
556
  }
550
557
 
551
- return items.filter(
558
+ return nonEmptyItems.filter(
552
559
  (item) => !(item.kind === 'agentMessage' && isTransientAgentHistoryItem(item)),
553
560
  );
554
561
  }