@lzdi/pty-remote-cli 0.1.3 → 0.1.4
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/package.json +2 -2
- package/src/providers/codex-jsonl.ts +85 -11
- package/src/providers/codex-manager.ts +78 -43
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lzdi/pty-remote-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"threads": "node bin/pty-remote-cli.js threads"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@lzdi/pty-remote-protocol": "0.1.
|
|
25
|
+
"@lzdi/pty-remote-protocol": "0.1.4",
|
|
26
26
|
"@xterm/headless": "^6.0.0",
|
|
27
27
|
"node-pty": "^1.1.0",
|
|
28
28
|
"socket.io-client": "^4.8.3",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ChatMessage,
|
|
3
|
+
ChatMessageMeta,
|
|
3
4
|
ChatMessageBlock,
|
|
4
5
|
TextChatMessageBlock,
|
|
5
6
|
ToolResultChatMessageBlock,
|
|
@@ -36,12 +37,17 @@ interface CodexEventMsgPayload {
|
|
|
36
37
|
message?: string;
|
|
37
38
|
text?: string;
|
|
38
39
|
phase?: string;
|
|
40
|
+
turn_id?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface CodexTurnContextPayload {
|
|
44
|
+
turn_id?: string;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
interface CodexJsonlRecord {
|
|
42
48
|
timestamp?: string;
|
|
43
49
|
type?: string;
|
|
44
|
-
payload?: CodexResponseItemPayload | CodexEventMsgPayload;
|
|
50
|
+
payload?: CodexResponseItemPayload | CodexEventMsgPayload | CodexTurnContextPayload;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
export type CodexJsonlRuntimePhase = 'idle' | 'running';
|
|
@@ -50,6 +56,7 @@ export interface CodexJsonlMessagesState {
|
|
|
50
56
|
orderedIds: string[];
|
|
51
57
|
messagesById: Map<string, ChatMessage>;
|
|
52
58
|
runtimePhase: CodexJsonlRuntimePhase;
|
|
59
|
+
activeTurnId: string | null;
|
|
53
60
|
activityRevision: number;
|
|
54
61
|
messageSequence: number;
|
|
55
62
|
seenAssistantTextKeys: Set<string>;
|
|
@@ -61,6 +68,7 @@ export function createCodexJsonlMessagesState(): CodexJsonlMessagesState {
|
|
|
61
68
|
orderedIds: [],
|
|
62
69
|
messagesById: new Map<string, ChatMessage>(),
|
|
63
70
|
runtimePhase: 'idle',
|
|
71
|
+
activeTurnId: null,
|
|
64
72
|
activityRevision: 0,
|
|
65
73
|
messageSequence: 0,
|
|
66
74
|
seenAssistantTextKeys: new Set<string>(),
|
|
@@ -166,6 +174,25 @@ function normalizeCreatedAt(timestamp: string | undefined, sequence: number): st
|
|
|
166
174
|
return new Date(sequence * 1_000).toISOString();
|
|
167
175
|
}
|
|
168
176
|
|
|
177
|
+
function normalizeTurnId(turnId: string | undefined): string | null {
|
|
178
|
+
const normalized = turnId?.trim();
|
|
179
|
+
return normalized ? normalized : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function createMessageMeta(state: CodexJsonlMessagesState, phase?: string | null): ChatMessageMeta | undefined {
|
|
183
|
+
const normalizedPhase = phase?.trim() || null;
|
|
184
|
+
const turnId = state.activeTurnId;
|
|
185
|
+
|
|
186
|
+
if (!normalizedPhase && !turnId) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
phase: normalizedPhase,
|
|
192
|
+
turnId
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
169
196
|
function hasVisibleBlocks(blocks: ChatMessageBlock[]): boolean {
|
|
170
197
|
return blocks.some((block) => {
|
|
171
198
|
switch (block.type) {
|
|
@@ -214,27 +241,42 @@ function createToolResultBlock(callId: string, content: string, isError: boolean
|
|
|
214
241
|
};
|
|
215
242
|
}
|
|
216
243
|
|
|
217
|
-
function
|
|
244
|
+
function getWebSearchPreview(action: CodexResponseItemPayload['action']): string {
|
|
218
245
|
if (!action || typeof action !== 'object') {
|
|
219
246
|
return '';
|
|
220
247
|
}
|
|
221
248
|
|
|
222
249
|
const normalizedAction = action as {
|
|
250
|
+
pattern?: string;
|
|
223
251
|
type?: string;
|
|
224
252
|
query?: string;
|
|
225
253
|
queries?: string[];
|
|
254
|
+
url?: string;
|
|
226
255
|
};
|
|
227
|
-
if (normalizedAction.type
|
|
256
|
+
if (normalizedAction.type === 'search') {
|
|
257
|
+
if (typeof normalizedAction.query === 'string' && normalizedAction.query.trim()) {
|
|
258
|
+
return normalizedAction.query.trim();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (Array.isArray(normalizedAction.queries)) {
|
|
262
|
+
const firstQuery = normalizedAction.queries.find((query) => typeof query === 'string' && query.trim());
|
|
263
|
+
return firstQuery?.trim() ?? '';
|
|
264
|
+
}
|
|
265
|
+
|
|
228
266
|
return '';
|
|
229
267
|
}
|
|
230
268
|
|
|
231
|
-
if (
|
|
232
|
-
return normalizedAction.
|
|
269
|
+
if (normalizedAction.type === 'open_page') {
|
|
270
|
+
return normalizedAction.url?.trim() ?? '';
|
|
233
271
|
}
|
|
234
272
|
|
|
235
|
-
if (
|
|
236
|
-
const
|
|
237
|
-
|
|
273
|
+
if (normalizedAction.type === 'find_in_page') {
|
|
274
|
+
const pattern = normalizedAction.pattern?.trim() ?? '';
|
|
275
|
+
const url = normalizedAction.url?.trim() ?? '';
|
|
276
|
+
if (pattern && url) {
|
|
277
|
+
return `${pattern} @ ${url}`;
|
|
278
|
+
}
|
|
279
|
+
return pattern || url;
|
|
238
280
|
}
|
|
239
281
|
|
|
240
282
|
return '';
|
|
@@ -282,6 +324,20 @@ function deriveMessageStatus(
|
|
|
282
324
|
return 'complete';
|
|
283
325
|
}
|
|
284
326
|
|
|
327
|
+
function mergeMessageMeta(existing: ChatMessageMeta | undefined, next: ChatMessageMeta | undefined): ChatMessageMeta | undefined {
|
|
328
|
+
const phase = next?.phase ?? existing?.phase ?? null;
|
|
329
|
+
const turnId = next?.turnId ?? existing?.turnId ?? null;
|
|
330
|
+
|
|
331
|
+
if (!phase && !turnId) {
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
phase,
|
|
337
|
+
turnId
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
285
341
|
export function refreshCodexJsonlMessageStatuses(state: CodexJsonlMessagesState): void {
|
|
286
342
|
for (const [messageId, message] of state.messagesById.entries()) {
|
|
287
343
|
const nextStatus = deriveMessageStatus(message.blocks, state.runtimePhase);
|
|
@@ -309,6 +365,7 @@ function upsertMessage(state: CodexJsonlMessagesState, nextMessage: ChatMessage)
|
|
|
309
365
|
id: nextMessage.id,
|
|
310
366
|
role: nextMessage.role,
|
|
311
367
|
blocks,
|
|
368
|
+
meta: mergeMessageMeta(existing?.meta, nextMessage.meta),
|
|
312
369
|
status: deriveMessageStatus(blocks, state.runtimePhase),
|
|
313
370
|
createdAt: existing?.createdAt ?? nextMessage.createdAt
|
|
314
371
|
};
|
|
@@ -369,6 +426,7 @@ function applyEventMsg(state: CodexJsonlMessagesState, payload: CodexEventMsgPay
|
|
|
369
426
|
id: messageId,
|
|
370
427
|
role: 'user',
|
|
371
428
|
blocks: [textBlock],
|
|
429
|
+
meta: createMessageMeta(state),
|
|
372
430
|
status: 'complete',
|
|
373
431
|
createdAt: normalizeCreatedAt(timestamp, sequence)
|
|
374
432
|
});
|
|
@@ -395,6 +453,7 @@ function applyEventMsg(state: CodexJsonlMessagesState, payload: CodexEventMsgPay
|
|
|
395
453
|
id: messageId,
|
|
396
454
|
role: 'assistant',
|
|
397
455
|
blocks: [textBlock],
|
|
456
|
+
meta: createMessageMeta(state),
|
|
398
457
|
status: 'complete',
|
|
399
458
|
createdAt: normalizeCreatedAt(timestamp, sequence)
|
|
400
459
|
});
|
|
@@ -421,6 +480,7 @@ function applyEventMsg(state: CodexJsonlMessagesState, payload: CodexEventMsgPay
|
|
|
421
480
|
id: messageId,
|
|
422
481
|
role: 'assistant',
|
|
423
482
|
blocks: [textBlock],
|
|
483
|
+
meta: createMessageMeta(state, payload?.phase),
|
|
424
484
|
status: 'complete',
|
|
425
485
|
createdAt: normalizeCreatedAt(timestamp, sequence)
|
|
426
486
|
});
|
|
@@ -428,11 +488,16 @@ function applyEventMsg(state: CodexJsonlMessagesState, payload: CodexEventMsgPay
|
|
|
428
488
|
}
|
|
429
489
|
|
|
430
490
|
let nextPhase: CodexJsonlRuntimePhase | null = null;
|
|
491
|
+
const payloadTurnId = normalizeTurnId(payload?.turn_id);
|
|
431
492
|
|
|
432
493
|
if (payloadType === 'task_started') {
|
|
433
494
|
nextPhase = 'running';
|
|
495
|
+
state.activeTurnId = payloadTurnId ?? state.activeTurnId;
|
|
434
496
|
} else if (payloadType === 'task_complete' || payloadType === 'turn_aborted') {
|
|
435
497
|
nextPhase = 'idle';
|
|
498
|
+
if (!payloadTurnId || payloadTurnId === state.activeTurnId) {
|
|
499
|
+
state.activeTurnId = null;
|
|
500
|
+
}
|
|
436
501
|
}
|
|
437
502
|
|
|
438
503
|
if (!nextPhase || nextPhase === state.runtimePhase) {
|
|
@@ -496,6 +561,7 @@ function applyResponseItem(
|
|
|
496
561
|
id: stableMessageId,
|
|
497
562
|
role,
|
|
498
563
|
blocks: stableBlocks,
|
|
564
|
+
meta: createMessageMeta(state, payload.phase),
|
|
499
565
|
status: 'complete',
|
|
500
566
|
createdAt: normalizeCreatedAt(timestamp, sequence)
|
|
501
567
|
});
|
|
@@ -513,6 +579,7 @@ function applyResponseItem(
|
|
|
513
579
|
id: `tool:${callId}`,
|
|
514
580
|
role: 'assistant',
|
|
515
581
|
blocks: [createToolUseBlock(callId, payload.name ?? 'unknown', stringifyUnknown(rawInput))],
|
|
582
|
+
meta: createMessageMeta(state),
|
|
516
583
|
status: 'streaming',
|
|
517
584
|
createdAt: normalizeCreatedAt(timestamp, state.messageSequence++)
|
|
518
585
|
});
|
|
@@ -520,8 +587,8 @@ function applyResponseItem(
|
|
|
520
587
|
}
|
|
521
588
|
|
|
522
589
|
if (payloadType === 'web_search_call') {
|
|
523
|
-
const
|
|
524
|
-
if (!
|
|
590
|
+
const preview = getWebSearchPreview(payload.action);
|
|
591
|
+
if (!preview) {
|
|
525
592
|
return;
|
|
526
593
|
}
|
|
527
594
|
|
|
@@ -530,7 +597,8 @@ function applyResponseItem(
|
|
|
530
597
|
upsertMessage(state, {
|
|
531
598
|
id: `tool:${callId}`,
|
|
532
599
|
role: 'assistant',
|
|
533
|
-
blocks: [createToolUseBlock(callId, 'web_search',
|
|
600
|
+
blocks: [createToolUseBlock(callId, 'web_search', preview)],
|
|
601
|
+
meta: createMessageMeta(state),
|
|
534
602
|
status: 'complete',
|
|
535
603
|
createdAt: normalizeCreatedAt(timestamp, sequence)
|
|
536
604
|
});
|
|
@@ -549,6 +617,7 @@ function applyResponseItem(
|
|
|
549
617
|
id: `tool:${callId}`,
|
|
550
618
|
role: 'assistant',
|
|
551
619
|
blocks: [createToolResultBlock(callId, stringifyUnknown(payload.output), isError)],
|
|
620
|
+
meta: createMessageMeta(state),
|
|
552
621
|
status: isError ? 'error' : 'complete',
|
|
553
622
|
createdAt: normalizeCreatedAt(timestamp, state.messageSequence++)
|
|
554
623
|
});
|
|
@@ -578,6 +647,11 @@ export function applyCodexJsonlLine(state: CodexJsonlMessagesState, line: string
|
|
|
578
647
|
return true;
|
|
579
648
|
}
|
|
580
649
|
|
|
650
|
+
if (parsed.type === 'turn_context') {
|
|
651
|
+
state.activeTurnId = normalizeTurnId((parsed.payload as CodexTurnContextPayload | undefined)?.turn_id);
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
|
|
581
655
|
return true;
|
|
582
656
|
}
|
|
583
657
|
|
|
@@ -137,6 +137,7 @@ function messageEqual(left: ChatMessage | undefined, right: ChatMessage | undefi
|
|
|
137
137
|
left.role === right.role &&
|
|
138
138
|
left.status === right.status &&
|
|
139
139
|
left.createdAt === right.createdAt &&
|
|
140
|
+
JSON.stringify(left.meta ?? null) === JSON.stringify(right.meta ?? null) &&
|
|
140
141
|
JSON.stringify(left.blocks) === JSON.stringify(right.blocks)
|
|
141
142
|
);
|
|
142
143
|
}
|
|
@@ -158,6 +159,7 @@ function sleep(ms: number): Promise<void> {
|
|
|
158
159
|
const AWAITING_JSONL_TURN_STALE_MS = 4000;
|
|
159
160
|
const READY_TIMEOUT_MAX_RETRIES = 2;
|
|
160
161
|
const TERMINAL_READY_BOTTOM_LINES = 8;
|
|
162
|
+
const INTERRUPT_READY_CHECK_DELAY_MS = 250;
|
|
161
163
|
|
|
162
164
|
function materializeTerminalFrameLinesText(lines: TerminalFrameLine[]): string {
|
|
163
165
|
return lines
|
|
@@ -408,50 +410,9 @@ export class CodexManager {
|
|
|
408
410
|
return;
|
|
409
411
|
}
|
|
410
412
|
|
|
411
|
-
const previousMessages = handle.runtime.messages;
|
|
412
|
-
const previousHasOlderMessages = handle.runtime.hasOlderMessages;
|
|
413
|
-
|
|
414
413
|
this.clearLastError(handle);
|
|
415
|
-
handle
|
|
416
|
-
|
|
417
|
-
handle.jsonlMessagesState.runtimePhase = 'idle';
|
|
418
|
-
handle.jsonlMessagesState.activityRevision += 1;
|
|
419
|
-
refreshCodexJsonlMessageStatuses(handle.jsonlMessagesState);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const nextAllMessages = materializeCodexJsonlMessages(handle.jsonlMessagesState);
|
|
423
|
-
const nextMessages = this.selectRecentMessages(nextAllMessages);
|
|
424
|
-
const nextHasOlderMessages = nextAllMessages.length > nextMessages.length;
|
|
425
|
-
const allMessagesChanged = !messagesEqual(handle.runtime.allMessages, nextAllMessages);
|
|
426
|
-
const messagesChanged = !messagesEqual(handle.runtime.messages, nextMessages);
|
|
427
|
-
const hasOlderMessagesChanged = handle.runtime.hasOlderMessages !== nextHasOlderMessages;
|
|
428
|
-
|
|
429
|
-
if (allMessagesChanged) {
|
|
430
|
-
handle.runtime.allMessages = nextAllMessages;
|
|
431
|
-
}
|
|
432
|
-
if (messagesChanged) {
|
|
433
|
-
handle.runtime.messages = nextMessages;
|
|
434
|
-
}
|
|
435
|
-
if (allMessagesChanged || messagesChanged || hasOlderMessagesChanged) {
|
|
436
|
-
handle.runtime.hasOlderMessages = nextHasOlderMessages;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
this.stopHandlePtyPreservingReplay(handle, 'stop-active-run');
|
|
440
|
-
this.setStatus(handle, 'idle', true);
|
|
441
|
-
|
|
442
|
-
if (this.isActiveHandle(handle) && (allMessagesChanged || messagesChanged || hasOlderMessagesChanged)) {
|
|
443
|
-
const upsertPayload = this.createMessagesUpsertPayload(
|
|
444
|
-
handle,
|
|
445
|
-
previousMessages,
|
|
446
|
-
nextMessages,
|
|
447
|
-
previousHasOlderMessages,
|
|
448
|
-
nextHasOlderMessages
|
|
449
|
-
);
|
|
450
|
-
if (upsertPayload) {
|
|
451
|
-
this.callbacks.emitMessagesUpsert(upsertPayload);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
414
|
+
await this.sendInterruptSequence(handle);
|
|
415
|
+
await this.maybeMarkHandleReadyAfterInterrupt(handle);
|
|
455
416
|
this.scheduleJsonlRefresh(0, 'stop-active-run');
|
|
456
417
|
}
|
|
457
418
|
|
|
@@ -1543,6 +1504,80 @@ export class CodexManager {
|
|
|
1543
1504
|
}
|
|
1544
1505
|
}
|
|
1545
1506
|
|
|
1507
|
+
private async sendInterruptSequence(handle: CodexHandle): Promise<void> {
|
|
1508
|
+
if (!handle.pty) {
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
this.log('info', 'sending codex interrupt sequence', {
|
|
1513
|
+
...this.handleContext(handle),
|
|
1514
|
+
runtimeStatus: handle.runtime.status
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
handle.pty.pty.write('\x1b');
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
private async maybeMarkHandleReadyAfterInterrupt(handle: CodexHandle): Promise<void> {
|
|
1521
|
+
await sleep(INTERRUPT_READY_CHECK_DELAY_MS);
|
|
1522
|
+
if (!this.isActiveHandle(handle) || !handle.pty) {
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
const previousMessages = handle.runtime.messages;
|
|
1527
|
+
const previousHasOlderMessages = handle.runtime.hasOlderMessages;
|
|
1528
|
+
|
|
1529
|
+
try {
|
|
1530
|
+
const { bottomText } = await this.getHandleTerminalView(handle);
|
|
1531
|
+
if (!looksReadyForInput(bottomText)) {
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
handle.awaitingJsonlTurn = false;
|
|
1536
|
+
if (handle.jsonlMessagesState.runtimePhase !== 'idle') {
|
|
1537
|
+
handle.jsonlMessagesState.runtimePhase = 'idle';
|
|
1538
|
+
handle.jsonlMessagesState.activityRevision += 1;
|
|
1539
|
+
refreshCodexJsonlMessageStatuses(handle.jsonlMessagesState);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
const nextAllMessages = materializeCodexJsonlMessages(handle.jsonlMessagesState);
|
|
1543
|
+
const nextMessages = this.selectRecentMessages(nextAllMessages);
|
|
1544
|
+
const nextHasOlderMessages = nextAllMessages.length > nextMessages.length;
|
|
1545
|
+
const allMessagesChanged = !messagesEqual(handle.runtime.allMessages, nextAllMessages);
|
|
1546
|
+
const messagesChanged = !messagesEqual(handle.runtime.messages, nextMessages);
|
|
1547
|
+
const hasOlderMessagesChanged = handle.runtime.hasOlderMessages !== nextHasOlderMessages;
|
|
1548
|
+
|
|
1549
|
+
if (allMessagesChanged) {
|
|
1550
|
+
handle.runtime.allMessages = nextAllMessages;
|
|
1551
|
+
}
|
|
1552
|
+
if (messagesChanged) {
|
|
1553
|
+
handle.runtime.messages = nextMessages;
|
|
1554
|
+
}
|
|
1555
|
+
if (allMessagesChanged || messagesChanged || hasOlderMessagesChanged) {
|
|
1556
|
+
handle.runtime.hasOlderMessages = nextHasOlderMessages;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
this.setStatus(handle, 'idle', true);
|
|
1560
|
+
|
|
1561
|
+
if (this.isActiveHandle(handle) && (allMessagesChanged || messagesChanged || hasOlderMessagesChanged)) {
|
|
1562
|
+
const upsertPayload = this.createMessagesUpsertPayload(
|
|
1563
|
+
handle,
|
|
1564
|
+
previousMessages,
|
|
1565
|
+
nextMessages,
|
|
1566
|
+
previousHasOlderMessages,
|
|
1567
|
+
nextHasOlderMessages
|
|
1568
|
+
);
|
|
1569
|
+
if (upsertPayload) {
|
|
1570
|
+
this.callbacks.emitMessagesUpsert(upsertPayload);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
} catch (error) {
|
|
1574
|
+
this.log('warn', 'failed to confirm codex ready state after interrupt', {
|
|
1575
|
+
...this.handleContext(handle),
|
|
1576
|
+
error: errorMessage(error, 'Failed to confirm ready state')
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1546
1581
|
private getLastActivityAt(handle: CodexHandle): number {
|
|
1547
1582
|
return Math.max(
|
|
1548
1583
|
handle.lastJsonlActivityAt ?? 0,
|