remote-codex 0.11.40 → 0.11.42

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-BvxWpPKW.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-BLThton-.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.40",
3
+ "version": "0.11.42",
4
4
  "description": "Local web supervisor for Codex workspaces and threads.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2983,4 +2983,139 @@ describe('ClaudeRuntimeAdapter', () => {
2983
2983
  ]),
2984
2984
  );
2985
2985
  });
2986
+
2987
+ it('keeps live Claude compaction in the active turn and hides its summary', async () => {
2988
+ const adapter = makeAdapter((prompt) => prompt === hiddenInitPrompt()
2989
+ ? [systemInit(), result()]
2990
+ : [
2991
+ systemInit(),
2992
+ {
2993
+ type: 'assistant',
2994
+ uuid: 'assistant-before-compact',
2995
+ session_id: 'claude-session-1',
2996
+ message: { role: 'assistant', content: [{ type: 'text', text: 'Before.' }] },
2997
+ parent_tool_use_id: null,
2998
+ },
2999
+ {
3000
+ type: 'system',
3001
+ subtype: 'status',
3002
+ status: 'compacting',
3003
+ uuid: 'compact-status',
3004
+ session_id: 'claude-session-1',
3005
+ },
3006
+ {
3007
+ type: 'user',
3008
+ uuid: 'compact-summary',
3009
+ session_id: 'claude-session-1',
3010
+ isCompactSummary: true,
3011
+ message: { role: 'user', content: 'HIDDEN LIVE SUMMARY' },
3012
+ parent_tool_use_id: null,
3013
+ },
3014
+ {
3015
+ type: 'system',
3016
+ subtype: 'compact_boundary',
3017
+ compact_metadata: { trigger: 'auto', pre_tokens: 180_000 },
3018
+ uuid: 'compact-boundary',
3019
+ session_id: 'claude-session-1',
3020
+ },
3021
+ {
3022
+ type: 'assistant',
3023
+ uuid: 'assistant-after-compact',
3024
+ session_id: 'claude-session-1',
3025
+ message: { role: 'assistant', content: [{ type: 'text', text: 'After.' }] },
3026
+ parent_tool_use_id: null,
3027
+ },
3028
+ result(),
3029
+ ]);
3030
+ const events: AgentRuntimeEvent[] = [];
3031
+ adapter.on('event', (event) => events.push(event));
3032
+
3033
+ await adapter.startTurn({
3034
+ providerSessionId: 'claude-session-1',
3035
+ displayTurnId: 'turn-compact',
3036
+ prompt: 'Continue working',
3037
+ } as any);
3038
+ await wait();
3039
+
3040
+ const completed = events.findLast((event) => event.type === 'turn.completed');
3041
+ const items = completed?.type === 'turn.completed' ? completed.turn.items : [];
3042
+ expect(completed).toMatchObject({
3043
+ type: 'turn.completed',
3044
+ turn: { providerTurnId: 'turn-compact', status: 'completed' },
3045
+ });
3046
+ expect(items.filter((item) => item.kind === 'contextCompaction')).toEqual([
3047
+ expect.objectContaining({
3048
+ text: 'Context compacted',
3049
+ detailText: null,
3050
+ status: 'completed',
3051
+ }),
3052
+ ]);
3053
+ expect(items).toEqual(expect.arrayContaining([
3054
+ expect.objectContaining({ kind: 'agentMessage', text: 'Before.' }),
3055
+ expect.objectContaining({ kind: 'agentMessage', text: 'After.' }),
3056
+ ]));
3057
+ expect(JSON.stringify(items)).not.toContain('HIDDEN LIVE SUMMARY');
3058
+ });
3059
+
3060
+ it('does not create a historical turn for Claude compact summaries', async () => {
3061
+ const adapter = new ClaudeRuntimeAdapter({
3062
+ home: '/tmp/claude-home',
3063
+ query: (() => new FakeQuery([])) as any,
3064
+ getSessionInfo: (async () => ({
3065
+ sessionId: 'claude-session-1',
3066
+ summary: 'Existing session',
3067
+ lastModified: 1_772_000_000_000,
3068
+ createdAt: 1_771_000_000_000,
3069
+ cwd: '/tmp/workspace',
3070
+ firstPrompt: 'Check training',
3071
+ })) as any,
3072
+ getSessionMessages: (async () => [
3073
+ {
3074
+ type: 'user',
3075
+ uuid: uuidV7At(1_771_000_000_000),
3076
+ session_id: 'claude-session-1',
3077
+ message: { role: 'user', content: 'Check training' },
3078
+ parent_tool_use_id: null,
3079
+ },
3080
+ {
3081
+ type: 'assistant',
3082
+ uuid: 'assistant-before-compact',
3083
+ session_id: 'claude-session-1',
3084
+ message: { role: 'assistant', content: [{ type: 'text', text: 'Before.' }] },
3085
+ parent_tool_use_id: null,
3086
+ },
3087
+ {
3088
+ type: 'user',
3089
+ uuid: 'compact-summary',
3090
+ session_id: 'claude-session-1',
3091
+ message: {
3092
+ role: 'user',
3093
+ content: 'This session is being continued from a previous conversation that ran out of context. HIDDEN HISTORICAL SUMMARY',
3094
+ },
3095
+ parent_tool_use_id: null,
3096
+ },
3097
+ {
3098
+ type: 'assistant',
3099
+ uuid: 'assistant-after-compact',
3100
+ session_id: 'claude-session-1',
3101
+ message: { role: 'assistant', content: [{ type: 'text', text: 'After.' }] },
3102
+ parent_tool_use_id: null,
3103
+ },
3104
+ ] satisfies SessionMessage[]) as any,
3105
+ });
3106
+
3107
+ const session = await adapter.readSession('claude-session-1');
3108
+ expect(session.turns).toHaveLength(1);
3109
+ expect(session.turns[0]?.items).toEqual(expect.arrayContaining([
3110
+ expect.objectContaining({ kind: 'userMessage', text: 'Check training' }),
3111
+ expect.objectContaining({ kind: 'agentMessage', text: 'Before.' }),
3112
+ expect.objectContaining({
3113
+ kind: 'contextCompaction',
3114
+ text: 'Context compacted',
3115
+ detailText: null,
3116
+ }),
3117
+ expect.objectContaining({ kind: 'agentMessage', text: 'After.' }),
3118
+ ]));
3119
+ expect(JSON.stringify(session.turns)).not.toContain('HIDDEN HISTORICAL SUMMARY');
3120
+ });
2986
3121
  });
