dev-sessions 0.2.2 → 0.2.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.
Files changed (39) hide show
  1. package/README.md +124 -77
  2. package/dist/backends/backend.d.ts +42 -0
  3. package/dist/backends/backend.js +3 -0
  4. package/dist/backends/backend.js.map +1 -0
  5. package/dist/backends/claude-backend.d.ts +22 -0
  6. package/dist/backends/claude-backend.js +176 -0
  7. package/dist/backends/claude-backend.js.map +1 -0
  8. package/dist/backends/claude-tmux.d.ts +0 -2
  9. package/dist/backends/claude-tmux.js +8 -21
  10. package/dist/backends/claude-tmux.js.map +1 -1
  11. package/dist/backends/codex-appserver.d.ts +44 -2
  12. package/dist/backends/codex-appserver.js +291 -109
  13. package/dist/backends/codex-appserver.js.map +1 -1
  14. package/dist/backends/codex-backend.d.ts +21 -0
  15. package/dist/backends/codex-backend.js +224 -0
  16. package/dist/backends/codex-backend.js.map +1 -0
  17. package/dist/cli.d.ts +3 -1
  18. package/dist/cli.js +31 -3
  19. package/dist/cli.js.map +1 -1
  20. package/dist/gateway/client.d.ts +3 -1
  21. package/dist/gateway/client.js +24 -2
  22. package/dist/gateway/client.js.map +1 -1
  23. package/dist/gateway/server.js +45 -2
  24. package/dist/gateway/server.js.map +1 -1
  25. package/dist/index.js +6 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/session-manager.d.ts +7 -9
  28. package/dist/session-manager.js +115 -338
  29. package/dist/session-manager.js.map +1 -1
  30. package/dist/session-store.d.ts +4 -0
  31. package/dist/session-store.js +115 -42
  32. package/dist/session-store.js.map +1 -1
  33. package/dist/transcript/claude-parser.d.ts +1 -0
  34. package/dist/transcript/claude-parser.js +4 -0
  35. package/dist/transcript/claude-parser.js.map +1 -1
  36. package/dist/types.d.ts +7 -1
  37. package/package.json +1 -1
  38. package/skills/delegate/SKILL.md +77 -0
  39. 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
- if (this.turnStatus) {
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
- if (!frame.includes('\n') && this.stdoutBuffer.length === 0) {
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.splice(0, this.waiters.length);
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 uses #[serde(tag = "type", rename_all = "camelCase")]:
561
- // { "type": "idle" } | { "type": "notLoaded" } | { "type": "systemError" } | { "type": "active", "activeFlags": [...] }
562
- // Older Codex versions omit the status field entirely treat absent status as idle.
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
- const type = status?.type;
571
- if (type === 'active' || type === 'idle' || type === 'systemError' || type === 'notLoaded') {
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
- const record = result;
581
- if (!Array.isArray(record.data)) {
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
- const threadIds = record.data
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 asRecord = params;
625
- const directText = asRecord.text;
626
- if (typeof directText === 'string') {
627
- return directText;
628
- }
629
- const delta = asRecord.delta;
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
- if (delta && typeof delta === 'object' && typeof delta.text === 'string') {
634
- return delta.text;
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 item = asRecord.item;
637
- if (item && typeof item === 'object') {
638
- const nestedDelta = item.delta;
639
- if (nestedDelta &&
640
- typeof nestedDelta === 'object' &&
641
- typeof nestedDelta.text === 'string') {
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
- return tid;
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 { result } = await this.withConnectedClient(async (client) => {
769
- const resumeResult = await client.request('thread/resume', { threadId: normalizedThreadId });
770
- const runtimeStatus = extractThreadRuntimeStatus(resumeResult);
771
- if (runtimeStatus === 'active') {
772
- return client.waitForTurnCompletion(safeTimeoutMs);
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
- if (runtimeStatus === 'systemError' || runtimeStatus === 'unknown') {
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
- completed: true,
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
- return { completed: true, timedOut: false, elapsedMs: 0, status: 'completed' };
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
- try {
892
- const { result } = await this.withConnectedClient(async (client) => {
893
- return this.threadExistsOnServer(client, threadId.trim());
894
- });
895
- return result;
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
- let cursor;
903
- const pageLimit = 100;
904
- while (true) {
905
- const listResult = await client.request('thread/list', {
906
- cursor,
907
- limit: pageLimit,
908
- modelProviders: []
1082
+ try {
1083
+ await client.request('thread/read', {
1084
+ threadId,
1085
+ includeTurns: false
909
1086
  });
910
- const page = extractThreadListPage(listResult);
911
- if (page.threadIds.includes(threadId)) {
912
- return true;
913
- }
914
- if (!page.nextCursor) {
1087
+ return true;
1088
+ }
1089
+ catch (error) {
1090
+ if (this.isThreadReadNotFoundError(error)) {
915
1091
  return false;
916
1092
  }
917
- cursor = page.nextCursor;
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