dev-sessions 0.2.2 → 0.2.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/README.md +124 -77
- package/dist/backends/backend.d.ts +42 -0
- package/dist/backends/backend.js +3 -0
- package/dist/backends/backend.js.map +1 -0
- package/dist/backends/claude-backend.d.ts +22 -0
- package/dist/backends/claude-backend.js +161 -0
- package/dist/backends/claude-backend.js.map +1 -0
- package/dist/backends/claude-tmux.d.ts +0 -2
- package/dist/backends/claude-tmux.js +8 -21
- package/dist/backends/claude-tmux.js.map +1 -1
- package/dist/backends/codex-appserver.d.ts +44 -2
- package/dist/backends/codex-appserver.js +291 -109
- package/dist/backends/codex-appserver.js.map +1 -1
- package/dist/backends/codex-backend.d.ts +21 -0
- package/dist/backends/codex-backend.js +224 -0
- package/dist/backends/codex-backend.js.map +1 -0
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +31 -3
- package/dist/cli.js.map +1 -1
- package/dist/gateway/client.d.ts +3 -1
- package/dist/gateway/client.js +24 -2
- package/dist/gateway/client.js.map +1 -1
- package/dist/gateway/server.js +45 -2
- package/dist/gateway/server.js.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/session-manager.d.ts +7 -9
- package/dist/session-manager.js +115 -338
- package/dist/session-manager.js.map +1 -1
- package/dist/session-store.js +16 -4
- package/dist/session-store.js.map +1 -1
- package/dist/transcript/claude-parser.d.ts +1 -0
- package/dist/transcript/claude-parser.js +4 -0
- package/dist/transcript/claude-parser.js.map +1 -1
- package/dist/types.d.ts +6 -1
- package/package.json +1 -1
- package/skills/delegate/SKILL.md +77 -0
- package/skills/dev-sessions/SKILL.md +5 -13
|
@@ -17,6 +17,7 @@ export interface CodexTurnWaitResult {
|
|
|
17
17
|
elapsedMs: number;
|
|
18
18
|
status: TurnCompletionStatus;
|
|
19
19
|
errorMessage?: string;
|
|
20
|
+
assistantText?: string;
|
|
20
21
|
}
|
|
21
22
|
export interface CodexSendMessageOptions {
|
|
22
23
|
workspacePath: string;
|
|
@@ -26,6 +27,8 @@ export interface CodexSendResult {
|
|
|
26
27
|
threadId: string;
|
|
27
28
|
appServerPid: number;
|
|
28
29
|
appServerPort: number;
|
|
30
|
+
turnId?: string;
|
|
31
|
+
assistantText?: string;
|
|
29
32
|
}
|
|
30
33
|
export interface CodexRpcClient {
|
|
31
34
|
readonly currentTurnText: string;
|
|
@@ -33,7 +36,7 @@ export interface CodexRpcClient {
|
|
|
33
36
|
readonly lastTurnError?: string;
|
|
34
37
|
connectAndInitialize(): Promise<void>;
|
|
35
38
|
request(method: string, params?: unknown): Promise<unknown>;
|
|
36
|
-
waitForTurnCompletion(timeoutMs: number): Promise<CodexTurnWaitResult>;
|
|
39
|
+
waitForTurnCompletion(timeoutMs: number, expectedThreadId?: string, expectedTurnId?: string): Promise<CodexTurnWaitResult>;
|
|
37
40
|
close(): Promise<void>;
|
|
38
41
|
}
|
|
39
42
|
export interface CodexAppServerDaemonManager {
|
|
@@ -47,6 +50,40 @@ export interface CodexAppServerBackendDependencies {
|
|
|
47
50
|
clientFactory?: (url: string) => CodexRpcClient;
|
|
48
51
|
daemonManager?: CodexAppServerDaemonManager;
|
|
49
52
|
}
|
|
53
|
+
export declare class CodexWebSocketRpcClient implements CodexRpcClient {
|
|
54
|
+
private readonly url;
|
|
55
|
+
private ws?;
|
|
56
|
+
private connectPromise?;
|
|
57
|
+
private nextRequestId;
|
|
58
|
+
private readonly pendingRequests;
|
|
59
|
+
private waiters;
|
|
60
|
+
private currentText;
|
|
61
|
+
private turnStatus?;
|
|
62
|
+
private turnError?;
|
|
63
|
+
private turnThreadId?;
|
|
64
|
+
private turnId?;
|
|
65
|
+
private closed;
|
|
66
|
+
private closing;
|
|
67
|
+
constructor(url: string);
|
|
68
|
+
get currentTurnText(): string;
|
|
69
|
+
get lastTurnStatus(): TurnCompletionStatus | undefined;
|
|
70
|
+
get lastTurnError(): string | undefined;
|
|
71
|
+
connectAndInitialize(): Promise<void>;
|
|
72
|
+
request(method: string, params?: unknown): Promise<unknown>;
|
|
73
|
+
waitForTurnCompletion(timeoutMs: number, expectedThreadId?: string, expectedTurnId?: string): Promise<CodexTurnWaitResult>;
|
|
74
|
+
close(): Promise<void>;
|
|
75
|
+
private connect;
|
|
76
|
+
private attachSocketHandlers;
|
|
77
|
+
private handleMessageFrame;
|
|
78
|
+
private handleMaybeJsonLine;
|
|
79
|
+
private handleRpcMessage;
|
|
80
|
+
private handleRpcResponse;
|
|
81
|
+
private handleNotification;
|
|
82
|
+
private hasMatchingWaiterForTurn;
|
|
83
|
+
private resolveWaiters;
|
|
84
|
+
private failConnection;
|
|
85
|
+
private notify;
|
|
86
|
+
}
|
|
50
87
|
export declare class CodexAppServerBackend {
|
|
51
88
|
private readonly sessionState;
|
|
52
89
|
private readonly daemonManager;
|
|
@@ -55,9 +92,13 @@ export declare class CodexAppServerBackend {
|
|
|
55
92
|
createSession(championId: string, workspacePath: string, model?: string): Promise<CodexSessionCreateResult>;
|
|
56
93
|
sendMessage(championId: string, threadId: string, message: string, options?: CodexSendMessageOptions): Promise<CodexSendResult>;
|
|
57
94
|
getThreadRuntimeStatus(threadId: string): Promise<'active' | 'idle' | 'notLoaded' | 'systemError' | 'unknown'>;
|
|
58
|
-
waitForThread(championId: string, threadId: string, timeoutMs?: number): Promise<CodexTurnWaitResult>;
|
|
95
|
+
waitForThread(championId: string, threadId: string, timeoutMs?: number, expectedTurnId?: string): Promise<CodexTurnWaitResult>;
|
|
59
96
|
waitForTurn(championId: string, timeoutMs?: number): Promise<CodexTurnWaitResult>;
|
|
60
97
|
getLastAssistantMessages(championId: string, threadId: string, count: number): Promise<string[]>;
|
|
98
|
+
getThreadTurns(threadId: string): Promise<Array<{
|
|
99
|
+
role: 'human' | 'assistant';
|
|
100
|
+
text: string;
|
|
101
|
+
}>>;
|
|
61
102
|
getSessionStatus(championId: string): AgentTurnStatus;
|
|
62
103
|
killSession(championId: string, pid?: number, threadId?: string, port?: number): Promise<void>;
|
|
63
104
|
stopAppServer(): Promise<void>;
|
|
@@ -69,5 +110,6 @@ export declare class CodexAppServerBackend {
|
|
|
69
110
|
private shouldResetDaemonAfterConnectionFailure;
|
|
70
111
|
private isResumeNotFoundError;
|
|
71
112
|
private isThreadReadUnmaterializedError;
|
|
113
|
+
private isThreadReadNotFoundError;
|
|
72
114
|
}
|
|
73
115
|
export {};
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.CodexAppServerBackend = void 0;
|
|
6
|
+
exports.CodexAppServerBackend = exports.CodexWebSocketRpcClient = void 0;
|
|
7
7
|
const node_child_process_1 = require("node:child_process");
|
|
8
8
|
const promises_1 = require("node:fs/promises");
|
|
9
9
|
const node_net_1 = require("node:net");
|
|
@@ -19,6 +19,7 @@ const CLOSE_TIMEOUT_MS = 500;
|
|
|
19
19
|
const STATE_FILE_VERSION = 1;
|
|
20
20
|
const RESUME_NOT_FOUND_PATTERN = /no rollout found|thread not found/i;
|
|
21
21
|
const THREAD_READ_UNMATERIALIZED_PATTERN = /includeTurns is unavailable before first user message/i;
|
|
22
|
+
const THREAD_READ_NOT_FOUND_PATTERN = /thread not loaded|thread not found|no rollout found|unknown thread/i;
|
|
22
23
|
const APP_SERVER_URL_PATTERN = /ws:\/\/127\.0\.0\.1:(\d+)/i;
|
|
23
24
|
function defaultSpawnCodexDaemon(args, options) {
|
|
24
25
|
return (0, node_child_process_1.spawn)('codex', args, options);
|
|
@@ -231,12 +232,13 @@ class CodexWebSocketRpcClient {
|
|
|
231
232
|
ws;
|
|
232
233
|
connectPromise;
|
|
233
234
|
nextRequestId = 1;
|
|
234
|
-
stdoutBuffer = '';
|
|
235
235
|
pendingRequests = new Map();
|
|
236
236
|
waiters = [];
|
|
237
237
|
currentText = '';
|
|
238
238
|
turnStatus;
|
|
239
239
|
turnError;
|
|
240
|
+
turnThreadId;
|
|
241
|
+
turnId;
|
|
240
242
|
closed = false;
|
|
241
243
|
closing = false;
|
|
242
244
|
constructor(url) {
|
|
@@ -303,8 +305,11 @@ class CodexWebSocketRpcClient {
|
|
|
303
305
|
}
|
|
304
306
|
});
|
|
305
307
|
}
|
|
306
|
-
async waitForTurnCompletion(timeoutMs) {
|
|
307
|
-
|
|
308
|
+
async waitForTurnCompletion(timeoutMs, expectedThreadId, expectedTurnId) {
|
|
309
|
+
const cachedMatchesThread = !expectedThreadId || (typeof this.turnThreadId === 'string' && this.turnThreadId === expectedThreadId);
|
|
310
|
+
const cachedMatchesTurn = !expectedTurnId || (typeof this.turnId === 'string' && this.turnId === expectedTurnId);
|
|
311
|
+
const cachedMatchesExpected = cachedMatchesThread && cachedMatchesTurn;
|
|
312
|
+
if (this.turnStatus && cachedMatchesExpected) {
|
|
308
313
|
return {
|
|
309
314
|
completed: true,
|
|
310
315
|
timedOut: false,
|
|
@@ -331,6 +336,8 @@ class CodexWebSocketRpcClient {
|
|
|
331
336
|
this.waiters.push({
|
|
332
337
|
startTime,
|
|
333
338
|
timeoutHandle,
|
|
339
|
+
expectedThreadId,
|
|
340
|
+
expectedTurnId,
|
|
334
341
|
resolve
|
|
335
342
|
});
|
|
336
343
|
});
|
|
@@ -424,21 +431,7 @@ class CodexWebSocketRpcClient {
|
|
|
424
431
|
});
|
|
425
432
|
}
|
|
426
433
|
handleMessageFrame(frame) {
|
|
427
|
-
|
|
428
|
-
this.handleMaybeJsonLine(frame);
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
this.stdoutBuffer += frame;
|
|
432
|
-
const lines = this.stdoutBuffer.split('\n');
|
|
433
|
-
this.stdoutBuffer = lines.pop() ?? '';
|
|
434
|
-
for (const line of lines) {
|
|
435
|
-
this.handleMaybeJsonLine(line);
|
|
436
|
-
}
|
|
437
|
-
if (this.stdoutBuffer.trim().length > 0) {
|
|
438
|
-
const pendingBuffer = this.stdoutBuffer;
|
|
439
|
-
this.stdoutBuffer = '';
|
|
440
|
-
this.handleMaybeJsonLine(pendingBuffer);
|
|
441
|
-
}
|
|
434
|
+
this.handleMaybeJsonLine(frame);
|
|
442
435
|
}
|
|
443
436
|
handleMaybeJsonLine(rawLine) {
|
|
444
437
|
const trimmed = rawLine.trim();
|
|
@@ -450,10 +443,6 @@ class CodexWebSocketRpcClient {
|
|
|
450
443
|
payload = JSON.parse(trimmed);
|
|
451
444
|
}
|
|
452
445
|
catch {
|
|
453
|
-
// If parsing fails, keep buffering behavior for newline-delimited mode.
|
|
454
|
-
if (!rawLine.includes('\n')) {
|
|
455
|
-
this.stdoutBuffer = rawLine;
|
|
456
|
-
}
|
|
457
446
|
return;
|
|
458
447
|
}
|
|
459
448
|
this.handleRpcMessage(payload);
|
|
@@ -492,6 +481,15 @@ class CodexWebSocketRpcClient {
|
|
|
492
481
|
}
|
|
493
482
|
return;
|
|
494
483
|
}
|
|
484
|
+
if (notification.method === 'turn/started') {
|
|
485
|
+
const turn = notification.params?.turn;
|
|
486
|
+
this.turnStatus = undefined;
|
|
487
|
+
this.turnError = undefined;
|
|
488
|
+
this.turnThreadId = extractNotificationThreadId(notification.params);
|
|
489
|
+
this.turnId = extractTurnId(turn);
|
|
490
|
+
this.currentText = '';
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
495
493
|
if (notification.method !== 'turn/completed') {
|
|
496
494
|
return;
|
|
497
495
|
}
|
|
@@ -500,20 +498,42 @@ class CodexWebSocketRpcClient {
|
|
|
500
498
|
if (!status) {
|
|
501
499
|
return;
|
|
502
500
|
}
|
|
501
|
+
const notificationThreadId = extractNotificationThreadId(notification.params);
|
|
502
|
+
const notificationTurnId = extractTurnId(turn);
|
|
503
|
+
if (this.waiters.length > 0 && !this.hasMatchingWaiterForTurn(notificationThreadId, notificationTurnId)) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
503
506
|
this.turnStatus = status;
|
|
504
507
|
this.turnError = extractTurnError(turn);
|
|
508
|
+
this.turnThreadId = notificationThreadId;
|
|
509
|
+
this.turnId = notificationTurnId;
|
|
505
510
|
this.resolveWaiters({
|
|
506
511
|
completed: true,
|
|
507
512
|
timedOut: false,
|
|
508
513
|
elapsedMs: 0,
|
|
509
514
|
status,
|
|
510
|
-
errorMessage: this.turnError
|
|
515
|
+
errorMessage: this.turnError,
|
|
516
|
+
assistantText: this.currentText.length > 0 ? this.currentText : undefined
|
|
517
|
+
}, notificationThreadId, notificationTurnId);
|
|
518
|
+
}
|
|
519
|
+
hasMatchingWaiterForTurn(threadId, turnId) {
|
|
520
|
+
return this.waiters.some((waiter) => {
|
|
521
|
+
const threadMatches = !waiter.expectedThreadId || threadId === waiter.expectedThreadId;
|
|
522
|
+
const turnMatches = !waiter.expectedTurnId || turnId === waiter.expectedTurnId;
|
|
523
|
+
return threadMatches && turnMatches;
|
|
511
524
|
});
|
|
512
525
|
}
|
|
513
|
-
resolveWaiters(baseResult) {
|
|
514
|
-
const waiters = this.waiters
|
|
526
|
+
resolveWaiters(baseResult, completedThreadId, completedTurnId, forceAll = false) {
|
|
527
|
+
const waiters = this.waiters;
|
|
528
|
+
this.waiters = [];
|
|
515
529
|
const now = Date.now();
|
|
516
530
|
for (const waiter of waiters) {
|
|
531
|
+
const threadMatches = !waiter.expectedThreadId || completedThreadId === waiter.expectedThreadId;
|
|
532
|
+
const turnMatches = !waiter.expectedTurnId || completedTurnId === waiter.expectedTurnId;
|
|
533
|
+
if (!forceAll && !(threadMatches && turnMatches)) {
|
|
534
|
+
this.waiters.push(waiter);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
517
537
|
clearTimeout(waiter.timeoutHandle);
|
|
518
538
|
waiter.resolve({
|
|
519
539
|
...baseResult,
|
|
@@ -540,7 +560,7 @@ class CodexWebSocketRpcClient {
|
|
|
540
560
|
elapsedMs: 0,
|
|
541
561
|
status: this.turnStatus,
|
|
542
562
|
errorMessage: this.turnError
|
|
543
|
-
});
|
|
563
|
+
}, undefined, undefined, true);
|
|
544
564
|
}
|
|
545
565
|
notify(method, params) {
|
|
546
566
|
const ws = this.ws;
|
|
@@ -550,6 +570,7 @@ class CodexWebSocketRpcClient {
|
|
|
550
570
|
ws.send(`${JSON.stringify({ jsonrpc: '2.0', method, params })}\n`);
|
|
551
571
|
}
|
|
552
572
|
}
|
|
573
|
+
exports.CodexWebSocketRpcClient = CodexWebSocketRpcClient;
|
|
553
574
|
function extractThreadId(result) {
|
|
554
575
|
const threadId = result?.thread?.id;
|
|
555
576
|
if (typeof threadId !== 'string' || threadId.trim().length === 0) {
|
|
@@ -557,35 +578,25 @@ function extractThreadId(result) {
|
|
|
557
578
|
}
|
|
558
579
|
return threadId;
|
|
559
580
|
}
|
|
560
|
-
// ThreadStatus
|
|
561
|
-
//
|
|
562
|
-
//
|
|
581
|
+
// Codex 0.104.0 shipped before ThreadStatus was added to the protocol and omits
|
|
582
|
+
// thread.status entirely for idle threads, so missing status must be treated as
|
|
583
|
+
// idle. Starting with the post-0.104.0 protocol (expected in 0.105.0 stable),
|
|
584
|
+
// ThreadStatus is a tagged serde enum and will appear as objects such as
|
|
585
|
+
// {"type":"idle"} / {"type":"active","activeFlags":[...]}. When 0.105.0 is
|
|
586
|
+
// supported here, update this parser to handle both shapes.
|
|
563
587
|
function extractThreadRuntimeStatus(result) {
|
|
564
588
|
const thread = result?.thread;
|
|
565
589
|
if (!thread || !('status' in thread)) {
|
|
566
|
-
// Old Codex version without Thread.status field — assume idle
|
|
567
590
|
return 'idle';
|
|
568
591
|
}
|
|
569
592
|
const status = thread.status;
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
return type;
|
|
573
|
-
}
|
|
574
|
-
return 'unknown';
|
|
575
|
-
}
|
|
576
|
-
function extractThreadListPage(result) {
|
|
577
|
-
if (!result || typeof result !== 'object') {
|
|
578
|
-
throw new Error('thread/list returned an invalid response');
|
|
593
|
+
if (status === 'idle' || status === 'notLoaded' || status === 'systemError') {
|
|
594
|
+
return status;
|
|
579
595
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
throw new Error('thread/list response is missing result.data');
|
|
596
|
+
if (status !== null && typeof status === 'object' && 'active' in status) {
|
|
597
|
+
return 'active';
|
|
583
598
|
}
|
|
584
|
-
|
|
585
|
-
.map((item) => (item && typeof item === 'object' ? item.id : undefined))
|
|
586
|
-
.filter((id) => typeof id === 'string' && id.trim().length > 0);
|
|
587
|
-
const nextCursor = typeof record.nextCursor === 'string' && record.nextCursor.length > 0 ? record.nextCursor : undefined;
|
|
588
|
-
return { threadIds, nextCursor };
|
|
599
|
+
return 'unknown';
|
|
589
600
|
}
|
|
590
601
|
function extractThreadReadAssistantMessages(result) {
|
|
591
602
|
if (!result || typeof result !== 'object') {
|
|
@@ -617,32 +628,92 @@ function extractThreadReadAssistantMessages(result) {
|
|
|
617
628
|
}
|
|
618
629
|
return messages;
|
|
619
630
|
}
|
|
631
|
+
function extractUserMessageText(item) {
|
|
632
|
+
if (Array.isArray(item.content)) {
|
|
633
|
+
const parts = [];
|
|
634
|
+
for (const part of item.content) {
|
|
635
|
+
if (typeof part === 'string') {
|
|
636
|
+
parts.push(part);
|
|
637
|
+
}
|
|
638
|
+
else if (part && typeof part === 'object') {
|
|
639
|
+
const p = part;
|
|
640
|
+
if (typeof p.text === 'string' && p.text.length > 0) {
|
|
641
|
+
parts.push(p.text);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (parts.length > 0) {
|
|
646
|
+
return parts.join('');
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (typeof item.text === 'string') {
|
|
650
|
+
return item.text;
|
|
651
|
+
}
|
|
652
|
+
return '';
|
|
653
|
+
}
|
|
654
|
+
function extractThreadTurns(result) {
|
|
655
|
+
if (!result || typeof result !== 'object') {
|
|
656
|
+
throw new Error('thread/read returned an invalid response');
|
|
657
|
+
}
|
|
658
|
+
const thread = result.thread;
|
|
659
|
+
if (!thread || typeof thread !== 'object') {
|
|
660
|
+
throw new Error('thread/read response is missing result.thread');
|
|
661
|
+
}
|
|
662
|
+
const turns = thread.turns;
|
|
663
|
+
if (!Array.isArray(turns)) {
|
|
664
|
+
throw new Error('thread/read response is missing result.thread.turns');
|
|
665
|
+
}
|
|
666
|
+
const sessionTurns = [];
|
|
667
|
+
for (const turn of turns) {
|
|
668
|
+
if (!turn || typeof turn !== 'object')
|
|
669
|
+
continue;
|
|
670
|
+
const items = turn.items;
|
|
671
|
+
if (!Array.isArray(items))
|
|
672
|
+
continue;
|
|
673
|
+
for (const item of items) {
|
|
674
|
+
if (!item || typeof item !== 'object')
|
|
675
|
+
continue;
|
|
676
|
+
const rec = item;
|
|
677
|
+
if (rec.type === 'userMessage') {
|
|
678
|
+
const text = extractUserMessageText(rec);
|
|
679
|
+
if (text.length > 0) {
|
|
680
|
+
sessionTurns.push({ role: 'human', text });
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
else if (rec.type === 'agentMessage' && typeof rec.text === 'string' && rec.text.length > 0) {
|
|
684
|
+
sessionTurns.push({ role: 'assistant', text: rec.text });
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return sessionTurns;
|
|
689
|
+
}
|
|
620
690
|
function extractDeltaText(params) {
|
|
621
691
|
if (!params || typeof params !== 'object') {
|
|
622
692
|
return '';
|
|
623
693
|
}
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
if (typeof delta === 'string') {
|
|
631
|
-
return delta;
|
|
694
|
+
const delta = params.delta;
|
|
695
|
+
return typeof delta === 'string' ? delta : '';
|
|
696
|
+
}
|
|
697
|
+
function extractNotificationThreadId(params) {
|
|
698
|
+
if (!params || typeof params !== 'object') {
|
|
699
|
+
return undefined;
|
|
632
700
|
}
|
|
633
|
-
|
|
634
|
-
|
|
701
|
+
const rec = params;
|
|
702
|
+
const threadId = rec.threadId ?? rec.thread_id;
|
|
703
|
+
return typeof threadId === 'string' && threadId.length > 0 ? threadId : undefined;
|
|
704
|
+
}
|
|
705
|
+
function extractTurnId(turn) {
|
|
706
|
+
if (!turn || typeof turn !== 'object') {
|
|
707
|
+
return undefined;
|
|
635
708
|
}
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
return nestedDelta.text;
|
|
643
|
-
}
|
|
709
|
+
const turnId = turn.id;
|
|
710
|
+
return typeof turnId === 'string' && turnId.length > 0 ? turnId : undefined;
|
|
711
|
+
}
|
|
712
|
+
function extractStartedTurnId(result) {
|
|
713
|
+
if (!result || typeof result !== 'object') {
|
|
714
|
+
return undefined;
|
|
644
715
|
}
|
|
645
|
-
return
|
|
716
|
+
return extractTurnId(result.turn);
|
|
646
717
|
}
|
|
647
718
|
function extractTurnStatus(turn) {
|
|
648
719
|
if (!turn || typeof turn !== 'object') {
|
|
@@ -735,16 +806,30 @@ class CodexAppServerBackend {
|
|
|
735
806
|
});
|
|
736
807
|
tid = extractThreadId(threadResult);
|
|
737
808
|
}
|
|
738
|
-
await client.request('turn/start', {
|
|
809
|
+
const turnStartResult = await client.request('turn/start', {
|
|
739
810
|
threadId: tid,
|
|
740
811
|
input: [{ type: 'text', text: message }]
|
|
741
812
|
});
|
|
742
|
-
|
|
813
|
+
const startedTurnId = extractStartedTurnId(turnStartResult);
|
|
814
|
+
// Best-effort: wait a short time for the turn to complete on this connection.
|
|
815
|
+
// Captures fast responses (e.g. short answers) without blocking for long tasks.
|
|
816
|
+
// Only use the result if the exact initiated turn actually completed — not
|
|
817
|
+
// if it timed out or a different turn completed on the same thread.
|
|
818
|
+
const FAST_CAPTURE_TIMEOUT_MS = 3_000;
|
|
819
|
+
const earlyResult = await client.waitForTurnCompletion(FAST_CAPTURE_TIMEOUT_MS, tid, startedTurnId);
|
|
820
|
+
const completedEarly = !earlyResult.timedOut && earlyResult.status === 'completed';
|
|
821
|
+
return { tid, turnId: startedTurnId, assistantText: completedEarly ? earlyResult.assistantText : undefined };
|
|
743
822
|
});
|
|
823
|
+
const state = this.ensureSessionState(championId);
|
|
824
|
+
if (activeThreadId.assistantText) {
|
|
825
|
+
state.assistantHistory.push(activeThreadId.assistantText);
|
|
826
|
+
}
|
|
744
827
|
return {
|
|
745
|
-
threadId: activeThreadId,
|
|
828
|
+
threadId: activeThreadId.tid,
|
|
746
829
|
appServerPid: server.pid,
|
|
747
|
-
appServerPort: server.port
|
|
830
|
+
appServerPort: server.port,
|
|
831
|
+
turnId: activeThreadId.turnId,
|
|
832
|
+
assistantText: activeThreadId.assistantText
|
|
748
833
|
};
|
|
749
834
|
}
|
|
750
835
|
async getThreadRuntimeStatus(threadId) {
|
|
@@ -758,34 +843,110 @@ class CodexAppServerBackend {
|
|
|
758
843
|
});
|
|
759
844
|
return result;
|
|
760
845
|
}
|
|
761
|
-
async waitForThread(championId, threadId, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
846
|
+
async waitForThread(championId, threadId, timeoutMs = DEFAULT_TIMEOUT_MS, expectedTurnId) {
|
|
762
847
|
const normalizedThreadId = threadId.trim();
|
|
763
848
|
if (normalizedThreadId.length === 0) {
|
|
764
849
|
return { completed: true, timedOut: false, elapsedMs: 0, status: 'completed' };
|
|
765
850
|
}
|
|
851
|
+
const normalizedExpectedTurnId = expectedTurnId?.trim() ?? '';
|
|
766
852
|
const state = this.ensureSessionState(championId);
|
|
767
853
|
const safeTimeoutMs = Math.max(1, timeoutMs);
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
854
|
+
const overallStart = Date.now();
|
|
855
|
+
let lastAssistantText;
|
|
856
|
+
let sawActiveTurn = false;
|
|
857
|
+
let result;
|
|
858
|
+
if (normalizedExpectedTurnId.length > 0) {
|
|
859
|
+
// Codex 0.104.0 can report thread/resume=idle and thread/read turn.status=completed
|
|
860
|
+
// while the initiated turn is still running tools. When we know the exact turn ID
|
|
861
|
+
// from turn/start, block on that turn's completion notification instead of trusting
|
|
862
|
+
// thread runtime status.
|
|
863
|
+
const waitCycle = await this.withConnectedClient(async (client) => {
|
|
864
|
+
// Resume first so this connection is subscribed to notifications for the thread.
|
|
865
|
+
await client.request('thread/resume', { threadId: normalizedThreadId });
|
|
866
|
+
return client.waitForTurnCompletion(safeTimeoutMs, normalizedThreadId, normalizedExpectedTurnId);
|
|
867
|
+
});
|
|
868
|
+
if (waitCycle.result.assistantText) {
|
|
869
|
+
lastAssistantText = waitCycle.result.assistantText;
|
|
773
870
|
}
|
|
774
|
-
|
|
871
|
+
result = {
|
|
872
|
+
...waitCycle.result,
|
|
873
|
+
elapsedMs: waitCycle.result.timedOut
|
|
874
|
+
? Math.max(waitCycle.result.elapsedMs, Date.now() - overallStart)
|
|
875
|
+
: waitCycle.result.elapsedMs,
|
|
876
|
+
assistantText: waitCycle.result.assistantText ?? lastAssistantText
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
while (!result) {
|
|
880
|
+
const elapsedMs = Date.now() - overallStart;
|
|
881
|
+
const remainingMs = safeTimeoutMs - elapsedMs;
|
|
882
|
+
if (remainingMs <= 0) {
|
|
883
|
+
result = {
|
|
884
|
+
completed: false,
|
|
885
|
+
timedOut: true,
|
|
886
|
+
elapsedMs: Math.max(1, elapsedMs),
|
|
887
|
+
status: 'interrupted',
|
|
888
|
+
errorMessage: 'Timed out waiting for Codex turn completion',
|
|
889
|
+
assistantText: lastAssistantText
|
|
890
|
+
};
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
const cycle = await this.withConnectedClient(async (client) => {
|
|
894
|
+
const resumeResult = await client.request('thread/resume', { threadId: normalizedThreadId });
|
|
895
|
+
const runtimeStatus = extractThreadRuntimeStatus(resumeResult);
|
|
896
|
+
if (runtimeStatus === 'active') {
|
|
897
|
+
const waitResult = await client.waitForTurnCompletion(remainingMs, normalizedThreadId);
|
|
898
|
+
return { kind: 'wait', waitResult };
|
|
899
|
+
}
|
|
900
|
+
if (runtimeStatus === 'systemError' || runtimeStatus === 'unknown') {
|
|
901
|
+
return {
|
|
902
|
+
kind: 'terminal',
|
|
903
|
+
waitResult: {
|
|
904
|
+
completed: true,
|
|
905
|
+
timedOut: false,
|
|
906
|
+
elapsedMs: 0,
|
|
907
|
+
status: 'failed',
|
|
908
|
+
errorMessage: runtimeStatus === 'systemError'
|
|
909
|
+
? 'Codex thread is in systemError state'
|
|
910
|
+
: 'Unable to determine Codex thread runtime status'
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
}
|
|
775
914
|
return {
|
|
776
|
-
|
|
777
|
-
timedOut: false,
|
|
778
|
-
elapsedMs: 0,
|
|
779
|
-
status: 'failed',
|
|
780
|
-
errorMessage: runtimeStatus === 'systemError'
|
|
781
|
-
? 'Codex thread is in systemError state'
|
|
782
|
-
: 'Unable to determine Codex thread runtime status'
|
|
915
|
+
kind: 'idle',
|
|
916
|
+
waitResult: { completed: true, timedOut: false, elapsedMs: 0, status: 'completed' }
|
|
783
917
|
};
|
|
918
|
+
});
|
|
919
|
+
if (cycle.result.kind === 'wait') {
|
|
920
|
+
sawActiveTurn = true;
|
|
921
|
+
if (cycle.result.waitResult.assistantText) {
|
|
922
|
+
lastAssistantText = cycle.result.waitResult.assistantText;
|
|
923
|
+
}
|
|
924
|
+
if (cycle.result.waitResult.timedOut ||
|
|
925
|
+
cycle.result.waitResult.status === 'failed' ||
|
|
926
|
+
cycle.result.waitResult.status === 'interrupted') {
|
|
927
|
+
result = {
|
|
928
|
+
...cycle.result.waitResult,
|
|
929
|
+
elapsedMs: Math.max(cycle.result.waitResult.elapsedMs, Date.now() - overallStart),
|
|
930
|
+
assistantText: cycle.result.waitResult.assistantText ?? lastAssistantText
|
|
931
|
+
};
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
// One turn completed, but the logical task may continue in a subsequent turn.
|
|
935
|
+
// Reconnect and re-check thread runtime state until the thread is quiescent.
|
|
936
|
+
continue;
|
|
784
937
|
}
|
|
785
|
-
|
|
786
|
-
|
|
938
|
+
const totalElapsedMs = Date.now() - overallStart;
|
|
939
|
+
result = {
|
|
940
|
+
...cycle.result.waitResult,
|
|
941
|
+
elapsedMs: sawActiveTurn ? Math.max(1, totalElapsedMs) : 0,
|
|
942
|
+
assistantText: lastAssistantText
|
|
943
|
+
};
|
|
944
|
+
}
|
|
787
945
|
state.lastTurnStatus = result.status;
|
|
788
946
|
state.lastTurnError = result.errorMessage;
|
|
947
|
+
if (result.assistantText) {
|
|
948
|
+
state.assistantHistory.push(result.assistantText);
|
|
949
|
+
}
|
|
789
950
|
return result;
|
|
790
951
|
}
|
|
791
952
|
async waitForTurn(championId, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
@@ -840,6 +1001,28 @@ class CodexAppServerBackend {
|
|
|
840
1001
|
throw error;
|
|
841
1002
|
}
|
|
842
1003
|
}
|
|
1004
|
+
async getThreadTurns(threadId) {
|
|
1005
|
+
const normalizedThreadId = threadId.trim();
|
|
1006
|
+
if (normalizedThreadId.length === 0) {
|
|
1007
|
+
return [];
|
|
1008
|
+
}
|
|
1009
|
+
try {
|
|
1010
|
+
const { result } = await this.withConnectedClient(async (client) => {
|
|
1011
|
+
const readResult = await client.request('thread/read', {
|
|
1012
|
+
threadId: normalizedThreadId,
|
|
1013
|
+
includeTurns: true
|
|
1014
|
+
});
|
|
1015
|
+
return extractThreadTurns(readResult);
|
|
1016
|
+
});
|
|
1017
|
+
return result;
|
|
1018
|
+
}
|
|
1019
|
+
catch (error) {
|
|
1020
|
+
if (this.isThreadReadUnmaterializedError(error)) {
|
|
1021
|
+
return [];
|
|
1022
|
+
}
|
|
1023
|
+
throw error;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
843
1026
|
getSessionStatus(championId) {
|
|
844
1027
|
const state = this.ensureSessionState(championId);
|
|
845
1028
|
if (state.lastTurnStatus === 'failed') {
|
|
@@ -888,33 +1071,26 @@ class CodexAppServerBackend {
|
|
|
888
1071
|
if (!threadId || threadId.trim().length === 0) {
|
|
889
1072
|
return this.daemonManager.isServerRunning(pid, port);
|
|
890
1073
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
catch {
|
|
898
|
-
return false;
|
|
899
|
-
}
|
|
1074
|
+
// Let transport errors propagate — callers that need tri-state liveness
|
|
1075
|
+
// should catch and treat as 'unknown' rather than 'dead'.
|
|
1076
|
+
const { result } = await this.withConnectedClient(async (client) => {
|
|
1077
|
+
return this.threadExistsOnServer(client, threadId.trim());
|
|
1078
|
+
});
|
|
1079
|
+
return result;
|
|
900
1080
|
}
|
|
901
1081
|
async threadExistsOnServer(client, threadId) {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
cursor,
|
|
907
|
-
limit: pageLimit,
|
|
908
|
-
modelProviders: []
|
|
1082
|
+
try {
|
|
1083
|
+
await client.request('thread/read', {
|
|
1084
|
+
threadId,
|
|
1085
|
+
includeTurns: false
|
|
909
1086
|
});
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
if (!page.nextCursor) {
|
|
1087
|
+
return true;
|
|
1088
|
+
}
|
|
1089
|
+
catch (error) {
|
|
1090
|
+
if (this.isThreadReadNotFoundError(error)) {
|
|
915
1091
|
return false;
|
|
916
1092
|
}
|
|
917
|
-
|
|
1093
|
+
throw error;
|
|
918
1094
|
}
|
|
919
1095
|
}
|
|
920
1096
|
ensureSessionState(championId) {
|
|
@@ -979,6 +1155,12 @@ class CodexAppServerBackend {
|
|
|
979
1155
|
}
|
|
980
1156
|
return THREAD_READ_UNMATERIALIZED_PATTERN.test(error.message);
|
|
981
1157
|
}
|
|
1158
|
+
isThreadReadNotFoundError(error) {
|
|
1159
|
+
if (!(error instanceof Error)) {
|
|
1160
|
+
return false;
|
|
1161
|
+
}
|
|
1162
|
+
return THREAD_READ_NOT_FOUND_PATTERN.test(error.message);
|
|
1163
|
+
}
|
|
982
1164
|
}
|
|
983
1165
|
exports.CodexAppServerBackend = CodexAppServerBackend;
|
|
984
1166
|
//# sourceMappingURL=codex-appserver.js.map
|