@@ -46,6 +46,7 @@ import {
46
46
  isHiddenContinuationMessage,
47
47
  isHiddenInitMessage,
48
48
  limitErrorFromHistoryItems,
49
+ messageContentText,
49
50
  partialReasoningDelta,
50
51
  partialTextDelta,
51
52
  resultForToolUse,
@@ -133,6 +134,16 @@ interface SDKMessage {
133
134
  errors?: string[];
134
135
  stop_reason?: string;
135
136
  rate_limit_info?: SDKRateLimitInfo;
137
+ compact_metadata?: {
138
+ trigger?: 'manual' | 'auto';
139
+ pre_tokens?: number;
140
+ post_tokens?: number;
141
+ };
142
+ compact_result?: 'success' | 'failed';
143
+ compact_error?: string;
144
+ status?: 'compacting' | 'requesting' | null;
145
+ isCompactSummary?: boolean;
146
+ is_compact_summary?: boolean;
136
147
  }
137
148
  interface SDKRateLimitInfo {
138
149
  status: 'allowed' | 'allowed_warning' | 'rejected';
@@ -244,11 +255,63 @@ interface ActiveClaudeTurn {
244
255
  assistantUsage: ClaudeTokenUsageBreakdown | null;
245
256
  resultUsage: ClaudeTokenUsageBreakdown | null;
246
257
  modelContextWindow: number | null;
258
+ currentCompactionItemId: string | null;
247
259
  }
248
260
 
249
261
  const promptPhotoTokenPattern = /\[PHOTO\s+([^\]]+)\]/g;
262
+ const claudeCompactSummaryPrefix =
263
+ 'This session is being continued from a previous conversation that ran out of context.';
250
264
  const activeTranscriptMatchWindowMs = 120_000;
251
265
 
266
+ function isClaudeCompactBoundaryMessage(message: SDKMessage | SessionMessage) {
267
+ if (message.type !== 'system') {
268
+ return false;
269
+ }
270
+ if (message.subtype === 'compact_boundary' || message.compact_metadata) {
271
+ return true;
272
+ }
273
+ return isRecord(message.message)
274
+ && (message.message.subtype === 'compact_boundary' || isRecord(message.message.compact_metadata));
275
+ }
276
+
277
+ function isClaudeCompactSummaryMessage(message: SDKMessage | SessionMessage) {
278
+ if (message.type !== 'user' || message.parent_tool_use_id) {
279
+ return false;
280
+ }
281
+ const payload = isRecord(message.message) ? message.message : null;
282
+ if (
283
+ message.isCompactSummary
284
+ || message.is_compact_summary
285
+ || payload?.isCompactSummary === true
286
+ || payload?.is_compact_summary === true
287
+ ) {
288
+ return true;
289
+ }
290
+ return messageContentText(message.message).trim().startsWith(claudeCompactSummaryPrefix);
291
+ }
292
+
293
+ function claudeContextCompactionItem(
294
+ id: string,
295
+ status: 'running' | 'completed' | 'failed',
296
+ error: string | null = null,
297
+ ): AgentHistoryItem {
298
+ const completed = status === 'completed';
299
+ const failed = status === 'failed';
300
+ const text = failed
301
+ ? 'Context compaction failed'
302
+ : completed
303
+ ? 'Context compacted'
304
+ : 'Compacting context';
305
+ return {
306
+ id,
307
+ kind: 'contextCompaction',
308
+ text,
309
+ previewText: text,
310
+ detailText: failed ? error : null,
311
+ status,
312
+ };
313
+ }
314
+
252
315
  function normalizePromptForTurnReconciliation(value: string) {
253
316
  return value
254
317
  // Claude history can rewrite or omit an image marker. The surrounding
@@ -1690,6 +1753,7 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
1690
1753
  assistantUsage: null,
1691
1754
  resultUsage: null,
1692
1755
  modelContextWindow: null,
1756
+ currentCompactionItemId: null,
1693
1757
  };
1694
1758
  this.knownSessionIds.add(input.providerSessionId);
1695
1759
  let sessionPrompts = this.liveUserPrompts.get(input.providerSessionId);
@@ -1976,6 +2040,62 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
1976
2040
  return;
1977
2041
  }
