remote-codex 0.11.25 → 0.11.27

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.
@@ -4,7 +4,7 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
 
6
6
  import { ClaudeRuntimeAdapter } from './runtimeAdapter';
7
- import { hiddenInitPrompt } from './historyItems';
7
+ import { assistantMessageToHistoryItems, hiddenInitPrompt } from './historyItems';
8
8
  import type { AgentRuntimeEvent } from '../../agent-runtime/src/index';
9
9
 
10
10
  type SDKMessage = Record<string, any>;
@@ -665,6 +665,350 @@ describe('ClaudeRuntimeAdapter', () => {
665
665
  await expect(adapter.listLoadedSessions()).resolves.toEqual(['claude-session-1']);
666
666
  });
667
667
 
668
+ it('keeps active Claude sessions running in list summaries', async () => {
669
+ const heldQuery = new FakeQuery([systemInit()], { holdOpen: true });
670
+ const adapter = makeAdapter((prompt) => {
671
+ if (prompt === hiddenInitPrompt()) {
672
+ return [systemInit(), result()];
673
+ }
674
+ return heldQuery;
675
+ });
676
+
677
+ await adapter.startSession({
678
+ cwd: '/tmp/workspace',
679
+ model: 'sonnet',
680
+ approvalMode: 'guarded',
681
+ sandboxMode: 'workspace-write',
682
+ });
683
+ await adapter.startTurn({
684
+ providerSessionId: 'claude-session-1',
685
+ providerTurnId: 'turn-1',
686
+ prompt: 'Keep running',
687
+ } as any);
688
+ await wait();
689
+
690
+ await expect(adapter.listSessions()).resolves.toEqual([
691
+ expect.objectContaining({
692
+ providerSessionId: 'claude-session-1',
693
+ status: 'running',
694
+ }),
695
+ ]);
696
+
697
+ heldQuery.close();
698
+ });
699
+
700
+ it('merges adjacent Claude thinking blocks into one reasoning item', () => {
701
+ const items = assistantMessageToHistoryItems({
702
+ messageId: 'msg_1',
703
+ message: {
704
+ role: 'assistant',
705
+ content: [
706
+ { type: 'thinking', thinking: 'First thought.' },
707
+ { type: 'thinking', thinking: 'Second thought.' },
708
+ { type: 'text', text: 'Final answer.' },
709
+ ],
710
+ },
711
+ });
712
+
713
+ expect(items).toEqual([
714
+ expect.objectContaining({
715
+ kind: 'reasoning',
716
+ text: 'First thought.\n\nSecond thought.',
717
+ }),
718
+ expect.objectContaining({
719
+ kind: 'agentMessage',
720
+ text: 'Final answer.',
721
+ }),
722
+ ]);
723
+ });
724
+
725
+ it('merges adjacent Claude reasoning messages while reading history', async () => {
726
+ const adapter = new ClaudeRuntimeAdapter({
727
+ home: '/tmp/claude-home',
728
+ query: (() => new FakeQuery([])) as any,
729
+ getSessionInfo: (async () => ({
730
+ sessionId: 'claude-session-1',
731
+ summary: 'Existing session',
732
+ lastModified: 1_772_000_000_000,
733
+ createdAt: 1_771_000_000_000,
734
+ cwd: '/tmp/workspace',
735
+ })) as any,
736
+ listSessions: (async () => []) as any,
737
+ getSessionMessages: (async () => [
738
+ {
739
+ type: 'user',
740
+ uuid: 'reasoning-user',
741
+ session_id: 'claude-session-1',
742
+ message: { role: 'user', content: 'Inspect the dev branch.' },
743
+ parent_tool_use_id: null,
744
+ },
745
+ {
746
+ type: 'assistant',
747
+ uuid: 'reasoning-1',
748
+ session_id: 'claude-session-1',
749
+ message: {
750
+ role: 'assistant',
751
+ content: [{ type: 'thinking', thinking: 'I should inspect dev.' }],
752
+ },
753
+ parent_tool_use_id: null,
754
+ },
755
+ {
756
+ type: 'assistant',
757
+ uuid: 'reasoning-2',
758
+ session_id: 'claude-session-1',
759
+ message: {
760
+ role: 'assistant',
761
+ content: [{ type: 'thinking', thinking: 'I should inspect dev.' }],
762
+ },
763
+ parent_tool_use_id: null,
764
+ },
765
+ {
766
+ type: 'assistant',
767
+ uuid: 'reasoning-3',
768
+ session_id: 'claude-session-1',
769
+ message: {
770
+ role: 'assistant',
771
+ content: [
772
+ {
773
+ type: 'thinking',
774
+ thinking: 'I should inspect dev. Then compare the merge.',
775
+ },
776
+ ],
777
+ },
778
+ parent_tool_use_id: null,
779
+ },
780
+ ] satisfies SessionMessage[]) as any,
781
+ });
782
+
783
+ const session = await adapter.readSession('claude-session-1');
784
+ expect(session.turns[0]?.items).toEqual([
785
+ expect.objectContaining({ kind: 'userMessage' }),
786
+ expect.objectContaining({
787
+ kind: 'reasoning',
788
+ text: 'I should inspect dev. Then compare the merge.',
789
+ }),
790
+ ]);
791
+ });
792
+
793
+ it('keeps late Claude subagent tool results in order before completing the turn', async () => {
794
+ const adapter = makeAdapter((prompt) => {
795
+ if (prompt === hiddenInitPrompt()) {
796
+ return [systemInit(), result()];
797
+ }
798
+ return [
799
+ systemInit(),
800
+ {
801
+ type: 'assistant',
802
+ message: {
803
+ id: 'msg_agent',
804
+ type: 'message',
805
+ role: 'assistant',
806
+ model: 'sonnet',
807
+ content: [
808
+ {
809
+ type: 'tool_use',
810
+ id: 'toolu_agent',
811
+ name: 'Task',
812
+ input: {
813
+ description: 'Inspect dependency graph',
814
+ prompt: 'Look for stale imports.',
815
+ },
816
+ },
817
+ ],
818
+ stop_reason: null,
819
+ stop_sequence: null,
820
+ stop_details: null,
821
+ usage: {} as any,
822
+ container: null,
823
+ context_management: null,
824
+ diagnostics: null,
825
+ },
826
+ parent_tool_use_id: null,
827
+ uuid: '00000000-0000-4000-8000-000000000031' as any,
828
+ session_id: 'claude-session-1',
829
+ },
830
+ result(),
831
+ {
832
+ type: 'user',
833
+ message: {
834
+ role: 'user',
835
+ content: [
836
+ {
837
+ type: 'tool_result',
838
+ tool_use_id: 'toolu_agent',
839
+ content: 'The subagent found one stale import.',
840
+ },
841
+ ],
842
+ },
843
+ parent_tool_use_id: null,
844
+ tool_use_result: 'The subagent found one stale import.',
845
+ uuid: '00000000-0000-4000-8000-000000000032' as any,
846
+ session_id: 'claude-session-1',
847
+ },
848
+ ];
849
+ });
850
+ const events: AgentRuntimeEvent[] = [];
851
+ adapter.on('event', (event) => events.push(event));
852
+
853
+ await adapter.startTurn({
854
+ providerSessionId: 'claude-session-1',
855
+ displayTurnId: 'turn-subagent',
856
+ prompt: 'Use a subagent',
857
+ } as any);
858
+ await wait();
859
+
860
+ const completed = events.at(-1);
861
+ expect(completed).toMatchObject({
862
+ type: 'turn.completed',
863
+ turn: {
864
+ providerTurnId: 'turn-subagent',
865
+ status: 'completed',
866
+ },
867
+ });
868
+ expect(completed?.type === 'turn.completed' ? completed.turn.items : []).toEqual([
869
+ expect.objectContaining({ id: 'turn-subagent:user', kind: 'userMessage' }),
870
+ expect.objectContaining({
871
+ id: 'toolu_agent',
872
+ kind: 'agentToolCall',
873
+ status: 'completed',
874
+ detailText: expect.stringContaining('The subagent found one stale import.'),
875
+ }),
876
+ ]);
877
+ expect(
878
+ events.findIndex(
879
+ (event) =>
880
+ event.type === 'item.completed' &&
881
+ event.item.id === 'toolu_agent' &&
882
+ event.item.status === 'completed',
883
+ ),
884
+ ).toBeLessThan(events.length - 1);
885
+ });
886
+
887
+ it('maps Claude task-notification text to the active subagent tool result', async () => {
888
+ const adapter = makeAdapter((prompt) => {
889
+ if (prompt === hiddenInitPrompt()) {
890
+ return [systemInit(), result()];
891
+ }
892
+ return [
893
+ systemInit(),
894
+ {
895
+ type: 'assistant',
896
+ message: {
897
+ id: 'msg_agent',
898
+ type: 'message',
899
+ role: 'assistant',
900
+ model: 'sonnet',
901
+ content: [
902
+ {
903
+ type: 'tool_use',
904
+ id: 'toolu_agent_xml',
905
+ name: 'Task',
906
+ input: {
907
+ description: 'Audit docs',
908
+ prompt: 'Audit docs.',
909
+ },
910
+ },
911
+ ],
912
+ usage: {} as any,
913
+ },
914
+ parent_tool_use_id: null,
915
+ uuid: '00000000-0000-4000-8000-000000000041' as any,
916
+ session_id: 'claude-session-1',
917
+ },
918
+ {
919
+ type: 'user',
920
+ message: {
921
+ role: 'user',
922
+ content:
923
+ '<task-notification>\\n<task-id>task-1</task-id>\\n<tool-use-id>toolu_agent_xml</tool-use-id>\\n<status>completed</status>\\n<summary>Agent \"Audit docs\" finished</summary>\\n<result>Found one stale runbook.</result>\\n</task-notification>',
924
+ },
925
+ parent_tool_use_id: null,
926
+ uuid: '00000000-0000-4000-8000-000000000042' as any,
927
+ session_id: 'claude-session-1',
928
+ },
929
+ result(),
930
+ ];
931
+ });
932
+ const events: AgentRuntimeEvent[] = [];
933
+ adapter.on('event', (event) => events.push(event));
934
+
935
+ await adapter.startTurn({
936
+ providerSessionId: 'claude-session-1',
937
+ displayTurnId: 'turn-subagent-xml',
938
+ prompt: 'Use a subagent',
939
+ } as any);
940
+ await wait();
941
+
942
+ const completed = events.at(-1);
943
+ expect(completed).toMatchObject({
944
+ type: 'turn.completed',
945
+ turn: {
946
+ providerTurnId: 'turn-subagent-xml',
947
+ status: 'completed',
948
+ },
949
+ });
950
+ expect(completed?.type === 'turn.completed' ? completed.turn.items : []).toEqual([
951
+ expect.objectContaining({ id: 'turn-subagent-xml:user', kind: 'userMessage' }),
952
+ expect.objectContaining({
953
+ id: 'toolu_agent_xml',
954
+ kind: 'agentToolCall',
955
+ status: 'completed',
956
+ detailText: expect.stringContaining('Found one stale runbook.'),
957
+ }),
958
+ ]);
959
+ });
960
+
961
+ it('marks Claude session limit assistant messages as failed even when the SDK result succeeds', async () => {
962
+ const adapter = makeAdapter((prompt) => {
963
+ if (prompt === hiddenInitPrompt()) {
964
+ return [systemInit(), result()];
965
+ }
966
+ return [
967
+ systemInit(),
968
+ {
969
+ type: 'assistant',
970
+ message: {
971
+ id: 'msg_limit',
972
+ type: 'message',
973
+ role: 'assistant',
974
+ model: 'sonnet',
975
+ content: [
976
+ {
977
+ type: 'text',
978
+ text: "You've hit your session limit · resets 10pm (America/Toronto)",
979
+ },
980
+ ],
981
+ usage: {} as any,
982
+ },
983
+ parent_tool_use_id: null,
984
+ uuid: '00000000-0000-4000-8000-000000000043' as any,
985
+ session_id: 'claude-session-1',
986
+ },
987
+ result(),
988
+ ];
989
+ });
990
+ const events: AgentRuntimeEvent[] = [];
991
+ adapter.on('event', (event) => events.push(event));
992
+
993
+ await adapter.startTurn({
994
+ providerSessionId: 'claude-session-1',
995
+ displayTurnId: 'turn-limit',
996
+ prompt: 'Continue',
997
+ } as any);
998
+ await wait();
999
+
1000
+ expect(events.at(-1)).toMatchObject({
1001
+ type: 'turn.completed',
1002
+ turn: {
1003
+ providerTurnId: 'turn-limit',
1004
+ status: 'failed',
1005
+ error: {
1006
+ message: expect.stringContaining("You've hit your session limit"),
1007
+ },
1008
+ },
1009
+ });
1010
+ });
1011
+
668
1012
  it('filters Claude generated titles derived from the synthetic init prompt', async () => {
669
1013
  const adapter = new ClaudeRuntimeAdapter({
670
1014
  home: '/tmp/claude-home',
@@ -1639,6 +1983,140 @@ describe('ClaudeRuntimeAdapter', () => {
1639
1983
  ]);
1640
1984
  });
1641
1985
 
1986
+ it('keeps historical task notifications inside the originating subagent tool call', async () => {
1987
+ const adapter = new ClaudeRuntimeAdapter({
1988
+ home: '/tmp/claude-home',
1989
+ query: (() => new FakeQuery([])) as any,
1990
+ getSessionInfo: (async () => ({
1991
+ sessionId: 'claude-session-1',
1992
+ summary: 'Existing session',
1993
+ lastModified: 1_772_000_000_000,
1994
+ createdAt: 1_771_000_000_000,
1995
+ cwd: '/tmp/workspace',
1996
+ })) as any,
1997
+ listSessions: (async () => []) as any,
1998
+ getSessionMessages: (async () => [
1999
+ {
2000
+ type: 'user',
2001
+ uuid: '019e4657-bd3c-72d1-b59d-324ed8a4b1ec',
2002
+ session_id: 'claude-session-1',
2003
+ message: { role: 'user', content: 'Audit both docs.' },
2004
+ parent_tool_use_id: null,
2005
+ },
2006
+ {
2007
+ type: 'assistant',
2008
+ uuid: 'assistant-task',
2009
+ session_id: 'claude-session-1',
2010
+ message: {
2011
+ role: 'assistant',
2012
+ content: [
2013
+ {
2014
+ type: 'tool_use',
2015
+ id: 'toolu_task_a',
2016
+ name: 'Task',
2017
+ input: {
2018
+ description: 'Audit infra docs',
2019
+ prompt: 'Audit infra docs.',
2020
+ },
2021
+ },
2022
+ ],
2023
+ },
2024
+ parent_tool_use_id: null,
2025
+ },
2026
+ {
2027
+ type: 'user',
2028
+ uuid: 'task-notification-a',
2029
+ session_id: 'claude-session-1',
2030
+ message: {
2031
+ role: 'user',
2032
+ content:
2033
+ '<task-notification>\\n<task-id>task-a</task-id>\\n<tool-use-id>toolu_task_a</tool-use-id>\\n<status>completed</status>\\n<summary>Agent \"Audit infra docs\" finished</summary>\\n<result>Infra docs need one migration checklist.</result>\\n</task-notification>',
2034
+ },
2035
+ parent_tool_use_id: null,
2036
+ },
2037
+ {
2038
+ type: 'assistant',
2039
+ uuid: 'assistant-final',
2040
+ session_id: 'claude-session-1',
2041
+ message: {
2042
+ role: 'assistant',
2043
+ content: [{ type: 'text', text: 'Infra audit complete.' }],
2044
+ },
2045
+ parent_tool_use_id: null,
2046
+ },
2047
+ ] satisfies SessionMessage[]) as any,
2048
+ });
2049
+
2050
+ const session = await adapter.readSession('claude-session-1');
2051
+ expect(session.turns).toHaveLength(1);
2052
+ expect(session.turns[0]?.items).toEqual([
2053
+ expect.objectContaining({ kind: 'userMessage', text: 'Audit both docs.' }),
2054
+ expect.objectContaining({
2055
+ id: 'toolu_task_a',
2056
+ kind: 'agentToolCall',
2057
+ status: 'completed',
2058
+ detailText: expect.stringContaining('Infra docs need one migration checklist.'),
2059
+ }),
2060
+ expect.objectContaining({ kind: 'agentMessage', text: 'Infra audit complete.' }),
2061
+ ]);
2062
+ expect(session.turns[0]?.items).not.toEqual(
2063
+ expect.arrayContaining([
2064
+ expect.objectContaining({
2065
+ kind: 'userMessage',
2066
+ text: expect.stringContaining('<task-notification>'),
2067
+ }),
2068
+ ]),
2069
+ );
2070
+ });
2071
+
2072
+ it('restores historical Claude session limit turns as failed', async () => {
2073
+ const adapter = new ClaudeRuntimeAdapter({
2074
+ home: '/tmp/claude-home',
2075
+ query: (() => new FakeQuery([])) as any,
2076
+ getSessionInfo: (async () => ({
2077
+ sessionId: 'claude-session-1',
2078
+ summary: 'Existing session',
2079
+ lastModified: 1_772_000_000_000,
2080
+ createdAt: 1_771_000_000_000,
2081
+ cwd: '/tmp/workspace',
2082
+ })) as any,
2083
+ listSessions: (async () => []) as any,
2084
+ getSessionMessages: (async () => [
2085
+ {
2086
+ type: 'user',
2087
+ uuid: '019e4657-bd3c-72d1-b59d-324ed8a4b1ec',
2088
+ session_id: 'claude-session-1',
2089
+ message: { role: 'user', content: 'Continue anyway.' },
2090
+ parent_tool_use_id: null,
2091
+ },
2092
+ {
2093
+ type: 'assistant',
2094
+ uuid: 'assistant-limit',
2095
+ session_id: 'claude-session-1',
2096
+ message: {
2097
+ role: 'assistant',
2098
+ content: [
2099
+ {
2100
+ type: 'text',
2101
+ text: "You've hit your session limit · resets 10pm (America/Toronto)",
2102
+ },
2103
+ ],
2104
+ },
2105
+ parent_tool_use_id: null,
2106
+ },
2107
+ ] satisfies SessionMessage[]) as any,
2108
+ });
2109
+
2110
+ const session = await adapter.readSession('claude-session-1');
2111
+ expect(session.turns).toHaveLength(1);
2112
+ expect(session.turns[0]).toMatchObject({
2113
+ status: 'failed',
2114
+ error: {
2115
+ message: expect.stringContaining("You've hit your session limit"),
2116
+ },
2117
+ });
2118
+ });
2119
+
1642
2120
  it('omits Claude AskUserQuestion tool results from historical turns', async () => {
1643
2121
  const adapter = new ClaudeRuntimeAdapter({
1644
2122
  home: '/tmp/claude-home',