remote-codex 0.11.41 → 0.11.43

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.
@@ -10,16 +10,16 @@
10
10
  <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
11
11
  <link rel="manifest" href="/site.webmanifest" />
12
12
  <title>Remote Codex</title>
13
- <script type="module" crossorigin src="/assets/index-B-qj7e8G.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-BT0SM9C-.js"></script>
14
14
  <link rel="modulepreload" crossorigin href="/assets/react-vendor-Dfg_6BLf.js">
15
15
  <link rel="modulepreload" crossorigin href="/assets/ui-vendor-CuR8GHb0.js">
16
16
  <link rel="modulepreload" crossorigin href="/assets/graph-vendor-DVQUpZ8C.js">
17
17
  <link rel="modulepreload" crossorigin href="/assets/terminal-vendor-C5bTa-Ka.js">
18
18
  <link rel="modulepreload" crossorigin href="/assets/markdown-vendor-RZk8L7-L.js">
19
- <link rel="modulepreload" crossorigin href="/assets/thread-ui-xWvswa2v.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/thread-ui-Dmrigdek.js">
20
20
  <link rel="stylesheet" crossorigin href="/assets/graph-vendor-C5ap-Sga.css">
21
21
  <link rel="stylesheet" crossorigin href="/assets/terminal-vendor-Beg8tuEN.css">
22
- <link rel="stylesheet" crossorigin href="/assets/index-CDFuOeJN.css">
22
+ <link rel="stylesheet" crossorigin href="/assets/index-BcCLYWAf.css">
23
23
  </head>
24
24
  <body class="bg-stone-950">
25
25
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-codex",
3
- "version": "0.11.41",
3
+ "version": "0.11.43",
4
4
  "description": "Local web supervisor for Codex workspaces and threads.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -88,6 +88,7 @@ class FakeQuery implements Query {