1978
2042
 
2043
+ if (message.type === 'system' && message.subtype === 'status') {
2044
+ if (message.status === 'compacting') {
2045
+ const existing = state.currentCompactionItemId
2046
+ ? state.items.get(state.currentCompactionItemId)
2047
+ : null;
2048
+ const itemId = existing?.status === 'running'
2049
+ ? existing.id
2050
+ : `claude-compaction-${messageUuid(message, randomUUID())}`;
2051
+ state.currentCompactionItemId = itemId;
2052
+ const item = withHistoryItemCreatedAt(
2053
+ claudeContextCompactionItem(itemId, 'running'),
2054
+ messageCreatedAt,
2055
+ );
2056
+ addOrUpdateItem(state, item);
2057
+ this.emitItem(state, item, 'item.started', { force: Boolean(existing) });
2058
+ return;
2059
+ }
2060
+ if (message.compact_result) {
2061
+ const itemId = state.currentCompactionItemId
2062
+ ?? `claude-compaction-${messageUuid(message, randomUUID())}`;
2063
+ const status = message.compact_result === 'success' ? 'completed' : 'failed';
2064
+ const item = withHistoryItemCreatedAt(
2065
+ claudeContextCompactionItem(itemId, status, message.compact_error ?? null),
2066
+ messageCreatedAt,
2067
+ );
2068
+ addOrUpdateItem(state, item);
2069
+ this.emitItem(state, item, 'item.completed');
2070
+ state.currentCompactionItemId = status === 'failed' ? null : itemId;
2071
+ return;
2072
+ }
2073
+ return;
2074
+ }
2075
+
2076
+ if (isClaudeCompactBoundaryMessage(message)) {
2077
+ const itemId = state.currentCompactionItemId
2078
+ ?? `claude-compaction-${messageUuid(message, randomUUID())}`;
2079
+ const previous = state.items.get(itemId);
2080
+ const item = withHistoryItemCreatedAt(
2081
+ claudeContextCompactionItem(itemId, 'completed'),
2082
+ previous?.createdAt ?? messageCreatedAt,
2083
+ );
2084
+ addOrUpdateItem(state, item);
2085
+ if (!previous) {
2086
+ this.emitItem(state, item, 'item.started');
2087
+ }
2088
+ if (previous?.status !== 'completed') {
2089
+ this.emitItem(state, item, 'item.completed');
2090
+ }
2091
+ state.currentCompactionItemId = null;
2092
+ return;
2093
+ }
2094
+
2095
+ if (isClaudeCompactSummaryMessage(message)) {
2096
+ return;
2097
+ }
2098
+
1979
2099
  if (message.type === 'stream_event') {
1980
2100
  const nextStreamMessageId = streamMessageId(message.event);
1981
2101
  if (nextStreamMessageId) {
@@ -2408,6 +2528,8 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
2408
2528
  itemsById: Map<string, AgentHistoryItem>;
2409
2529
  } | null = null;
2410
2530
  let skippingHiddenInit = false;
2531
+ let pendingCompactSummaryItemId: string | null = null;
2532
+ let pendingCompactBoundaryItemId: string | null = null;
2411
2533
  const suppressedToolUseIds = new Set<string>();
2412
2534
 
2413
2535
  const upsertCurrentItem = (item: AgentHistoryItem) => {
@@ -2441,6 +2563,20 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
2441
2563
  };
2442
2564
 
2443
2565
  for (const message of messages) {
2566
+ if (isClaudeCompactBoundaryMessage(message)) {
2567
+ const itemId: string = pendingCompactSummaryItemId
2568
+ ?? `claude-compaction-${messageUuid(message, randomUUID())}`;
2569
+ pendingCompactSummaryItemId = null;
2570
+ pendingCompactBoundaryItemId = itemId;
2571
+ upsertCurrentItem(
2572
+ withHistoryItemCreatedAt(
2573
+ claudeContextCompactionItem(itemId, 'completed'),
2574
+ sessionMessageTimestamp(message) ?? current?.startedAt,
2575
+ ),
2576
+ );
2577
+ continue;
2578
+ }
2579
+
2444
2580
  if (message.type === 'user') {
2445
2581
  const taskNotification = taskNotificationToolResult(message.message);
2446
2582
  if (taskNotification) {
@@ -2487,6 +2623,21 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
2487
2623
  }
2488
2624
 
2489
2625
  if (message.type === 'user' && !message.parent_tool_use_id) {
2626
+ if (isClaudeCompactSummaryMessage(message)) {
2627
+ const itemId: string = pendingCompactBoundaryItemId
2628
+ ?? `claude-compaction-${messageUuid(message, randomUUID())}`;
2629
+ if (!pendingCompactBoundaryItemId) {
2630
+ pendingCompactSummaryItemId = itemId;
2631
+ }
2632
+ pendingCompactBoundaryItemId = null;
2633
+ upsertCurrentItem(
2634
+ withHistoryItemCreatedAt(
2635
+ claudeContextCompactionItem(itemId, 'completed'),
2636
+ sessionMessageTimestamp(message) ?? current?.startedAt,
2637
+ ),
2638
+ );
2639
+ continue;
2640
+ }
2490
2641
  if (isHiddenInitMessage(message.message)) {
2491
2642
  skippingHiddenInit = true;
2492
2643
  current = null;
@@ -2506,19 +2657,19 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
2506
2657
  items: current.items,
2507
2658
  }));
2508
2659
  }
2509
- const messageUuid = message.uuid ?? randomUUID();
2510
- const userStartedAt = isoFromUuidV7(messageUuid);
2660
+ const userMessageUuid = message.uuid ?? randomUUID();
2661
+ const userStartedAt = isoFromUuidV7(userMessageUuid);
2511
2662
  const userItem = await this.userMessageToHistoryItem(
2512
- messageUuid,
2663
+ userMessageUuid,
2513
2664
  message.message,
2514
2665
  context,
2515
2666
  );
2516
2667
  const stampedUserItem = withHistoryItemCreatedAt(userItem, userStartedAt);
2517
2668
  current = {
2518
- providerTurnId: `claude-turn-${messageUuid}`,
2669
+ providerTurnId: `claude-turn-${userMessageUuid}`,
2519
2670
  startedAt: userStartedAt,
2520
2671
  items: [stampedUserItem],
2521
- itemsById: new Map([[messageUuid, stampedUserItem]]),
2672
+ itemsById: new Map([[userMessageUuid, stampedUserItem]]),
2522
2673
  };
2523
2674
  continue;
2524
2675
  }
@@ -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;