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.
- package/apps/relay-server/dist/index.js +136 -49
- package/apps/supervisor-api/dist/index.js +166 -20
- package/apps/supervisor-web/dist/assets/index-BcCLYWAf.css +1 -0
- package/apps/supervisor-web/dist/assets/index-BvxWpPKW.js +21 -0
- package/apps/supervisor-web/dist/assets/{thread-ui-xWvswa2v.js → thread-ui-BLThton-.js} +40 -38
- package/apps/supervisor-web/dist/index.html +3 -3
- package/package.json +1 -1
- package/packages/claude/src/runtimeAdapter.test.ts +135 -0
- package/packages/claude/src/runtimeAdapter.ts +156 -5
- package/packages/shared/src/index.ts +91 -1
- package/apps/supervisor-web/dist/assets/index-B-qj7e8G.js +0 -21
- package/apps/supervisor-web/dist/assets/index-CDFuOeJN.css +0 -1
|
@@ -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-
|
|
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-
|
|
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-
|
|
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
|
@@ -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
|
|
2510
|
-
const userStartedAt = isoFromUuidV7(
|
|
2660
|
+
const userMessageUuid = message.uuid ?? randomUUID();
|
|
2661
|
+
const userStartedAt = isoFromUuidV7(userMessageUuid);
|
|
2511
2662
|
const userItem = await this.userMessageToHistoryItem(
|
|
2512
|
-
|
|
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-${
|
|
2669
|
+
providerTurnId: `claude-turn-${userMessageUuid}`,
|
|
2519
2670
|
startedAt: userStartedAt,
|
|
2520
2671
|
items: [stampedUserItem],
|
|
2521
|
-
itemsById: new Map([[
|
|
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;
|