@lzdi/pty-remote-cli 0.1.3
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/bin/pty-remote-cli.js +24 -0
- package/cli.conf +51 -0
- package/codex_template.jsonl +1 -0
- package/package.json +45 -0
- package/scripts/ensure-node-pty-helper.js +24 -0
- package/src/attachments/manager.ts +196 -0
- package/src/cli/cli-config.ts +58 -0
- package/src/cli/client.ts +674 -0
- package/src/cli/jsonl.ts +483 -0
- package/src/cli/pty-manager.ts +1509 -0
- package/src/cli/pty.ts +162 -0
- package/src/cli-main.ts +18 -0
- package/src/project-history.ts +175 -0
- package/src/providers/claude-history.ts +124 -0
- package/src/providers/claude.ts +66 -0
- package/src/providers/codex-history.ts +390 -0
- package/src/providers/codex-jsonl.ts +604 -0
- package/src/providers/codex-manager.ts +1662 -0
- package/src/providers/codex-pty.ts +144 -0
- package/src/providers/codex-resume-session.ts +253 -0
- package/src/providers/codex.ts +67 -0
- package/src/providers/provider-runtime.ts +58 -0
- package/src/providers/slash-commands.ts +115 -0
- package/src/terminal/frame-state.ts +457 -0
- package/src/threads-cli.ts +164 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChatMessage,
|
|
3
|
+
ChatMessageBlock,
|
|
4
|
+
TextChatMessageBlock,
|
|
5
|
+
ToolResultChatMessageBlock,
|
|
6
|
+
ToolUseChatMessageBlock
|
|
7
|
+
} from '@lzdi/pty-remote-protocol/runtime-types.ts';
|
|
8
|
+
|
|
9
|
+
interface CodexTextContentBlock {
|
|
10
|
+
type?: string;
|
|
11
|
+
text?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface CodexResponseItemPayload {
|
|
15
|
+
type?: string;
|
|
16
|
+
role?: string;
|
|
17
|
+
phase?: string;
|
|
18
|
+
content?: string | CodexTextContentBlock[];
|
|
19
|
+
arguments?: string;
|
|
20
|
+
input?: unknown;
|
|
21
|
+
output?: unknown;
|
|
22
|
+
call_id?: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
status?: string;
|
|
25
|
+
action?: {
|
|
26
|
+
type?: string;
|
|
27
|
+
query?: string;
|
|
28
|
+
queries?: string[];
|
|
29
|
+
url?: string;
|
|
30
|
+
pattern?: string;
|
|
31
|
+
} | Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface CodexEventMsgPayload {
|
|
35
|
+
type?: string;
|
|
36
|
+
message?: string;
|
|
37
|
+
text?: string;
|
|
38
|
+
phase?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface CodexJsonlRecord {
|
|
42
|
+
timestamp?: string;
|
|
43
|
+
type?: string;
|
|
44
|
+
payload?: CodexResponseItemPayload | CodexEventMsgPayload;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type CodexJsonlRuntimePhase = 'idle' | 'running';
|
|
48
|
+
|
|
49
|
+
export interface CodexJsonlMessagesState {
|
|
50
|
+
orderedIds: string[];
|
|
51
|
+
messagesById: Map<string, ChatMessage>;
|
|
52
|
+
runtimePhase: CodexJsonlRuntimePhase;
|
|
53
|
+
activityRevision: number;
|
|
54
|
+
messageSequence: number;
|
|
55
|
+
seenAssistantTextKeys: Set<string>;
|
|
56
|
+
seenUserTextKeys: Set<string>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createCodexJsonlMessagesState(): CodexJsonlMessagesState {
|
|
60
|
+
return {
|
|
61
|
+
orderedIds: [],
|
|
62
|
+
messagesById: new Map<string, ChatMessage>(),
|
|
63
|
+
runtimePhase: 'idle',
|
|
64
|
+
activityRevision: 0,
|
|
65
|
+
messageSequence: 0,
|
|
66
|
+
seenAssistantTextKeys: new Set<string>(),
|
|
67
|
+
seenUserTextKeys: new Set<string>()
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeAssistantText(text: string): string {
|
|
72
|
+
return text.trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isSyntheticEnvironmentContext(text: string): boolean {
|
|
76
|
+
const normalized = text.trim();
|
|
77
|
+
return /^<environment_context>\s*[\s\S]*<\/environment_context>$/.test(normalized);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function hashText(input: string): string {
|
|
81
|
+
let hash = 2166136261;
|
|
82
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
83
|
+
hash ^= input.charCodeAt(index);
|
|
84
|
+
hash = Math.imul(hash, 16777619);
|
|
85
|
+
}
|
|
86
|
+
return (hash >>> 0).toString(36);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function createStableTextMessageId(kind: string, timestamp: string | undefined, text: string, sequence: number): string {
|
|
90
|
+
const normalized = text.trim();
|
|
91
|
+
const digest = hashText(normalized);
|
|
92
|
+
const normalizedTimestamp = timestamp?.trim();
|
|
93
|
+
if (normalizedTimestamp) {
|
|
94
|
+
return `${kind}:${normalizedTimestamp}:${digest}`;
|
|
95
|
+
}
|
|
96
|
+
return `${kind}:seq:${sequence}:${digest}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function rememberAssistantText(
|
|
100
|
+
state: CodexJsonlMessagesState,
|
|
101
|
+
timestamp: string | undefined,
|
|
102
|
+
text: string
|
|
103
|
+
): boolean {
|
|
104
|
+
const normalizedText = normalizeAssistantText(text);
|
|
105
|
+
if (!normalizedText) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const parsedTimestampMs = new Date(timestamp ?? '').getTime();
|
|
110
|
+
const timestampBucket = Number.isFinite(parsedTimestampMs)
|
|
111
|
+
? String(Math.floor(parsedTimestampMs / 1_000))
|
|
112
|
+
: `seq:${Math.floor(state.messageSequence / 4)}`;
|
|
113
|
+
const dedupeKey = `${timestampBucket}\u0000${normalizedText}`;
|
|
114
|
+
if (state.seenAssistantTextKeys.has(dedupeKey)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
state.seenAssistantTextKeys.add(dedupeKey);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function rememberUserText(
|
|
123
|
+
state: CodexJsonlMessagesState,
|
|
124
|
+
timestamp: string | undefined,
|
|
125
|
+
text: string
|
|
126
|
+
): boolean {
|
|
127
|
+
const normalizedText = text.trim();
|
|
128
|
+
if (!normalizedText) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const parsedTimestampMs = new Date(timestamp ?? '').getTime();
|
|
133
|
+
const timestampBucket = Number.isFinite(parsedTimestampMs)
|
|
134
|
+
? String(Math.floor(parsedTimestampMs / 1_000))
|
|
135
|
+
: `seq:${Math.floor(state.messageSequence / 4)}`;
|
|
136
|
+
const dedupeKey = `${timestampBucket}\u0000${normalizedText}`;
|
|
137
|
+
if (state.seenUserTextKeys.has(dedupeKey)) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
state.seenUserTextKeys.add(dedupeKey);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function stringifyUnknown(value: unknown): string {
|
|
146
|
+
if (typeof value === 'string') {
|
|
147
|
+
return value.trim();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (value === null || value === undefined) {
|
|
151
|
+
return '';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (typeof value === 'object') {
|
|
155
|
+
return JSON.stringify(value, null, 2);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return String(value);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function normalizeCreatedAt(timestamp: string | undefined, sequence: number): string {
|
|
162
|
+
const parsed = new Date(timestamp ?? '').getTime();
|
|
163
|
+
if (Number.isFinite(parsed)) {
|
|
164
|
+
return new Date(parsed).toISOString();
|
|
165
|
+
}
|
|
166
|
+
return new Date(sequence * 1_000).toISOString();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function hasVisibleBlocks(blocks: ChatMessageBlock[]): boolean {
|
|
170
|
+
return blocks.some((block) => {
|
|
171
|
+
switch (block.type) {
|
|
172
|
+
case 'text':
|
|
173
|
+
return Boolean(block.text.trim());
|
|
174
|
+
case 'tool_use':
|
|
175
|
+
return Boolean(block.toolName || block.input);
|
|
176
|
+
case 'tool_result':
|
|
177
|
+
return Boolean(block.content.trim());
|
|
178
|
+
default:
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function createTextBlock(baseId: string, text: string, index = 0): TextChatMessageBlock | null {
|
|
185
|
+
const normalized = text.trimEnd();
|
|
186
|
+
if (!normalized.trim()) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
id: `${baseId}:text:${index}`,
|
|
192
|
+
type: 'text',
|
|
193
|
+
text: normalized
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function createToolUseBlock(callId: string, toolName: string, input: string): ToolUseChatMessageBlock {
|
|
198
|
+
return {
|
|
199
|
+
id: `tool:${callId}:use`,
|
|
200
|
+
type: 'tool_use',
|
|
201
|
+
toolCallId: callId,
|
|
202
|
+
toolName: toolName.trim() || 'unknown',
|
|
203
|
+
input
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function createToolResultBlock(callId: string, content: string, isError: boolean): ToolResultChatMessageBlock {
|
|
208
|
+
return {
|
|
209
|
+
id: `tool:${callId}:result`,
|
|
210
|
+
type: 'tool_result',
|
|
211
|
+
toolCallId: callId,
|
|
212
|
+
content,
|
|
213
|
+
isError
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function getWebSearchQuery(action: CodexResponseItemPayload['action']): string {
|
|
218
|
+
if (!action || typeof action !== 'object') {
|
|
219
|
+
return '';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const normalizedAction = action as {
|
|
223
|
+
type?: string;
|
|
224
|
+
query?: string;
|
|
225
|
+
queries?: string[];
|
|
226
|
+
};
|
|
227
|
+
if (normalizedAction.type !== 'search') {
|
|
228
|
+
return '';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (typeof normalizedAction.query === 'string' && normalizedAction.query.trim()) {
|
|
232
|
+
return normalizedAction.query.trim();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (Array.isArray(normalizedAction.queries)) {
|
|
236
|
+
const firstQuery = normalizedAction.queries.find((query) => typeof query === 'string' && query.trim());
|
|
237
|
+
return firstQuery?.trim() ?? '';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return '';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function mergeMessageBlocks(existingBlocks: ChatMessageBlock[], nextBlocks: ChatMessageBlock[]): ChatMessageBlock[] {
|
|
244
|
+
if (existingBlocks.length === 0) {
|
|
245
|
+
return nextBlocks;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (nextBlocks.length === 0) {
|
|
249
|
+
return existingBlocks;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const merged = existingBlocks.slice();
|
|
253
|
+
const blockIndexById = new Map(merged.map((block, index) => [block.id, index]));
|
|
254
|
+
|
|
255
|
+
for (const block of nextBlocks) {
|
|
256
|
+
const existingIndex = blockIndexById.get(block.id);
|
|
257
|
+
if (existingIndex === undefined) {
|
|
258
|
+
blockIndexById.set(block.id, merged.length);
|
|
259
|
+
merged.push(block);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
merged[existingIndex] = block;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return merged;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function deriveMessageStatus(
|
|
269
|
+
blocks: ChatMessageBlock[],
|
|
270
|
+
runtimePhase: CodexJsonlRuntimePhase
|
|
271
|
+
): ChatMessage['status'] {
|
|
272
|
+
if (blocks.some((block) => block.type === 'tool_result' && block.isError)) {
|
|
273
|
+
return 'error';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const hasToolUse = blocks.some((block) => block.type === 'tool_use');
|
|
277
|
+
const hasToolResult = blocks.some((block) => block.type === 'tool_result');
|
|
278
|
+
if (hasToolUse && !hasToolResult && runtimePhase === 'running') {
|
|
279
|
+
return 'streaming';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return 'complete';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function refreshCodexJsonlMessageStatuses(state: CodexJsonlMessagesState): void {
|
|
286
|
+
for (const [messageId, message] of state.messagesById.entries()) {
|
|
287
|
+
const nextStatus = deriveMessageStatus(message.blocks, state.runtimePhase);
|
|
288
|
+
if (message.status === nextStatus) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
state.messagesById.set(messageId, {
|
|
292
|
+
...message,
|
|
293
|
+
status: nextStatus
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function upsertMessage(state: CodexJsonlMessagesState, nextMessage: ChatMessage): void {
|
|
299
|
+
const existing = state.messagesById.get(nextMessage.id);
|
|
300
|
+
const blocks = hasVisibleBlocks(nextMessage.blocks)
|
|
301
|
+
? mergeMessageBlocks(existing?.blocks ?? [], nextMessage.blocks)
|
|
302
|
+
: existing?.blocks ?? [];
|
|
303
|
+
|
|
304
|
+
if (!hasVisibleBlocks(blocks)) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const mergedMessage: ChatMessage = {
|
|
309
|
+
id: nextMessage.id,
|
|
310
|
+
role: nextMessage.role,
|
|
311
|
+
blocks,
|
|
312
|
+
status: deriveMessageStatus(blocks, state.runtimePhase),
|
|
313
|
+
createdAt: existing?.createdAt ?? nextMessage.createdAt
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
if (!existing) {
|
|
317
|
+
state.orderedIds.push(nextMessage.id);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
state.messagesById.set(nextMessage.id, mergedMessage);
|
|
321
|
+
state.activityRevision += 1;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function extractTextBlocks(content: string | CodexTextContentBlock[] | undefined, baseId: string): ChatMessageBlock[] {
|
|
325
|
+
if (typeof content === 'string') {
|
|
326
|
+
const textBlock = createTextBlock(baseId, content);
|
|
327
|
+
return textBlock ? [textBlock] : [];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!Array.isArray(content)) {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const blocks: ChatMessageBlock[] = [];
|
|
335
|
+
for (const [index, block] of content.entries()) {
|
|
336
|
+
if (!block || typeof block !== 'object') {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if ((block.type === 'input_text' || block.type === 'output_text') && typeof block.text === 'string') {
|
|
341
|
+
const textBlock = createTextBlock(baseId, block.text, index);
|
|
342
|
+
if (textBlock) {
|
|
343
|
+
blocks.push(textBlock);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return blocks;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function applyEventMsg(state: CodexJsonlMessagesState, payload: CodexEventMsgPayload | undefined, timestamp: string | undefined): void {
|
|
352
|
+
const payloadType = payload?.type;
|
|
353
|
+
if (payloadType === 'user_message') {
|
|
354
|
+
const sequence = state.messageSequence++;
|
|
355
|
+
const messageText = payload?.message ?? '';
|
|
356
|
+
if (isSyntheticEnvironmentContext(messageText)) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (!rememberUserText(state, timestamp, messageText)) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const messageId = createStableTextMessageId('codex:user', timestamp, messageText, sequence);
|
|
363
|
+
const textBlock = createTextBlock(messageId, messageText);
|
|
364
|
+
if (!textBlock) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
upsertMessage(state, {
|
|
369
|
+
id: messageId,
|
|
370
|
+
role: 'user',
|
|
371
|
+
blocks: [textBlock],
|
|
372
|
+
status: 'complete',
|
|
373
|
+
createdAt: normalizeCreatedAt(timestamp, sequence)
|
|
374
|
+
});
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (payloadType === 'agent_reasoning') {
|
|
379
|
+
const reasoningText = typeof payload?.text === 'string' ? payload.text.trim() : '';
|
|
380
|
+
if (!reasoningText) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (!rememberAssistantText(state, timestamp, reasoningText)) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const sequence = state.messageSequence++;
|
|
388
|
+
const messageId = createStableTextMessageId('codex:assistant_reasoning', timestamp, reasoningText, sequence);
|
|
389
|
+
const textBlock = createTextBlock(messageId, reasoningText);
|
|
390
|
+
if (!textBlock) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
upsertMessage(state, {
|
|
395
|
+
id: messageId,
|
|
396
|
+
role: 'assistant',
|
|
397
|
+
blocks: [textBlock],
|
|
398
|
+
status: 'complete',
|
|
399
|
+
createdAt: normalizeCreatedAt(timestamp, sequence)
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (payloadType === 'agent_message') {
|
|
405
|
+
const messageText = typeof payload?.message === 'string' ? payload.message.trim() : '';
|
|
406
|
+
if (!messageText) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (!rememberAssistantText(state, timestamp, messageText)) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const sequence = state.messageSequence++;
|
|
414
|
+
const messageId = createStableTextMessageId('codex:assistant_text', timestamp, messageText, sequence);
|
|
415
|
+
const textBlock = createTextBlock(messageId, messageText);
|
|
416
|
+
if (!textBlock) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
upsertMessage(state, {
|
|
421
|
+
id: messageId,
|
|
422
|
+
role: 'assistant',
|
|
423
|
+
blocks: [textBlock],
|
|
424
|
+
status: 'complete',
|
|
425
|
+
createdAt: normalizeCreatedAt(timestamp, sequence)
|
|
426
|
+
});
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let nextPhase: CodexJsonlRuntimePhase | null = null;
|
|
431
|
+
|
|
432
|
+
if (payloadType === 'task_started') {
|
|
433
|
+
nextPhase = 'running';
|
|
434
|
+
} else if (payloadType === 'task_complete' || payloadType === 'turn_aborted') {
|
|
435
|
+
nextPhase = 'idle';
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (!nextPhase || nextPhase === state.runtimePhase) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
state.runtimePhase = nextPhase;
|
|
443
|
+
state.activityRevision += 1;
|
|
444
|
+
refreshCodexJsonlMessageStatuses(state);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function applyResponseItem(
|
|
448
|
+
state: CodexJsonlMessagesState,
|
|
449
|
+
payload: CodexResponseItemPayload | undefined,
|
|
450
|
+
timestamp: string | undefined
|
|
451
|
+
): void {
|
|
452
|
+
const payloadType = payload?.type;
|
|
453
|
+
if (!payloadType) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (payloadType === 'message') {
|
|
458
|
+
const role = payload?.role;
|
|
459
|
+
if (role !== 'assistant' && role !== 'user') {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const sequence = state.messageSequence++;
|
|
464
|
+
const provisionalMessageId = `codex:assistant:${sequence}`;
|
|
465
|
+
const blocks = extractTextBlocks(payload.content, provisionalMessageId);
|
|
466
|
+
if (blocks.length === 0) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const messageText = blocks
|
|
470
|
+
.filter((block): block is TextChatMessageBlock => block.type === 'text')
|
|
471
|
+
.map((block) => block.text)
|
|
472
|
+
.join('\n')
|
|
473
|
+
.trim();
|
|
474
|
+
if (role === 'user' && isSyntheticEnvironmentContext(messageText)) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (role === 'user' && !rememberUserText(state, timestamp, messageText)) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (role === 'assistant' && !rememberAssistantText(state, timestamp, messageText)) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const stableMessageId = createStableTextMessageId(
|
|
485
|
+
role === 'assistant' ? 'codex:assistant_text' : 'codex:user',
|
|
486
|
+
timestamp,
|
|
487
|
+
messageText,
|
|
488
|
+
sequence
|
|
489
|
+
);
|
|
490
|
+
const stableBlocks = extractTextBlocks(payload.content, stableMessageId);
|
|
491
|
+
if (stableBlocks.length === 0) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
upsertMessage(state, {
|
|
496
|
+
id: stableMessageId,
|
|
497
|
+
role,
|
|
498
|
+
blocks: stableBlocks,
|
|
499
|
+
status: 'complete',
|
|
500
|
+
createdAt: normalizeCreatedAt(timestamp, sequence)
|
|
501
|
+
});
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (payloadType === 'function_call' || payloadType === 'custom_tool_call') {
|
|
506
|
+
const callId = payload.call_id?.trim();
|
|
507
|
+
if (!callId) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const rawInput = payloadType === 'custom_tool_call' ? payload.input : payload.arguments;
|
|
512
|
+
upsertMessage(state, {
|
|
513
|
+
id: `tool:${callId}`,
|
|
514
|
+
role: 'assistant',
|
|
515
|
+
blocks: [createToolUseBlock(callId, payload.name ?? 'unknown', stringifyUnknown(rawInput))],
|
|
516
|
+
status: 'streaming',
|
|
517
|
+
createdAt: normalizeCreatedAt(timestamp, state.messageSequence++)
|
|
518
|
+
});
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (payloadType === 'web_search_call') {
|
|
523
|
+
const query = getWebSearchQuery(payload.action);
|
|
524
|
+
if (!query) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const sequence = state.messageSequence++;
|
|
529
|
+
const callId = `web_search_${sequence}`;
|
|
530
|
+
upsertMessage(state, {
|
|
531
|
+
id: `tool:${callId}`,
|
|
532
|
+
role: 'assistant',
|
|
533
|
+
blocks: [createToolUseBlock(callId, 'web_search', query)],
|
|
534
|
+
status: 'complete',
|
|
535
|
+
createdAt: normalizeCreatedAt(timestamp, sequence)
|
|
536
|
+
});
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (payloadType === 'function_call_output' || payloadType === 'custom_tool_call_output') {
|
|
541
|
+
const callId = payload.call_id?.trim();
|
|
542
|
+
if (!callId) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const normalizedStatus = payload.status?.trim().toLowerCase();
|
|
547
|
+
const isError = normalizedStatus === 'error' || normalizedStatus === 'failed' || normalizedStatus === 'cancelled';
|
|
548
|
+
upsertMessage(state, {
|
|
549
|
+
id: `tool:${callId}`,
|
|
550
|
+
role: 'assistant',
|
|
551
|
+
blocks: [createToolResultBlock(callId, stringifyUnknown(payload.output), isError)],
|
|
552
|
+
status: isError ? 'error' : 'complete',
|
|
553
|
+
createdAt: normalizeCreatedAt(timestamp, state.messageSequence++)
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function applyCodexJsonlLine(state: CodexJsonlMessagesState, line: string): boolean {
|
|
559
|
+
const trimmed = line.trim();
|
|
560
|
+
if (!trimmed) {
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
let parsed: CodexJsonlRecord;
|
|
565
|
+
try {
|
|
566
|
+
parsed = JSON.parse(trimmed) as CodexJsonlRecord;
|
|
567
|
+
} catch {
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (parsed.type === 'event_msg') {
|
|
572
|
+
applyEventMsg(state, parsed.payload as CodexEventMsgPayload | undefined, parsed.timestamp);
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (parsed.type === 'response_item') {
|
|
577
|
+
applyResponseItem(state, parsed.payload as CodexResponseItemPayload | undefined, parsed.timestamp);
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export function materializeCodexJsonlMessages(state: CodexJsonlMessagesState): ChatMessage[] {
|
|
585
|
+
return state.orderedIds
|
|
586
|
+
.map((messageId) => state.messagesById.get(messageId))
|
|
587
|
+
.filter((message): message is ChatMessage => Boolean(message));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export function parseCodexJsonlMessages(raw: string): {
|
|
591
|
+
isRunning: boolean;
|
|
592
|
+
messages: ChatMessage[];
|
|
593
|
+
} {
|
|
594
|
+
const state = createCodexJsonlMessagesState();
|
|
595
|
+
|
|
596
|
+
for (const line of raw.split('\n')) {
|
|
597
|
+
applyCodexJsonlLine(state, line);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
isRunning: state.runtimePhase === 'running',
|
|
602
|
+
messages: materializeCodexJsonlMessages(state)
|
|
603
|
+
};
|
|
604
|
+
}
|