remote-codex 0.11.39 → 0.11.40

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,7 +10,7 @@
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-CUhMNGXI.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-B-qj7e8G.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">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-codex",
3
- "version": "0.11.39",
3
+ "version": "0.11.40",
4
4
  "description": "Local web supervisor for Codex workspaces and threads.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -22,6 +22,11 @@ function wait(ms = 0) {
22
22
  return new Promise((resolve) => setTimeout(resolve, ms));
23
23
  }
24
24
 
25
+ function uuidV7At(timestamp = Date.now()) {
26
+ const timestampHex = Math.trunc(timestamp).toString(16).padStart(12, '0');
27
+ return `${timestampHex.slice(0, 8)}-${timestampHex.slice(8)}-7000-8000-000000000001`;
28
+ }
29
+
25
30
  class FakeQuery implements Query {
26
31
  interrupted = false;
27
32
  closed = false;
@@ -541,6 +546,7 @@ describe('ClaudeRuntimeAdapter', () => {
541
546
 
542
547
  it('reconciles active multimodal transcript turns back to the live runtime turn id', async () => {
543
548
  let queryMessages: SDKMessage[] = [systemInit()];
549
+ const currentMessageUuid = uuidV7At();
544
550
  const adapter = new ClaudeRuntimeAdapter({
545
551
  home: '/tmp/claude-home',
546
552
  command: 'claude',
@@ -556,7 +562,7 @@ describe('ClaudeRuntimeAdapter', () => {
556
562
  getSessionMessages: (async () => [
557
563
  {
558
564
  type: 'user',
559
- uuid: '019e4657-bd3c-72d1-b59d-324ed8a4b1ec',
565
+ uuid: currentMessageUuid,
560
566
  session_id: 'claude-session-1',
561
567
  message: {
562
568
  role: 'user',
@@ -609,6 +615,99 @@ describe('ClaudeRuntimeAdapter', () => {
609
615
  expect(reloadedSession.turns[0]?.providerTurnId).toBe(started.providerTurnId);
610
616
  });
611
617
 
618
+ it('does not reconcile an old matching user-only turn as the active turn', async () => {
619
+ const historicalMessageUuid = uuidV7At(Date.now() - 10 * 60_000);
620
+ const adapter = new ClaudeRuntimeAdapter({
621
+ home: '/tmp/claude-home',
622
+ command: 'claude',
623
+ query: (() => new FakeQuery([systemInit()], { holdOpen: true })) as any,
624
+ listSessions: (async () => [] satisfies SDKSessionInfo[]) as any,
625
+ getSessionInfo: (async () => ({
626
+ sessionId: 'claude-session-1',
627
+ summary: 'Existing session',
628
+ cwd: '/tmp/workspace',
629
+ })) as any,
630
+ getSessionMessages: (async () => [{
631
+ type: 'user',
632
+ uuid: historicalMessageUuid,
633
+ session_id: 'claude-session-1',
634
+ message: { role: 'user', content: 'Repeat this prompt' },
635
+ parent_tool_use_id: null,
636
+ }] satisfies SessionMessage[]) as any,
637
+ });
638
+
639
+ const started = await adapter.startTurn({
640
+ providerSessionId: 'claude-session-1',
641
+ prompt: 'Repeat this prompt',
642
+ model: 'sonnet',
643
+ workspacePath: '/tmp/workspace',
644
+ });
645
+ const session = await adapter.readSession('claude-session-1');
646
+
647
+ expect(session.turns).toHaveLength(1);
648
+ expect(session.turns[0]).toMatchObject({
649
+ providerTurnId: `claude-turn-${historicalMessageUuid}`,
650
+ status: 'completed',
651
+ });
652
+
653
+ await adapter.interruptTurn({
654
+ providerSessionId: 'claude-session-1',
655
+ providerTurnId: started.providerTurnId,
656
+ });
657
+ });
658
+
659
+ it('reconciles only the current prompt when an older user-only turn exists', async () => {
660
+ const historicalMessageUuid = uuidV7At(Date.now() - 10 * 60_000);
661
+ const currentMessageUuid = uuidV7At();
662
+ const adapter = new ClaudeRuntimeAdapter({
663
+ home: '/tmp/claude-home',
664
+ command: 'claude',
665
+ query: (() => new FakeQuery([systemInit()], { holdOpen: true })) as any,
666
+ listSessions: (async () => [] satisfies SDKSessionInfo[]) as any,
667
+ getSessionInfo: (async () => ({
668
+ sessionId: 'claude-session-1',
669
+ summary: 'Existing session',
670
+ cwd: '/tmp/workspace',
671
+ })) as any,
672
+ getSessionMessages: (async () => [
673
+ {
674
+ type: 'user',
675
+ uuid: historicalMessageUuid,
676
+ session_id: 'claude-session-1',
677
+ message: { role: 'user', content: 'An older prompt' },
678
+ parent_tool_use_id: null,
679
+ },
680
+ {
681
+ type: 'user',
682
+ uuid: currentMessageUuid,
683
+ session_id: 'claude-session-1',
684
+ message: { role: 'user', content: 'The current prompt' },
685
+ parent_tool_use_id: null,
686
+ },
687
+ ] satisfies SessionMessage[]) as any,
688
+ });
689
+
690
+ const started = await adapter.startTurn({
691
+ providerSessionId: 'claude-session-1',
692
+ prompt: 'The current prompt',
693
+ model: 'sonnet',
694
+ workspacePath: '/tmp/workspace',
695
+ });
696
+ const session = await adapter.readSession('claude-session-1');
697
+
698
+ expect(session.turns.map((turn) => turn.providerTurnId)).toEqual([
699
+ `claude-turn-${historicalMessageUuid}`,
700
+ started.providerTurnId,
701
+ ]);
702
+ expect(session.turns[0]?.items[0]).toMatchObject({ text: 'An older prompt' });
703
+ expect(session.turns[1]).toMatchObject({ status: 'inProgress' });
704
+
705
+ await adapter.interruptTurn({
706
+ providerSessionId: 'claude-session-1',
707
+ providerTurnId: started.providerTurnId,
708
+ });
709
+ });
710
+
612
711
  it('keeps image blocks visible when reading Claude session history', async () => {
613
712
  const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-history-image-'));
614
713
  const adapter = new ClaudeRuntimeAdapter({
@@ -247,6 +247,33 @@ interface ActiveClaudeTurn {
247
247
  }
248
248
 
249
249
  const promptPhotoTokenPattern = /\[PHOTO\s+([^\]]+)\]/g;
250
+ const activeTranscriptMatchWindowMs = 120_000;
251
+
252
+ function normalizePromptForTurnReconciliation(value: string) {
253
+ return value
254
+ // Claude history can rewrite or omit an image marker. The surrounding
255
+ // text and UUIDv7 timestamp remain stable reconciliation identifiers.
256
+ .replace(/\[PHOTO\s+[^\]]+\]/g, ' ')
257
+ .replace(/\s+/g, ' ')
258
+ .trim();
259
+ }
260
+
261
+ function userPromptForTurn(turn: AgentTurn) {
262
+ return turn.items.find((item) => item.kind === 'userMessage')?.text ?? null;
263
+ }
264
+
265
+ function isRecentActiveTranscriptTurn(turn: AgentTurn, activeStartedAt: string) {
266
+ if (!turn.startedAt) {
267
+ return true;
268
+ }
269
+ const transcriptStartedAt = Date.parse(turn.startedAt);
270
+ const activeStartedAtMs = Date.parse(activeStartedAt);
271
+ return (
272
+ !Number.isFinite(transcriptStartedAt)
273
+ || !Number.isFinite(activeStartedAtMs)
274
+ || Math.abs(transcriptStartedAt - activeStartedAtMs) <= activeTranscriptMatchWindowMs
275
+ );
276
+ }
250
277
 
251
278
  function mimeTypeForImagePath(filePath: string) {
252
279
  const extension = path.extname(filePath).toLowerCase();
@@ -1766,12 +1793,17 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
1766
1793
  const activeItems = [...activeTurn.itemOrder]
1767
1794
  .map((itemId) => activeTurn.items.get(itemId))
1768
1795
  .filter((item): item is AgentHistoryItem => Boolean(item));
1769
- const transcriptTurnIndex = turns.findLastIndex(
1770
- (turn) =>
1771
- turn.status === 'completed' &&
1772
- turn.items.some((item) => item.kind === 'userMessage') &&
1773
- !turn.items.some((item) => item.kind === 'agentMessage'),
1774
- );
1796
+ const normalizedPrompt = normalizePromptForTurnReconciliation(prompt);
1797
+ const transcriptTurnIndex = turns.findLastIndex((turn) => {
1798
+ const transcriptPrompt = userPromptForTurn(turn);
1799
+ return (
1800
+ turn.status === 'completed'
1801
+ && transcriptPrompt !== null
1802
+ && normalizePromptForTurnReconciliation(transcriptPrompt) === normalizedPrompt
1803
+ && !turn.items.some((item) => item.kind === 'agentMessage')
1804
+ && isRecentActiveTranscriptTurn(turn, activeTurn.startedAt)
1805
+ );
1806
+ });
1775
1807
 
1776
1808
  if (transcriptTurnIndex < 0) {
1777
1809
  return turns;
@@ -1784,6 +1816,14 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
1784
1816
  aliases = new Map();
1785
1817
  this.historicalTurnIdAliases.set(providerSessionId, aliases);
1786
1818
  }
1819
+ for (const [historicalTurnId, displayTurnId] of aliases) {
1820
+ if (
1821
+ displayTurnId === activeTurn.providerTurnId
1822
+ && historicalTurnId !== transcriptTurn.providerTurnId
1823
+ ) {
1824
+ aliases.delete(historicalTurnId);
1825
+ }
1826
+ }
1787
1827
  aliases.set(transcriptTurn.providerTurnId, activeTurn.providerTurnId);
1788
1828
  }
1789
1829
 
@@ -1812,9 +1852,20 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
1812
1852
  if (!aliases || aliases.size === 0) {
1813
1853
  return turns;
1814
1854
  }
1855
+ const rawTurnIds = new Set(turns.map((turn) => turn.providerTurnId));
1856
+ const usedTurnIds = new Set<string>();
1815
1857
  return turns.map((turn) => {
1816
1858
  const providerTurnId = aliases.get(turn.providerTurnId);
1817
- return providerTurnId ? { ...turn, providerTurnId } : turn;
1859
+ if (
1860
+ !providerTurnId
1861
+ || usedTurnIds.has(providerTurnId)
1862
+ || (providerTurnId !== turn.providerTurnId && rawTurnIds.has(providerTurnId))
1863
+ ) {
1864
+ usedTurnIds.add(turn.providerTurnId);
1865
+ return turn;
1866
+ }
1867
+ usedTurnIds.add(providerTurnId);
1868
+ return { ...turn, providerTurnId };
1818
1869
  });
1819
1870
  }
1820
1871