remote-codex 0.11.38 → 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.
- package/apps/supervisor-api/dist/index.js +47 -8
- package/apps/supervisor-web/dist/assets/{index-pkf-_mec.js → index-B-qj7e8G.js} +4 -4
- package/apps/supervisor-web/dist/assets/index-CDFuOeJN.css +1 -0
- package/apps/supervisor-web/dist/index.html +2 -2
- package/package.json +1 -1
- package/packages/claude/src/runtimeAdapter.test.ts +190 -2
- package/packages/claude/src/runtimeAdapter.ts +72 -9
- package/apps/supervisor-web/dist/assets/index-P0J5du_C.css +0 -1
|
@@ -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:
|
|
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({
|
|
@@ -824,6 +923,91 @@ describe('ClaudeRuntimeAdapter', () => {
|
|
|
824
923
|
heldQuery.close();
|
|
825
924
|
});
|
|
826
925
|
|
|
926
|
+
it('keeps an active Claude session running when its detail is re-read', async () => {
|
|
927
|
+
const heldQuery = new FakeQuery([systemInit()], { holdOpen: true });
|
|
928
|
+
const adapter = makeAdapter((prompt) => {
|
|
929
|
+
if (prompt === hiddenInitPrompt()) {
|
|
930
|
+
return [systemInit(), result()];
|
|
931
|
+
}
|
|
932
|
+
return heldQuery;
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
await adapter.startSession({
|
|
936
|
+
cwd: '/tmp/workspace',
|
|
937
|
+
model: 'sonnet',
|
|
938
|
+
approvalMode: 'guarded',
|
|
939
|
+
sandboxMode: 'workspace-write',
|
|
940
|
+
});
|
|
941
|
+
await adapter.startTurn({
|
|
942
|
+
providerSessionId: 'claude-session-1',
|
|
943
|
+
providerTurnId: 'turn-1',
|
|
944
|
+
prompt: 'Keep running',
|
|
945
|
+
} as any);
|
|
946
|
+
await wait();
|
|
947
|
+
|
|
948
|
+
await expect(adapter.readSession('claude-session-1')).resolves.toMatchObject({
|
|
949
|
+
providerSessionId: 'claude-session-1',
|
|
950
|
+
status: 'running',
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
heldQuery.close();
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
it('emits complete assistant progress messages before the Claude turn finishes', async () => {
|
|
957
|
+
const heldQuery = new FakeQuery([
|
|
958
|
+
systemInit(),
|
|
959
|
+
{
|
|
960
|
+
type: 'assistant',
|
|
961
|
+
message: {
|
|
962
|
+
id: 'msg_progress',
|
|
963
|
+
type: 'message',
|
|
964
|
+
role: 'assistant',
|
|
965
|
+
model: 'sonnet',
|
|
966
|
+
content: [{ type: 'text', text: 'I found the failing integration.' }],
|
|
967
|
+
usage: {} as any,
|
|
968
|
+
},
|
|
969
|
+
parent_tool_use_id: null,
|
|
970
|
+
uuid: '00000000-0000-4000-8000-000000000050' as any,
|
|
971
|
+
session_id: 'claude-session-1',
|
|
972
|
+
},
|
|
973
|
+
], { holdOpen: true });
|
|
974
|
+
const adapter = makeAdapter((prompt) => {
|
|
975
|
+
if (prompt === hiddenInitPrompt()) {
|
|
976
|
+
return [systemInit(), result()];
|
|
977
|
+
}
|
|
978
|
+
return heldQuery;
|
|
979
|
+
});
|
|
980
|
+
const events: AgentRuntimeEvent[] = [];
|
|
981
|
+
adapter.on('event', (event) => events.push(event));
|
|
982
|
+
|
|
983
|
+
await adapter.startSession({
|
|
984
|
+
cwd: '/tmp/workspace',
|
|
985
|
+
model: 'sonnet',
|
|
986
|
+
approvalMode: 'guarded',
|
|
987
|
+
sandboxMode: 'workspace-write',
|
|
988
|
+
});
|
|
989
|
+
await adapter.startTurn({
|
|
990
|
+
providerSessionId: 'claude-session-1',
|
|
991
|
+
providerTurnId: 'turn-1',
|
|
992
|
+
prompt: 'Investigate the failure',
|
|
993
|
+
} as any);
|
|
994
|
+
await wait();
|
|
995
|
+
|
|
996
|
+
expect(events).toContainEqual(
|
|
997
|
+
expect.objectContaining({
|
|
998
|
+
type: 'item.completed',
|
|
999
|
+
item: expect.objectContaining({
|
|
1000
|
+
id: 'msg_progress:content:0',
|
|
1001
|
+
kind: 'agentMessage',
|
|
1002
|
+
text: 'I found the failing integration.',
|
|
1003
|
+
}),
|
|
1004
|
+
}),
|
|
1005
|
+
);
|
|
1006
|
+
expect(events.some((event) => event.type === 'turn.completed')).toBe(false);
|
|
1007
|
+
|
|
1008
|
+
heldQuery.close();
|
|
1009
|
+
});
|
|
1010
|
+
|
|
827
1011
|
it('merges adjacent Claude thinking blocks into one reasoning item', () => {
|
|
828
1012
|
const items = assistantMessageToHistoryItems({
|
|
829
1013
|
messageId: 'msg_1',
|
|
@@ -1988,7 +2172,11 @@ describe('ClaudeRuntimeAdapter', () => {
|
|
|
1988
2172
|
: null,
|
|
1989
2173
|
)
|
|
1990
2174
|
.filter(Boolean),
|
|
1991
|
-
).toEqual([
|
|
2175
|
+
).toEqual([
|
|
2176
|
+
'claude-turn-visible',
|
|
2177
|
+
'claude-turn-visible',
|
|
2178
|
+
'claude-turn-visible',
|
|
2179
|
+
]);
|
|
1992
2180
|
});
|
|
1993
2181
|
|
|
1994
2182
|
it('interrupts an active query', async () => {
|
|
@@ -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();
|
|
@@ -1503,6 +1530,12 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
1503
1530
|
return {
|
|
1504
1531
|
...summary,
|
|
1505
1532
|
cwd,
|
|
1533
|
+
// Claude's persisted session metadata remains "idle" while a query is
|
|
1534
|
+
// streaming. The in-memory runtime is authoritative for that interval:
|
|
1535
|
+
// returning the stale persisted status makes detail polling clear the
|
|
1536
|
+
// active provider turn, which in turn drops live tool items from the
|
|
1537
|
+
// transcript until Claude writes its final history record.
|
|
1538
|
+
status: activeTurn ? 'running' : summary.status,
|
|
1506
1539
|
turns: this.reconcileActiveTranscriptTurn(providerSessionId, turns, activeTurn),
|
|
1507
1540
|
};
|
|
1508
1541
|
}
|
|
@@ -1760,12 +1793,17 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
1760
1793
|
const activeItems = [...activeTurn.itemOrder]
|
|
1761
1794
|
.map((itemId) => activeTurn.items.get(itemId))
|
|
1762
1795
|
.filter((item): item is AgentHistoryItem => Boolean(item));
|
|
1763
|
-
const
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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
|
+
});
|
|
1769
1807
|
|
|
1770
1808
|
if (transcriptTurnIndex < 0) {
|
|
1771
1809
|
return turns;
|
|
@@ -1778,6 +1816,14 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
1778
1816
|
aliases = new Map();
|
|
1779
1817
|
this.historicalTurnIdAliases.set(providerSessionId, aliases);
|
|
1780
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
|
+
}
|
|
1781
1827
|
aliases.set(transcriptTurn.providerTurnId, activeTurn.providerTurnId);
|
|
1782
1828
|
}
|
|
1783
1829
|
|
|
@@ -1806,9 +1852,20 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
1806
1852
|
if (!aliases || aliases.size === 0) {
|
|
1807
1853
|
return turns;
|
|
1808
1854
|
}
|
|
1855
|
+
const rawTurnIds = new Set(turns.map((turn) => turn.providerTurnId));
|
|
1856
|
+
const usedTurnIds = new Set<string>();
|
|
1809
1857
|
return turns.map((turn) => {
|
|
1810
1858
|
const providerTurnId = aliases.get(turn.providerTurnId);
|
|
1811
|
-
|
|
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 };
|
|
1812
1869
|
});
|
|
1813
1870
|
}
|
|
1814
1871
|
|
|
@@ -2054,9 +2111,15 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
2054
2111
|
const nextItem = withHistoryItemCreatedAt(item, messageCreatedAt);
|
|
2055
2112
|
const existing = state.items.get(nextItem.id);
|
|
2056
2113
|
addOrUpdateItem(state, nextItem);
|
|
2057
|
-
if (nextItem.kind
|
|
2114
|
+
if (nextItem.kind === 'agentMessage') {
|
|
2115
|
+
// Unlike stream_event text deltas, complete assistant messages are
|
|
2116
|
+
// delivered as SDK assistant events. Emit these immediately so
|
|
2117
|
+
// progress text between Claude tool calls is visible before the
|
|
2118
|
+
// enclosing turn finishes.
|
|
2119
|
+
this.emitItem(state, nextItem, 'item.completed', { force: true });
|
|
2120
|
+
} else if (!existing) {
|
|
2058
2121
|
this.emitItem(state, nextItem, 'item.started');
|
|
2059
|
-
} else
|
|
2122
|
+
} else {
|
|
2060
2123
|
this.emitItem(state, nextItem, 'item.started', { force: true });
|
|
2061
2124
|
}
|
|
2062
2125
|
}
|