dev-sessions 0.2.1 → 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 +1 -3
- package/dist/backends/claude-tmux.js +15 -24
- package/dist/backends/claude-tmux.js.map +1 -1
- package/dist/backends/codex-appserver.d.ts +46 -6
- package/dist/backends/codex-appserver.js +299 -130
- 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 +122 -367
- 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
|
@@ -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') {
|
|
@@ -701,15 +772,14 @@ class CodexAppServerBackend {
|
|
|
701
772
|
if (!options || options.workspacePath.trim().length === 0) {
|
|
702
773
|
throw new Error('Codex workspace path is required to send a message');
|
|
703
774
|
}
|
|
704
|
-
const timeoutMs = Math.max(1, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
705
775
|
const model = options.model ?? DEFAULT_MODEL;
|
|
706
|
-
|
|
707
|
-
const { server, result } = await this.withConnectedClient(async (client) => {
|
|
708
|
-
let
|
|
709
|
-
if (
|
|
776
|
+
this.ensureSessionState(championId);
|
|
777
|
+
const { server, result: activeThreadId } = await this.withConnectedClient(async (client) => {
|
|
778
|
+
let tid = threadId.trim();
|
|
779
|
+
if (tid.length > 0) {
|
|
710
780
|
try {
|
|
711
781
|
await client.request('thread/resume', {
|
|
712
|
-
threadId:
|
|
782
|
+
threadId: tid,
|
|
713
783
|
cwd: options.workspacePath,
|
|
714
784
|
model,
|
|
715
785
|
approvalPolicy: 'never',
|
|
@@ -721,10 +791,10 @@ class CodexAppServerBackend {
|
|
|
721
791
|
if (!this.isResumeNotFoundError(error)) {
|
|
722
792
|
throw error;
|
|
723
793
|
}
|
|
724
|
-
|
|
794
|
+
tid = '';
|
|
725
795
|
}
|
|
726
796
|
}
|
|
727
|
-
if (
|
|
797
|
+
if (tid.length === 0) {
|
|
728
798
|
const threadResult = await client.request('thread/start', {
|
|
729
799
|
model,
|
|
730
800
|
cwd: options.workspacePath,
|
|
@@ -734,30 +804,32 @@ class CodexAppServerBackend {
|
|
|
734
804
|
persistExtendedHistory: true,
|
|
735
805
|
experimentalRawEvents: false
|
|
736
806
|
});
|
|
737
|
-
|
|
807
|
+
tid = extractThreadId(threadResult);
|
|
738
808
|
}
|
|
739
|
-
await client.request('turn/start', {
|
|
740
|
-
threadId:
|
|
809
|
+
const turnStartResult = await client.request('turn/start', {
|
|
810
|
+
threadId: tid,
|
|
741
811
|
input: [{ type: 'text', text: message }]
|
|
742
812
|
});
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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 };
|
|
749
822
|
});
|
|
750
|
-
state
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
state.assistantHistory.push(result.assistantMessage);
|
|
823
|
+
const state = this.ensureSessionState(championId);
|
|
824
|
+
if (activeThreadId.assistantText) {
|
|
825
|
+
state.assistantHistory.push(activeThreadId.assistantText);
|
|
754
826
|
}
|
|
755
827
|
return {
|
|
756
|
-
|
|
757
|
-
threadId: result.threadId,
|
|
758
|
-
assistantMessage: result.assistantMessage,
|
|
828
|
+
threadId: activeThreadId.tid,
|
|
759
829
|
appServerPid: server.pid,
|
|
760
|
-
appServerPort: server.port
|
|
830
|
+
appServerPort: server.port,
|
|
831
|
+
turnId: activeThreadId.turnId,
|
|
832
|
+
assistantText: activeThreadId.assistantText
|
|
761
833
|
};
|
|
762
834
|
}
|
|
763
835
|
async getThreadRuntimeStatus(threadId) {
|
|
@@ -771,34 +843,110 @@ class CodexAppServerBackend {
|
|
|
771
843
|
});
|
|
772
844
|
return result;
|
|
773
845
|
}
|
|
774
|
-
async waitForThread(championId, threadId, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
846
|
+
async waitForThread(championId, threadId, timeoutMs = DEFAULT_TIMEOUT_MS, expectedTurnId) {
|
|
775
847
|
const normalizedThreadId = threadId.trim();
|
|
776
848
|
if (normalizedThreadId.length === 0) {
|
|
777
849
|
return { completed: true, timedOut: false, elapsedMs: 0, status: 'completed' };
|
|
778
850
|
}
|
|
851
|
+
const normalizedExpectedTurnId = expectedTurnId?.trim() ?? '';
|
|
779
852
|
const state = this.ensureSessionState(championId);
|
|
780
853
|
const safeTimeoutMs = Math.max(1, timeoutMs);
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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;
|
|
786
870
|
}
|
|
787
|
-
|
|
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
|
+
}
|
|
788
914
|
return {
|
|
789
|
-
|
|
790
|
-
timedOut: false,
|
|
791
|
-
elapsedMs: 0,
|
|
792
|
-
status: 'failed',
|
|
793
|
-
errorMessage: runtimeStatus === 'systemError'
|
|
794
|
-
? 'Codex thread is in systemError state'
|
|
795
|
-
: 'Unable to determine Codex thread runtime status'
|
|
915
|
+
kind: 'idle',
|
|
916
|
+
waitResult: { completed: true, timedOut: false, elapsedMs: 0, status: 'completed' }
|
|
796
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;
|
|
797
937
|
}
|
|
798
|
-
|
|
799
|
-
|
|
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
|
+
}
|
|
800
945
|
state.lastTurnStatus = result.status;
|
|
801
946
|
state.lastTurnError = result.errorMessage;
|
|
947
|
+
if (result.assistantText) {
|
|
948
|
+
state.assistantHistory.push(result.assistantText);
|
|
949
|
+
}
|
|
802
950
|
return result;
|
|
803
951
|
}
|
|
804
952
|
async waitForTurn(championId, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
@@ -853,6 +1001,28 @@ class CodexAppServerBackend {
|
|
|
853
1001
|
throw error;
|
|
854
1002
|
}
|
|
855
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
|
+
}
|
|
856
1026
|
getSessionStatus(championId) {
|
|
857
1027
|
const state = this.ensureSessionState(championId);
|
|
858
1028
|
if (state.lastTurnStatus === 'failed') {
|
|
@@ -901,33 +1071,26 @@ class CodexAppServerBackend {
|
|
|
901
1071
|
if (!threadId || threadId.trim().length === 0) {
|
|
902
1072
|
return this.daemonManager.isServerRunning(pid, port);
|
|
903
1073
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
catch {
|
|
911
|
-
return false;
|
|
912
|
-
}
|
|
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;
|
|
913
1080
|
}
|
|
914
1081
|
async threadExistsOnServer(client, threadId) {
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
cursor,
|
|
920
|
-
limit: pageLimit,
|
|
921
|
-
modelProviders: []
|
|
1082
|
+
try {
|
|
1083
|
+
await client.request('thread/read', {
|
|
1084
|
+
threadId,
|
|
1085
|
+
includeTurns: false
|
|
922
1086
|
});
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
if (!page.nextCursor) {
|
|
1087
|
+
return true;
|
|
1088
|
+
}
|
|
1089
|
+
catch (error) {
|
|
1090
|
+
if (this.isThreadReadNotFoundError(error)) {
|
|
928
1091
|
return false;
|
|
929
1092
|
}
|
|
930
|
-
|
|
1093
|
+
throw error;
|
|
931
1094
|
}
|
|
932
1095
|
}
|
|
933
1096
|
ensureSessionState(championId) {
|
|
@@ -992,6 +1155,12 @@ class CodexAppServerBackend {
|
|
|
992
1155
|
}
|
|
993
1156
|
return THREAD_READ_UNMATERIALIZED_PATTERN.test(error.message);
|
|
994
1157
|
}
|
|
1158
|
+
isThreadReadNotFoundError(error) {
|
|
1159
|
+
if (!(error instanceof Error)) {
|
|
1160
|
+
return false;
|
|
1161
|
+
}
|
|
1162
|
+
return THREAD_READ_NOT_FOUND_PATTERN.test(error.message);
|
|
1163
|
+
}
|
|
995
1164
|
}
|
|
996
1165
|
exports.CodexAppServerBackend = CodexAppServerBackend;
|
|
997
1166
|
//# sourceMappingURL=codex-appserver.js.map
|