remote-codex 0.11.39 → 0.11.41

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.41",
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({
@@ -2884,4 +2983,139 @@ describe('ClaudeRuntimeAdapter', () => {
2884
2983
  ]),
2885
2984
  );
2886
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
+ });
2887
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,9 +255,88 @@ 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.';
264
+ const activeTranscriptMatchWindowMs = 120_000;
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
+
315
+ function normalizePromptForTurnReconciliation(value: string) {
316
+ return value
317
+ // Claude history can rewrite or omit an image marker. The surrounding
318
+ // text and UUIDv7 timestamp remain stable reconciliation identifiers.
319
+ .replace(/\[PHOTO\s+[^\]]+\]/g, ' ')
320
+ .replace(/\s+/g, ' ')
321
+ .trim();
322
+ }
323
+
324
+ function userPromptForTurn(turn: AgentTurn) {
325
+ return turn.items.find((item) => item.kind === 'userMessage')?.text ?? null;
326
+ }
327
+
328
+ function isRecentActiveTranscriptTurn(turn: AgentTurn, activeStartedAt: string) {
329
+ if (!turn.startedAt) {
330
+ return true;
331
+ }
332
+ const transcriptStartedAt = Date.parse(turn.startedAt);
333
+ const activeStartedAtMs = Date.parse(activeStartedAt);
334
+ return (
335
+ !Number.isFinite(transcriptStartedAt)
336
+ || !Number.isFinite(activeStartedAtMs)
337
+ || Math.abs(transcriptStartedAt - activeStartedAtMs) <= activeTranscriptMatchWindowMs
338
+ );
339
+ }
250
340
 
251
341
  function mimeTypeForImagePath(filePath: string) {
252
342
  const extension = path.extname(filePath).toLowerCase();
@@ -1663,6 +1753,7 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
1663
1753
  assistantUsage: null,
1664
1754
  resultUsage: null,
1665
1755
  modelContextWindow: null,
1756
+ currentCompactionItemId: null,
1666
1757
  };
1667
1758
  this.knownSessionIds.add(input.providerSessionId);
1668
1759
  let sessionPrompts = this.liveUserPrompts.get(input.providerSessionId);
@@ -1766,12 +1857,17 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
1766
1857
  const activeItems = [...activeTurn.itemOrder]
1767
1858
  .map((itemId) => activeTurn.items.get(itemId))
1768
1859
  .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
- );
1860
+ const normalizedPrompt = normalizePromptForTurnReconciliation(prompt);
1861
+ const transcriptTurnIndex = turns.findLastIndex((turn) => {
1862
+ const transcriptPrompt = userPromptForTurn(turn);
1863
+ return (
1864
+ turn.status === 'completed'
1865
+ && transcriptPrompt !== null
1866
+ && normalizePromptForTurnReconciliation(transcriptPrompt) === normalizedPrompt
1867
+ && !turn.items.some((item) => item.kind === 'agentMessage')
1868
+ && isRecentActiveTranscriptTurn(turn, activeTurn.startedAt)
1869
+ );
1870
+ });
1775
1871
 
1776
1872
  if (transcriptTurnIndex < 0) {
1777
1873
  return turns;
@@ -1784,6 +1880,14 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
1784
1880
  aliases = new Map();
1785
1881
  this.historicalTurnIdAliases.set(providerSessionId, aliases);
1786
1882
  }
1883
+ for (const [historicalTurnId, displayTurnId] of aliases) {
1884
+ if (
1885
+ displayTurnId === activeTurn.providerTurnId
1886
+ && historicalTurnId !== transcriptTurn.providerTurnId
1887
+ ) {
1888
+ aliases.delete(historicalTurnId);
1889
+ }
1890
+ }
1787
1891
  aliases.set(transcriptTurn.providerTurnId, activeTurn.providerTurnId);
1788
1892
  }
1789
1893
 
@@ -1812,9 +1916,20 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
1812
1916
  if (!aliases || aliases.size === 0) {
1813
1917
  return turns;
1814
1918
  }
1919
+ const rawTurnIds = new Set(turns.map((turn) => turn.providerTurnId));
1920
+ const usedTurnIds = new Set<string>();
1815
1921
  return turns.map((turn) => {
1816
1922
  const providerTurnId = aliases.get(turn.providerTurnId);
1817
- return providerTurnId ? { ...turn, providerTurnId } : turn;
1923
+ if (
1924
+ !providerTurnId
1925
+ || usedTurnIds.has(providerTurnId)
1926
+ || (providerTurnId !== turn.providerTurnId && rawTurnIds.has(providerTurnId))
1927
+ ) {
1928
+ usedTurnIds.add(turn.providerTurnId);
1929
+ return turn;
1930
+ }
1931
+ usedTurnIds.add(providerTurnId);
1932
+ return { ...turn, providerTurnId };
1818
1933
  });
1819
1934
  }
1820
1935
 
@@ -1925,6 +2040,62 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
1925
2040
  return;
1926
2041
  }
1927
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
+
1928
2099
  if (message.type === 'stream_event') {
1929
2100
  const nextStreamMessageId = streamMessageId(message.event);
1930
2101
  if (nextStreamMessageId) {
@@ -2357,6 +2528,8 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
2357
2528
  itemsById: Map<string, AgentHistoryItem>;
2358
2529
  } | null = null;
2359
2530
  let skippingHiddenInit = false;
2531
+ let pendingCompactSummaryItemId: string | null = null;
2532
+ let pendingCompactBoundaryItemId: string | null = null;
2360
2533
  const suppressedToolUseIds = new Set<string>();
2361
2534
 
2362
2535
  const upsertCurrentItem = (item: AgentHistoryItem) => {
@@ -2390,6 +2563,20 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
2390
2563
  };
2391
2564
 
2392
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
+
2393
2580
  if (message.type === 'user') {
2394
2581
  const taskNotification = taskNotificationToolResult(message.message);
2395
2582
  if (taskNotification) {
@@ -2436,6 +2623,21 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
2436
2623
  }
2437
2624
 
2438
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
+ }
2439
2641
  if (isHiddenInitMessage(message.message)) {
2440
2642
  skippingHiddenInit = true;
2441
2643
  current = null;
@@ -2455,19 +2657,19 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
2455
2657
  items: current.items,
2456
2658
  }));
2457
2659
  }
2458
- const messageUuid = message.uuid ?? randomUUID();
2459
- const userStartedAt = isoFromUuidV7(messageUuid);
2660
+ const userMessageUuid = message.uuid ?? randomUUID();
2661
+ const userStartedAt = isoFromUuidV7(userMessageUuid);
2460
2662
  const userItem = await this.userMessageToHistoryItem(
2461
- messageUuid,
2663
+ userMessageUuid,
2462
2664
  message.message,
2463
2665
  context,
2464
2666
  );
2465
2667
  const stampedUserItem = withHistoryItemCreatedAt(userItem, userStartedAt);
2466
2668
  current = {
2467
- providerTurnId: `claude-turn-${messageUuid}`,
2669
+ providerTurnId: `claude-turn-${userMessageUuid}`,
2468
2670
  startedAt: userStartedAt,
2469
2671
  items: [stampedUserItem],
2470
- itemsById: new Map([[messageUuid, stampedUserItem]]),
2672
+ itemsById: new Map([[userMessageUuid, stampedUserItem]]),
2471
2673
  };
2472
2674
  continue;
2473
2675
  }