88
88
  return [
89
89
  {
90
90
  value: 'claude-sonnet-4-5',
91
+ resolvedModel: 'claude-sonnet-4-5-20250929',
91
92
  displayName: 'Claude Sonnet 4.5',
92
93
  description: 'Test model',
93
94
  supportsEffort: true,
@@ -801,14 +802,17 @@ describe('ClaudeRuntimeAdapter', () => {
801
802
  await expect(adapter.listLoadedSessions()).resolves.toEqual(['claude-session-1']);
802
803
  });
803
804
 
804
- it('exposes Claude Code model aliases and enables the 1M context beta', async () => {
805
+ it('discovers versioned Claude Code model labels while idle and enables the 1M context beta', async () => {
805
806
  const sdkOptions: Record<string, unknown>[] = [];
807
+ const queries: FakeQuery[] = [];
806
808
  const adapter = new ClaudeRuntimeAdapter({
807
809
  home: '/tmp/claude-home',
808
810
  command: 'claude',
809
811
  query: ((params: { prompt: string; options: Record<string, unknown> }) => {
810
812
  sdkOptions.push(params.options);
811
- return new FakeQuery([systemInit(), result()]);
813
+ const query = new FakeQuery([systemInit(), result()]);
814
+ queries.push(query);
815
+ return query;
812
816
  }) as any,
813
817
  listSessions: (async () => [] satisfies SDKSessionInfo[]) as any,
814
818
  getSessionInfo: (async () => null) as any,
@@ -819,12 +823,12 @@ describe('ClaudeRuntimeAdapter', () => {
819
823
  expect.arrayContaining([
820
824
  expect.objectContaining({
821
825
  model: 'sonnet',
822
- displayName: 'Claude Sonnet',
826
+ displayName: 'Sonnet · 4.5',
823
827
  isDefault: true,
824
828
  }),
825
829
  expect.objectContaining({
826
830
  model: 'sonnet[1m]',
827
- displayName: 'Claude Sonnet 1M',
831
+ displayName: 'Sonnet · 4.5 (1M context)',
828
832
  }),
829
833
  expect.objectContaining({
830
834
  model: 'fable',
@@ -836,6 +840,9 @@ describe('ClaudeRuntimeAdapter', () => {
836
840
  }),
837
841
  ]),
838
842
  );
843
+ expect(queries[0]?.closed).toBe(true);
844
+ await adapter.listModels();
845
+ expect(queries).toHaveLength(1);
839
846
 
840
847
  await adapter.startSession({
841
848
  cwd: '/tmp/workspace',
@@ -844,7 +851,7 @@ describe('ClaudeRuntimeAdapter', () => {
844
851
  sandboxMode: 'workspace-write',
845
852
  });
846
853
 
847
- expect(sdkOptions[0]).toMatchObject({
854
+ expect(sdkOptions.at(-1)).toMatchObject({
848
855
  model: 'sonnet',
849
856
  betas: ['context-1m-2025-08-07'],
850
857
  });
@@ -177,6 +177,7 @@ interface SDKSessionInfo {
177
177
  type SessionMessage = SDKMessage;
178
178
  interface ModelInfo {
179
179
  value: string;
180
+ resolvedModel?: string;
180
181
  displayName: string;
181
182
  description: string;
182
183
  supportedEffortLevels?: string[];
@@ -711,7 +712,7 @@ function mapModelInfo(model: ModelInfo, index: number): AgentModel {
711
712
  return {
712
713
  id: model.value,
713
714
  model: model.value,
714
- displayName: model.displayName,
715
+ displayName: versionedClaudeModelDisplayName(model),
715
716
  description: model.description,
716
717
  isDefault: index === 0,
717
718
  hidden: false,
@@ -723,11 +724,52 @@ function mapModelInfo(model: ModelInfo, index: number): AgentModel {
723
724
  };
724
725
  }
725
726
 
727
+ function versionedClaudeModelDisplayName(model: ModelInfo) {
728
+ const resolved = model.resolvedModel?.match(
729
+ /^claude-(sonnet|opus|haiku|fable)-(\d+)(?:-(\d+))?(?:-|\[|$)/i,
730
+ );
731
+ if (!resolved) {
732
+ return model.displayName;
733
+ }
734
+
735
+ const family = `${resolved[1]![0]!.toUpperCase()}${resolved[1]!.slice(1).toLowerCase()}`;
736
+ const version = resolved[3] ? `${resolved[2]}.${resolved[3]}` : resolved[2]!;
737
+ if (model.value === 'default') {
738
+ return `Default · ${family} ${version}`;
739
+ }
740
+
741
+ const qualifier = model.displayName.match(/\s*(\([^)]+\))\s*$/)?.[1];
742
+ return `${family} · ${version}${qualifier ? ` ${qualifier}` : ''}`;
743
+ }
744
+
745
+ function versionedAlias(
746
+ alias: AgentModel,
747
+ discovered: AgentModel | undefined,
748
+ qualifier?: string,
749
+ ) {
750
+ if (!discovered) {
751
+ return alias;
752
+ }
753
+ return {
754
+ ...alias,
755
+ displayName: `${discovered.displayName.replace(/\s+\([^)]+\)\s*$/, '')}${qualifier ?? ''}`,
756
+ description: discovered.description,
757
+ supportedReasoningEfforts: discovered.supportedReasoningEfforts,
758
+ defaultReasoningEffort: discovered.defaultReasoningEffort,
759
+ };
760
+ }
761
+
726
762
  function withClaudeCodeModelAliases(models: AgentModel[]) {
727
763
  const output = [...models];
728
- const defaultSonnet = DEFAULT_CLAUDE_MODELS[0]!;
729
- const oneMillionSonnet = DEFAULT_CLAUDE_MODELS[1]!;
730
- const fable = DEFAULT_CLAUDE_MODELS[2]!;
764
+ const discoveredSonnet = output.find((model) => /^Sonnet · /.test(model.displayName));
765
+ const discoveredFable = output.find((model) => /^Fable · /.test(model.displayName));
766
+ const defaultSonnet = versionedAlias(DEFAULT_CLAUDE_MODELS[0]!, discoveredSonnet);
767
+ const oneMillionSonnet = versionedAlias(
768
+ DEFAULT_CLAUDE_MODELS[1]!,
769
+ discoveredSonnet,
770
+ ' (1M context)',
771
+ );
772
+ const fable = versionedAlias(DEFAULT_CLAUDE_MODELS[2]!, discoveredFable);
731
773
  const hasSonnetAlias = output.some((model) => model.model === 'sonnet');
732
774
  if (!hasSonnetAlias) {
733
775
  output.unshift(defaultSonnet);
@@ -1344,6 +1386,7 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
1344
1386
  >();
1345
1387
  private subscriptionAuthKind: 'subscription' | 'apiKey' | 'unknown' = 'unknown';
1346
1388
  private subscriptionUsageObservedAt: string | null = null;
1389
+ private modelOptionsCache: AgentModel[] | null = null;
1347
1390
  private readonly historicalTurnIdAliases = new Map<string, Map<string, string>>();
1348
1391
  private readonly clientApp: string;
1349
1392
  private sdkLoadError: string | null = null;
@@ -1509,15 +1552,35 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
1509
1552
 
1510
1553
  async listModels(): Promise<AgentModel[]> {
1511
1554
  const active = [...this.activeTurns.values()][0];
1512
- if (!active) {
1513
- return DEFAULT_CLAUDE_MODELS;
1555
+ if (!active && this.modelOptionsCache) {
1556
+ return this.modelOptionsCache;
1514
1557
  }
1515
1558
 
1559
+ let query = active?.query ?? null;
1560
+
1516
1561
  try {
1517
- const models = await active.query.supportedModels();
1518
- return withClaudeCodeModelAliases(models.map(mapModelInfo));
1562
+ query ??= this.queryFactory({
1563
+ prompt: '',
1564
+ options: queryOptionsForRuntime({
1565
+ home: this.options.home,
1566
+ command: this.options.command,
1567
+ clientApp: this.clientApp,
1568
+ approvalMode: 'guarded',
1569
+ includePartialMessages: false,
1570
+ tools: [],
1571
+ maxTurns: 1,
1572
+ }),
1573
+ });
1574
+ const models = await query.supportedModels();
1575
+ const mapped = withClaudeCodeModelAliases(models.map(mapModelInfo));
1576
+ this.modelOptionsCache = mapped;
1577
+ return mapped;
1519
1578
  } catch {
1520
- return DEFAULT_CLAUDE_MODELS;
1579
+ return this.modelOptionsCache ?? DEFAULT_CLAUDE_MODELS;
1580
+ } finally {
1581
+ if (!active && query) {
1582
+ query.close();
1583
+ }
1521
1584
  }
1522
1585
  }
1523
1586
 
@@ -1156,7 +1156,7 @@ export interface ThreadAnsweredRequestNoteDto {
1156
1156
 
1157
1157
  export interface ThreadActivityNoteDto {
1158
1158
  id: string;
1159
- kind: 'fastMode' | 'forkCreated' | 'forkSource';
1159
+ kind: 'fastMode' | 'goal' | 'forkCreated' | 'forkSource';
1160
1160
  createdAt: string;
1161
1161
  text?: string;
1162
1162
  anchorTurnId?: string | null;
@@ -1374,6 +1374,96 @@ export interface ThreadDetailDto {
1374
1374
  liveItems?: ThreadLiveItemsDto | null;
1375
1375
  }
1376
1376
 
1377
+ export function prependEarlierThreadTurns(
1378
+ existing: ThreadDetailDto['turns'],
1379
+ earlier: ThreadDetailDto['turns'],
1380
+ ) {
1381
+ const existingIds = new Set(existing.map((turn) => turn.id));
1382
+ return [...earlier.filter((turn) => !existingIds.has(turn.id)), ...existing];
1383
+ }
1384
+
1385
+ export function mergeLatestThreadTurns(
1386
+ existing: ThreadDetailDto['turns'],
1387
+ latest: ThreadDetailDto['turns'],
1388
+ activeTurnId: string | null = null,
1389
+ ) {
1390
+ const seenLatestIds = new Set<string>();
1391
+ const uniqueLatest = latest.reduceRight<ThreadDetailDto['turns']>(
1392
+ (turns, turn) => {
1393
+ if (!seenLatestIds.has(turn.id)) {
1394
+ seenLatestIds.add(turn.id);
1395
+ turns.unshift(turn);
1396
+ }
1397
+ return turns;
1398
+ },
1399
+ [],
1400
+ );
1401
+ const latestById = new Map(uniqueLatest.map((turn) => [turn.id, turn]));
1402
+ const latestReplacements = uniqueLatest.filter((turn) =>
1403
+ turn.items.some((item) => item.kind === 'userMessage'),
1404
+ );
1405
+ const replacedExistingIds = new Set<string>();
1406
+ const seenExistingIds = new Set<string>();
1407
+ const uniqueExisting = existing.reduceRight<ThreadDetailDto['turns']>(
1408
+ (turns, turn) => {
1409
+ if (!seenExistingIds.has(turn.id)) {
1410
+ seenExistingIds.add(turn.id);
1411
+ turns.unshift(turn);
1412
+ }
1413
+ return turns;
1414
+ },
1415
+ [],
1416
+ );
1417
+
1418
+ for (const turn of uniqueExisting) {
1419
+ if (latestById.has(turn.id)) {
1420
+ replacedExistingIds.add(turn.id);
1421
+ continue;
1422
+ }
1423
+ if (turn.status !== 'inProgress') {
1424
+ continue;
1425
+ }
1426
+ const existingPrompt = turn.items.find(
1427
+ (item) => item.kind === 'userMessage',
1428
+ )?.text.trim();
1429
+ const existingStartedAt = turn.startedAt
1430
+ ? Date.parse(turn.startedAt)
1431
+ : Number.NaN;
1432
+ if (!existingPrompt || !Number.isFinite(existingStartedAt)) {
1433
+ continue;
1434
+ }
1435
+ const materializedReplacement = latestReplacements.find((replacement) => {
1436
+ const replacementPrompt = replacement.items.find(
1437
+ (item) => item.kind === 'userMessage',
1438
+ )?.text.trim();
1439
+ const replacementStartedAt = replacement.startedAt
1440
+ ? Date.parse(replacement.startedAt)
1441
+ : Number.NaN;
1442
+ return (
1443
+ replacementPrompt === existingPrompt &&
1444
+ Number.isFinite(replacementStartedAt) &&
1445
+ Math.abs(replacementStartedAt - existingStartedAt) <= 15_000
1446
+ );
1447
+ });
1448
+ if (materializedReplacement) {
1449
+ replacedExistingIds.add(turn.id);
1450
+ }
1451
+ }
1452
+
1453
+ const olderTurns = uniqueExisting.filter(
1454
+ (turn) => !latestById.has(turn.id) && !replacedExistingIds.has(turn.id),
1455
+ );
1456
+ const ordered = [...olderTurns, ...uniqueLatest];
1457
+ if (!activeTurnId) {
1458
+ return ordered;
1459
+ }
1460
+
1461
+ const activeTurn = ordered.find((turn) => turn.id === activeTurnId);
1462
+ return activeTurn
1463
+ ? [...ordered.filter((turn) => turn.id !== activeTurnId), activeTurn]
1464
+ : ordered;
1465
+ }
1466
+
1377
1467
  export interface ThreadExportTurnOptionDto {
1378
1468
  turnId: string;
1379
1469
  turnNumber: number;