@love-moon/ai-sdk 0.2.16

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.
@@ -0,0 +1,886 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { CodexAppServerTransport } from "../transports/codex-app-server-transport.js";
3
+ import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
4
+ const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
5
+ const MIN_TURN_DEADLINE_MS = 30 * 1000;
6
+ const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
7
+ function waitForever() {
8
+ return new Promise(() => { });
9
+ }
10
+ function isCodexBackend(backend) {
11
+ const normalized = String(backend || "").trim().toLowerCase();
12
+ return normalized === "codex" || normalized === "code";
13
+ }
14
+ function normalizeCodexBackend(backend) {
15
+ return isCodexBackend(backend) ? "codex" : String(backend || "codex").trim().toLowerCase();
16
+ }
17
+ function extractSessionConfiguredValue(params, ...keys) {
18
+ for (const key of keys) {
19
+ if (params && typeof params === "object" && params[key] !== undefined && params[key] !== null) {
20
+ return params[key];
21
+ }
22
+ }
23
+ const session = params?.session;
24
+ if (session && typeof session === "object") {
25
+ for (const key of keys) {
26
+ if (session[key] !== undefined && session[key] !== null) {
27
+ return session[key];
28
+ }
29
+ }
30
+ }
31
+ return undefined;
32
+ }
33
+ function buildTurnInput(promptText) {
34
+ return [
35
+ {
36
+ type: "text",
37
+ text: promptText,
38
+ },
39
+ ];
40
+ }
41
+ function normalizeItemId(value) {
42
+ return typeof value === "string" ? value.trim() : "";
43
+ }
44
+ function extractItemId(item) {
45
+ if (!item || typeof item !== "object") {
46
+ return "";
47
+ }
48
+ return normalizeItemId(item.id || item.itemId || item.item_id);
49
+ }
50
+ function resolveItemPhase(item) {
51
+ if (!item || typeof item !== "object") {
52
+ return null;
53
+ }
54
+ const candidates = new Set();
55
+ const collectCandidate = (value) => {
56
+ if (typeof value !== "string") {
57
+ return;
58
+ }
59
+ const normalized = value.trim().toLowerCase();
60
+ if (!normalized) {
61
+ return;
62
+ }
63
+ candidates.add(normalized);
64
+ };
65
+ collectCandidate(item.type);
66
+ collectCandidate(item.kind);
67
+ collectCandidate(item.phase);
68
+ collectCandidate(item.name);
69
+ collectCandidate(item.toolName);
70
+ if (item.reasoning || item.reasoningItem || item.summary || item.summaryText) {
71
+ return "reasoning";
72
+ }
73
+ if (item.agentMessage || item.message) {
74
+ return "assistant_message";
75
+ }
76
+ if (item.commandExecution || item.command || item.commandExecutionId) {
77
+ return "command_execution";
78
+ }
79
+ if (item.mcpToolCall || item.toolCall || item.toolName) {
80
+ return "mcp_tool_call";
81
+ }
82
+ const hasCandidate = (...values) => values.some((value) => candidates.has(value));
83
+ const includesCandidate = (...values) => [...candidates].some((candidate) => values.some((value) => candidate.includes(value)));
84
+ if (hasCandidate("reasoning", "agent_reasoning", "summary_text", "summary") ||
85
+ includesCandidate("reason", "summary")) {
86
+ return "reasoning";
87
+ }
88
+ if (hasCandidate("message", "agent_message", "output_text", "final_answer")) {
89
+ return "assistant_message";
90
+ }
91
+ if (hasCandidate("compaction", "context_compacted", "compacted") || includesCandidate("compact")) {
92
+ return "context_compaction";
93
+ }
94
+ if (hasCandidate("update_plan", "plan_update", "plan") ||
95
+ includesCandidate("plan")) {
96
+ return "planning";
97
+ }
98
+ if (hasCandidate("open_page", "search", "web_search_call") ||
99
+ includesCandidate("web_search", "open_page", "search_query", "navigate_page", "find", "image_query")) {
100
+ return "web_lookup";
101
+ }
102
+ if (hasCandidate("custom_tool_call", "function_call", "mcp_tool_call", "tool_call") ||
103
+ includesCandidate("tool_call", "mcp", "tool")) {
104
+ if (includesCandidate("exec_command", "shell", "bash", "terminal", "command")) {
105
+ return "command_execution";
106
+ }
107
+ if (includesCandidate("apply_patch", "write_file", "edit_file", "replace", "patch")) {
108
+ return "file_update";
109
+ }
110
+ if (includesCandidate("search_query", "open_page", "navigate_page", "image_query", "find", "web_search")) {
111
+ return "web_lookup";
112
+ }
113
+ if (includesCandidate("read_", "list_", "rg", "cat", "view_", "read_mcp_resource", "list_mcp")) {
114
+ return "workspace_inspection";
115
+ }
116
+ return "tool_call";
117
+ }
118
+ if (includesCandidate("exec_command", "shell", "bash", "terminal", "command_execution", "command")) {
119
+ return "command_execution";
120
+ }
121
+ if (includesCandidate("apply_patch", "write_file", "edit_file", "patch")) {
122
+ return "file_update";
123
+ }
124
+ if (includesCandidate("read_", "list_", "rg", "cat", "view_", "read_mcp_resource", "list_mcp")) {
125
+ return "workspace_inspection";
126
+ }
127
+ return null;
128
+ }
129
+ function statusLineForPhase(phase) {
130
+ switch (phase) {
131
+ case "reasoning":
132
+ return "codex reasoning";
133
+ case "planning":
134
+ return "codex updating plan";
135
+ case "workspace_inspection":
136
+ return "codex reading workspace";
137
+ case "command_execution":
138
+ return "codex running command";
139
+ case "file_update":
140
+ return "codex editing files";
141
+ case "web_lookup":
142
+ return "codex browsing";
143
+ case "mcp_tool_call":
144
+ case "tool_call":
145
+ return "codex calling tool";
146
+ case "context_compaction":
147
+ return "codex compacting context";
148
+ case "message_aggregation":
149
+ case "assistant_message":
150
+ return "codex composing reply";
151
+ default:
152
+ return "codex is working";
153
+ }
154
+ }
155
+ function createTurnError(message, extras = {}) {
156
+ const error = new Error(message);
157
+ for (const [key, value] of Object.entries(extras)) {
158
+ error[key] = value;
159
+ }
160
+ return error;
161
+ }
162
+ export class CodexAppServerSession extends EventEmitter {
163
+ constructor(backend, options = {}) {
164
+ super();
165
+ this.backend = normalizeCodexBackend(backend);
166
+ this.options = options;
167
+ this.logger = normalizeLogger(options.logger);
168
+ this.cwd =
169
+ typeof options.cwd === "string" && options.cwd.trim()
170
+ ? options.cwd.trim()
171
+ : process.cwd();
172
+ this.resumeSessionId = typeof options.resumeSessionId === "string" ? options.resumeSessionId.trim() : "";
173
+ this.sessionId = this.resumeSessionId || "";
174
+ this.threadPath = "";
175
+ this.sessionInfo = null;
176
+ this.history = Array.isArray(options.initialHistory) ? [...options.initialHistory] : [];
177
+ this.pendingHistorySeed = this.history.length > 0;
178
+ this.closeRequested = false;
179
+ this.closed = false;
180
+ this.closeWaiters = new Set();
181
+ this.sessionMessageHandler = null;
182
+ this.workingStatusHandler = null;
183
+ this.activeReplyTarget = "";
184
+ this.lastReplyTarget = "";
185
+ this.manualResumeReady = false;
186
+ this.nativeSessionId = "";
187
+ this.rateLimits = null;
188
+ this.tokenUsage = null;
189
+ this.turnDeadlineMs = getBoundedEnvInt("CONDUCTOR_TURN_DEADLINE_MS", DEFAULT_TURN_DEADLINE_MS, MIN_TURN_DEADLINE_MS, MAX_TURN_DEADLINE_MS);
190
+ this.currentTurn = null;
191
+ this.bootPromise = null;
192
+ this.booted = false;
193
+ const envConfig = loadEnvConfig(options.configFile);
194
+ const proxyEnv = proxyToEnv(envConfig);
195
+ const extraEnv = envConfig && typeof envConfig === "object" ? { ...envConfig, ...proxyEnv } : proxyEnv;
196
+ this.transport = new CodexAppServerTransport({
197
+ cwd: this.cwd,
198
+ env: extraEnv,
199
+ logger: {
200
+ log: (message) => {
201
+ this.writeLog(message);
202
+ },
203
+ },
204
+ commandLine: options.commandLine,
205
+ });
206
+ this.transport.on("notification", ({ method, params }) => {
207
+ void this.handleNotification(method, params);
208
+ });
209
+ this.transport.on("process_exit", (payload) => {
210
+ this.handleTransportExit(payload);
211
+ });
212
+ this.transport.on("process_error", (payload) => {
213
+ const error = createTurnError(payload?.message || "Codex app-server transport error", payload || {});
214
+ this.handleTransportFailure(error);
215
+ });
216
+ }
217
+ writeLog(message) {
218
+ emitLog(this.logger, message);
219
+ }
220
+ trace(message) {
221
+ this.writeLog(`[${this.backend}] [app-server] ${message}`);
222
+ }
223
+ get threadId() {
224
+ return this.sessionId;
225
+ }
226
+ get threadOptions() {
227
+ return { model: this.backend };
228
+ }
229
+ getSnapshot() {
230
+ return {
231
+ backend: this.backend,
232
+ provider: "codex-app-server",
233
+ cwd: this.cwd,
234
+ sessionId: this.sessionId || undefined,
235
+ sessionInfo: this.getSessionInfo(),
236
+ useSessionFileReplyStream: this.usesSessionFileReplyStream(),
237
+ resumeReady: this.manualResumeReady,
238
+ manualResume: this.sessionId
239
+ ? {
240
+ ready: this.manualResumeReady,
241
+ command: `codex resume ${this.sessionId}`,
242
+ }
243
+ : null,
244
+ pid: this.transport.pid || undefined,
245
+ };
246
+ }
247
+ getSessionInfo() {
248
+ return this.sessionInfo ? { ...this.sessionInfo } : null;
249
+ }
250
+ async ensureSessionInfo() {
251
+ await this.boot();
252
+ return this.getSessionInfo();
253
+ }
254
+ async getSessionUsageSummary() {
255
+ return {
256
+ sessionId: this.sessionId || undefined,
257
+ sessionFilePath: this.threadPath || undefined,
258
+ tokenUsagePercent: Number.isFinite(Number(this.rateLimits?.primary?.usedPercent))
259
+ ? Number(this.rateLimits.primary.usedPercent)
260
+ : undefined,
261
+ contextUsagePercent: this.resolveContextUsagePercent(),
262
+ tokenUsage: this.tokenUsage ? { ...this.tokenUsage } : null,
263
+ rateLimits: this.rateLimits ? { ...this.rateLimits } : null,
264
+ manualResume: this.sessionId
265
+ ? {
266
+ ready: this.manualResumeReady,
267
+ command: `codex resume ${this.sessionId}`,
268
+ }
269
+ : null,
270
+ };
271
+ }
272
+ usesSessionFileReplyStream() {
273
+ return true;
274
+ }
275
+ setSessionMessageHandler(handler) {
276
+ this.sessionMessageHandler = typeof handler === "function" ? handler : null;
277
+ }
278
+ setWorkingStatusHandler(handler) {
279
+ this.workingStatusHandler = typeof handler === "function" ? handler : null;
280
+ }
281
+ setSessionReplyTarget(replyTo) {
282
+ const normalizedReplyTo = typeof replyTo === "string" ? replyTo.trim() : "";
283
+ this.activeReplyTarget = normalizedReplyTo;
284
+ if (normalizedReplyTo) {
285
+ this.lastReplyTarget = normalizedReplyTo;
286
+ }
287
+ }
288
+ getCurrentReplyTarget() {
289
+ return this.activeReplyTarget || this.lastReplyTarget || undefined;
290
+ }
291
+ async boot() {
292
+ if (this.booted) {
293
+ return;
294
+ }
295
+ if (this.bootPromise) {
296
+ return this.bootPromise;
297
+ }
298
+ this.bootPromise = this.bootInternal();
299
+ try {
300
+ await this.bootPromise;
301
+ this.booted = true;
302
+ }
303
+ finally {
304
+ this.bootPromise = null;
305
+ }
306
+ }
307
+ async bootInternal() {
308
+ await this.transport.boot();
309
+ const params = {
310
+ cwd: this.cwd,
311
+ approvalPolicy: "never",
312
+ sandbox: "danger-full-access",
313
+ personality: "pragmatic",
314
+ ephemeral: false,
315
+ };
316
+ if (this.options.model) {
317
+ params.model = this.options.model;
318
+ }
319
+ let result;
320
+ if (this.resumeSessionId) {
321
+ result = await this.transport.request("thread/resume", {
322
+ ...params,
323
+ threadId: this.resumeSessionId,
324
+ });
325
+ }
326
+ else {
327
+ result = await this.transport.request("thread/start", params);
328
+ }
329
+ this.applyThreadInfo(result?.thread, { resumeReady: Boolean(this.resumeSessionId) });
330
+ }
331
+ applyThreadInfo(thread, { resumeReady = false } = {}) {
332
+ const threadId = typeof thread?.id === "string" ? thread.id.trim() : "";
333
+ const threadPath = typeof thread?.path === "string" ? thread.path.trim() : "";
334
+ if (!threadId) {
335
+ return;
336
+ }
337
+ this.sessionId = threadId;
338
+ this.threadPath = threadPath;
339
+ this.sessionInfo = {
340
+ backend: this.backend,
341
+ sessionId: threadId,
342
+ sessionFilePath: threadPath || undefined,
343
+ };
344
+ if (resumeReady) {
345
+ this.manualResumeReady = true;
346
+ }
347
+ this.trace(`thread ready id=${threadId} path="${sanitizeForLog(threadPath, 180)}"`);
348
+ this.emit("session", this.getSessionInfo());
349
+ }
350
+ applySessionConfigured(params) {
351
+ const nativeSessionId = extractSessionConfiguredValue(params, "session_id", "sessionId");
352
+ const rolloutPath = extractSessionConfiguredValue(params, "rollout_path", "rolloutPath");
353
+ if (typeof nativeSessionId === "string" && nativeSessionId.trim()) {
354
+ this.nativeSessionId = nativeSessionId.trim();
355
+ }
356
+ if (typeof rolloutPath === "string" && rolloutPath.trim()) {
357
+ this.threadPath = rolloutPath.trim();
358
+ if (this.sessionInfo) {
359
+ this.sessionInfo = {
360
+ ...this.sessionInfo,
361
+ sessionFilePath: this.threadPath,
362
+ };
363
+ this.emit("session", this.getSessionInfo());
364
+ }
365
+ }
366
+ }
367
+ resolveContextUsagePercent() {
368
+ const totalTokens = Number(this.tokenUsage?.total?.totalTokens);
369
+ const contextWindow = Number(this.tokenUsage?.modelContextWindow);
370
+ if (!Number.isFinite(totalTokens) || !Number.isFinite(contextWindow) || contextWindow <= 0) {
371
+ return undefined;
372
+ }
373
+ return Math.max(0, Math.min(100, Math.round((totalTokens / contextWindow) * 100)));
374
+ }
375
+ normalizeWorkingStatusPayload(payload) {
376
+ return {
377
+ source: "codex-app-server",
378
+ reply_in_progress: Boolean(payload?.reply_in_progress),
379
+ replyTo: payload?.replyTo || this.getCurrentReplyTarget(),
380
+ state: payload?.state,
381
+ phase: payload?.phase,
382
+ status_line: payload?.status_line,
383
+ status_done_line: payload?.status_done_line,
384
+ reply_preview: payload?.reply_preview,
385
+ thread_id: this.sessionId || undefined,
386
+ };
387
+ }
388
+ async emitWorkingStatus(payload) {
389
+ const normalized = this.normalizeWorkingStatusPayload(payload);
390
+ if (typeof this.workingStatusHandler === "function") {
391
+ await this.workingStatusHandler(normalized);
392
+ }
393
+ this.emit("working_status", normalized);
394
+ }
395
+ async emitAssistantMessage(text) {
396
+ const payload = {
397
+ text,
398
+ preserveWhitespace: true,
399
+ source: "codex-app-server",
400
+ replyTo: this.getCurrentReplyTarget(),
401
+ sessionId: this.sessionId || undefined,
402
+ sessionFilePath: this.threadPath || undefined,
403
+ timestamp: new Date().toISOString(),
404
+ };
405
+ if (typeof this.sessionMessageHandler === "function") {
406
+ await this.sessionMessageHandler(payload);
407
+ }
408
+ this.emit("assistant_message", payload);
409
+ }
410
+ async finalizeActiveAssistantMessage(currentTurn = this.currentTurn, { messageId = "" } = {}) {
411
+ if (!currentTurn) {
412
+ return false;
413
+ }
414
+ const finalizedMessageId = normalizeItemId(messageId) || currentTurn.activeAssistantMessageId || "";
415
+ const activeMessageId = currentTurn.activeAssistantMessageId || "";
416
+ if (finalizedMessageId && activeMessageId && activeMessageId !== finalizedMessageId) {
417
+ return false;
418
+ }
419
+ const text = currentTurn.activeAssistantMessageText || "";
420
+ currentTurn.activeAssistantMessageText = "";
421
+ if (!finalizedMessageId || !activeMessageId || activeMessageId === finalizedMessageId) {
422
+ currentTurn.activeAssistantMessageId = "";
423
+ }
424
+ if (!text) {
425
+ return false;
426
+ }
427
+ await this.emitAssistantMessage(text);
428
+ return true;
429
+ }
430
+ createSessionClosedError() {
431
+ const error = new Error("Codex app-server session closed");
432
+ error.reason = "session_closed";
433
+ return error;
434
+ }
435
+ createTurnTimeoutError(timeoutMs) {
436
+ const seconds = Math.max(1, Math.round(timeoutMs / 1000));
437
+ const error = new Error(`Turn exceeded hard deadline (${seconds}s)`);
438
+ error.reason = "turn_timeout";
439
+ error.timeoutMs = timeoutMs;
440
+ return error;
441
+ }
442
+ createCloseGuard() {
443
+ if (this.closeRequested) {
444
+ return {
445
+ promise: Promise.reject(this.createSessionClosedError()),
446
+ cleanup: () => { },
447
+ };
448
+ }
449
+ let waiter = null;
450
+ const promise = new Promise((_, reject) => {
451
+ waiter = () => {
452
+ reject(this.createSessionClosedError());
453
+ };
454
+ this.closeWaiters.add(waiter);
455
+ });
456
+ return {
457
+ promise,
458
+ cleanup: () => {
459
+ if (waiter) {
460
+ this.closeWaiters.delete(waiter);
461
+ }
462
+ },
463
+ };
464
+ }
465
+ createTurnTimeoutGuard() {
466
+ if (!Number.isFinite(this.turnDeadlineMs) || this.turnDeadlineMs <= 0) {
467
+ return {
468
+ promise: waitForever(),
469
+ cleanup: () => { },
470
+ };
471
+ }
472
+ let timer = null;
473
+ const promise = new Promise((_, reject) => {
474
+ timer = setTimeout(() => {
475
+ reject(this.createTurnTimeoutError(this.turnDeadlineMs));
476
+ }, this.turnDeadlineMs);
477
+ if (typeof timer.unref === "function") {
478
+ timer.unref();
479
+ }
480
+ });
481
+ return {
482
+ promise,
483
+ cleanup: () => {
484
+ if (timer) {
485
+ clearTimeout(timer);
486
+ }
487
+ },
488
+ };
489
+ }
490
+ flushCloseWaiters() {
491
+ if (this.closeWaiters.size === 0) {
492
+ return;
493
+ }
494
+ for (const waiter of this.closeWaiters) {
495
+ try {
496
+ waiter();
497
+ }
498
+ catch {
499
+ // best effort
500
+ }
501
+ }
502
+ this.closeWaiters.clear();
503
+ }
504
+ buildPrompt(promptText, { useInitialImages = false } = {}) {
505
+ let effectivePrompt = String(promptText || "").trim();
506
+ if (!effectivePrompt) {
507
+ return "";
508
+ }
509
+ if (this.pendingHistorySeed) {
510
+ const historyText = this.history
511
+ .map((item) => {
512
+ const role = String(item?.role || "").toLowerCase() === "assistant" ? "Assistant" : "User";
513
+ return `${role}: ${String(item?.content || "").trim()}`;
514
+ })
515
+ .filter(Boolean)
516
+ .join("\n\n");
517
+ if (historyText) {
518
+ effectivePrompt = [
519
+ "Continue the existing conversation with this history.",
520
+ "",
521
+ historyText,
522
+ "",
523
+ `User: ${effectivePrompt}`,
524
+ ].join("\n");
525
+ }
526
+ this.pendingHistorySeed = false;
527
+ }
528
+ const images = Array.isArray(this.options.initialImages) ? this.options.initialImages : [];
529
+ if (useInitialImages && images.length > 0) {
530
+ const imageContext = images.map((item, idx) => `${idx + 1}. ${item}`).join("\n");
531
+ effectivePrompt = `${effectivePrompt}\n\nAttached image files:\n${imageContext}`;
532
+ }
533
+ return effectivePrompt;
534
+ }
535
+ getCurrentTurn() {
536
+ return this.currentTurn;
537
+ }
538
+ ensureCurrentTurn(params) {
539
+ const currentTurn = this.currentTurn;
540
+ if (!currentTurn) {
541
+ return null;
542
+ }
543
+ const turnId = typeof params?.turnId === "string" ? params.turnId.trim() : "";
544
+ if (turnId && currentTurn.turnId && currentTurn.turnId !== turnId) {
545
+ return null;
546
+ }
547
+ return currentTurn;
548
+ }
549
+ async queueAssistantDelta(delta, { messageId = "" } = {}) {
550
+ const currentTurn = this.currentTurn;
551
+ if (!currentTurn) {
552
+ return;
553
+ }
554
+ const normalizedMessageId = normalizeItemId(messageId);
555
+ if (normalizedMessageId &&
556
+ currentTurn.activeAssistantMessageId &&
557
+ currentTurn.activeAssistantMessageId !== normalizedMessageId) {
558
+ await this.finalizeActiveAssistantMessage(currentTurn, {
559
+ messageId: currentTurn.activeAssistantMessageId,
560
+ });
561
+ }
562
+ if (normalizedMessageId) {
563
+ currentTurn.activeAssistantMessageId = normalizedMessageId;
564
+ }
565
+ currentTurn.fullText += delta;
566
+ currentTurn.activeAssistantMessageText += delta;
567
+ }
568
+ async handleNotification(method, params = {}) {
569
+ if (this.closeRequested) {
570
+ return;
571
+ }
572
+ switch (method) {
573
+ case "thread/started":
574
+ this.applyThreadInfo(params?.thread, { resumeReady: Boolean(this.resumeSessionId) });
575
+ return;
576
+ case "sessionConfigured":
577
+ case "session_configured":
578
+ this.applySessionConfigured(params);
579
+ return;
580
+ case "turn/started": {
581
+ const currentTurn = this.getCurrentTurn();
582
+ if (!currentTurn) {
583
+ return;
584
+ }
585
+ const turnId = typeof params?.turn?.id === "string"
586
+ ? params.turn.id.trim()
587
+ : typeof params?.turnId === "string"
588
+ ? params.turnId.trim()
589
+ : "";
590
+ if (turnId) {
591
+ currentTurn.turnId = turnId;
592
+ }
593
+ await this.emitWorkingStatus({
594
+ phase: "turn_started",
595
+ reply_in_progress: true,
596
+ status_line: "codex is working",
597
+ });
598
+ return;
599
+ }
600
+ case "item/started": {
601
+ const currentTurn = this.ensureCurrentTurn(params);
602
+ if (!currentTurn) {
603
+ return;
604
+ }
605
+ const phase = resolveItemPhase(params?.item);
606
+ if (!phase) {
607
+ return;
608
+ }
609
+ if (phase === "assistant_message") {
610
+ const nextMessageId = extractItemId(params?.item);
611
+ if (nextMessageId &&
612
+ currentTurn.activeAssistantMessageId &&
613
+ currentTurn.activeAssistantMessageId !== nextMessageId) {
614
+ await this.finalizeActiveAssistantMessage(currentTurn, {
615
+ messageId: currentTurn.activeAssistantMessageId,
616
+ });
617
+ }
618
+ if (nextMessageId) {
619
+ currentTurn.activeAssistantMessageId = nextMessageId;
620
+ }
621
+ }
622
+ await this.emitWorkingStatus({
623
+ phase: phase === "assistant_message" ? "message_aggregation" : phase,
624
+ reply_in_progress: true,
625
+ status_line: statusLineForPhase(phase === "assistant_message" ? "message_aggregation" : phase),
626
+ });
627
+ return;
628
+ }
629
+ case "item/completed": {
630
+ const currentTurn = this.ensureCurrentTurn(params);
631
+ if (!currentTurn) {
632
+ return;
633
+ }
634
+ const phase = resolveItemPhase(params?.item);
635
+ if (phase !== "assistant_message") {
636
+ return;
637
+ }
638
+ const completedMessageId = extractItemId(params?.item) || currentTurn.activeAssistantMessageId || "";
639
+ await this.finalizeActiveAssistantMessage(currentTurn, {
640
+ messageId: completedMessageId,
641
+ });
642
+ return;
643
+ }
644
+ case "item/reasoning/summaryTextDelta":
645
+ case "item/reasoning/textDelta":
646
+ await this.emitWorkingStatus({
647
+ phase: "reasoning",
648
+ reply_in_progress: true,
649
+ status_line: statusLineForPhase("reasoning"),
650
+ });
651
+ return;
652
+ case "item/commandExecution/outputDelta":
653
+ await this.emitWorkingStatus({
654
+ phase: "command_execution",
655
+ reply_in_progress: true,
656
+ status_line: statusLineForPhase("command_execution"),
657
+ });
658
+ return;
659
+ case "item/mcpToolCall/progress":
660
+ await this.emitWorkingStatus({
661
+ phase: "mcp_tool_call",
662
+ reply_in_progress: true,
663
+ status_line: statusLineForPhase("mcp_tool_call"),
664
+ });
665
+ return;
666
+ case "item/agentMessage/delta": {
667
+ const currentTurn = this.ensureCurrentTurn(params);
668
+ if (!currentTurn) {
669
+ return;
670
+ }
671
+ const delta = typeof params?.delta === "string" ? params.delta : "";
672
+ const messageId = normalizeItemId(params?.itemId || params?.item_id);
673
+ if (!delta) {
674
+ return;
675
+ }
676
+ await this.emitWorkingStatus({
677
+ phase: "message_aggregation",
678
+ reply_in_progress: true,
679
+ status_line: statusLineForPhase("message_aggregation"),
680
+ });
681
+ await this.queueAssistantDelta(delta, { messageId });
682
+ return;
683
+ }
684
+ case "thread/tokenUsage/updated":
685
+ this.tokenUsage = params?.tokenUsage && typeof params.tokenUsage === "object" ? { ...params.tokenUsage } : null;
686
+ return;
687
+ case "account/rateLimits/updated":
688
+ this.rateLimits = params?.rateLimits && typeof params.rateLimits === "object" ? { ...params.rateLimits } : null;
689
+ return;
690
+ case "turn/completed": {
691
+ const currentTurn = this.ensureCurrentTurn(params);
692
+ if (!currentTurn) {
693
+ return;
694
+ }
695
+ await this.finalizeActiveAssistantMessage(currentTurn, {
696
+ messageId: currentTurn.activeAssistantMessageId,
697
+ });
698
+ this.manualResumeReady = true;
699
+ this.activeReplyTarget = "";
700
+ const turn = params?.turn && typeof params.turn === "object" ? params.turn : {};
701
+ const status = typeof turn.status === "string" ? turn.status.trim() : "";
702
+ const turnError = turn?.error;
703
+ if (status && status !== "completed") {
704
+ const error = createTurnError(`Codex turn failed (${status})`, {
705
+ reason: "turn_failed",
706
+ turnStatus: status,
707
+ turnError,
708
+ });
709
+ await this.emitWorkingStatus({
710
+ phase: "turn_failed",
711
+ reply_in_progress: false,
712
+ status_done_line: `codex failed (${status})`,
713
+ });
714
+ currentTurn.reject(error);
715
+ this.currentTurn = null;
716
+ return;
717
+ }
718
+ await this.emitWorkingStatus({
719
+ phase: "turn_completed",
720
+ reply_in_progress: false,
721
+ status_done_line: "codex finished",
722
+ });
723
+ currentTurn.resolve({
724
+ turn,
725
+ usage: this.tokenUsage ? { ...this.tokenUsage } : null,
726
+ });
727
+ this.currentTurn = null;
728
+ return;
729
+ }
730
+ default:
731
+ return;
732
+ }
733
+ }
734
+ handleTransportFailure(error) {
735
+ if (this.currentTurn) {
736
+ this.currentTurn.reject(error);
737
+ this.currentTurn = null;
738
+ }
739
+ }
740
+ handleTransportExit(payload) {
741
+ const exitError = createTurnError("Codex app-server exited", {
742
+ reason: this.closeRequested ? "session_closed" : "transport_exited",
743
+ code: payload?.code,
744
+ signal: payload?.signal,
745
+ stderr: payload?.stderr,
746
+ });
747
+ this.closed = true;
748
+ this.closeRequested = true;
749
+ this.flushCloseWaiters();
750
+ this.handleTransportFailure(exitError);
751
+ this.emit("process.exited", {
752
+ pid: this.transport.pid || null,
753
+ code: payload?.code,
754
+ signal: payload?.signal,
755
+ stderr: payload?.stderr,
756
+ });
757
+ }
758
+ maybeEmitAuthRequired(error) {
759
+ const message = String(error?.message || "").toLowerCase();
760
+ if (!message.includes("unauthorized") && !message.includes("login")) {
761
+ return;
762
+ }
763
+ this.emit("auth_required", {
764
+ reason: "login_required",
765
+ message: error.message,
766
+ });
767
+ }
768
+ async interruptCurrentTurn() {
769
+ const currentTurn = this.currentTurn;
770
+ if (!currentTurn || !this.sessionId || !currentTurn.turnId) {
771
+ return;
772
+ }
773
+ try {
774
+ await this.transport.request("turn/interrupt", {
775
+ threadId: this.sessionId,
776
+ turnId: currentTurn.turnId,
777
+ });
778
+ }
779
+ catch {
780
+ // best effort
781
+ }
782
+ }
783
+ async runTurn(promptText, { useInitialImages = false } = {}) {
784
+ if (this.closeRequested) {
785
+ throw this.createSessionClosedError();
786
+ }
787
+ await this.boot();
788
+ const effectivePrompt = this.buildPrompt(promptText, { useInitialImages });
789
+ if (!effectivePrompt) {
790
+ return {
791
+ text: "",
792
+ usage: null,
793
+ items: [],
794
+ events: [],
795
+ };
796
+ }
797
+ if (this.currentTurn) {
798
+ throw createTurnError("Codex app-server turn already running", {
799
+ reason: "turn_already_running",
800
+ });
801
+ }
802
+ this.history.push({ role: "user", content: promptText });
803
+ const closeGuard = this.createCloseGuard();
804
+ const turnTimeoutGuard = this.createTurnTimeoutGuard();
805
+ let resolveTurn = null;
806
+ let rejectTurn = null;
807
+ const completion = new Promise((resolve, reject) => {
808
+ resolveTurn = resolve;
809
+ rejectTurn = reject;
810
+ });
811
+ const currentTurn = {
812
+ turnId: "",
813
+ fullText: "",
814
+ activeAssistantMessageId: "",
815
+ activeAssistantMessageText: "",
816
+ resolve: resolveTurn,
817
+ reject: rejectTurn,
818
+ };
819
+ this.currentTurn = currentTurn;
820
+ try {
821
+ const startPromise = this.transport.request("turn/start", {
822
+ threadId: this.sessionId,
823
+ cwd: this.cwd,
824
+ approvalPolicy: "never",
825
+ input: buildTurnInput(effectivePrompt),
826
+ });
827
+ const turnResult = await Promise.race([
828
+ (async () => {
829
+ const requestResult = await startPromise;
830
+ const completionResult = await completion;
831
+ return {
832
+ requestResult,
833
+ completionResult,
834
+ };
835
+ })(),
836
+ closeGuard.promise,
837
+ turnTimeoutGuard.promise,
838
+ ]);
839
+ await this.finalizeActiveAssistantMessage(currentTurn, {
840
+ messageId: currentTurn.activeAssistantMessageId,
841
+ });
842
+ if (currentTurn.fullText) {
843
+ this.history.push({ role: "assistant", content: currentTurn.fullText });
844
+ }
845
+ return {
846
+ text: currentTurn.fullText,
847
+ usage: turnResult?.completionResult?.usage || null,
848
+ items: Array.isArray(turnResult?.requestResult?.turn?.items) ? turnResult.requestResult.turn.items : [],
849
+ events: [],
850
+ provider: this.backend,
851
+ metadata: {
852
+ source: "codex-app-server",
853
+ threadId: this.sessionId,
854
+ threadPath: this.threadPath || undefined,
855
+ nativeSessionId: this.nativeSessionId || undefined,
856
+ },
857
+ };
858
+ }
859
+ catch (error) {
860
+ if (error?.reason === "turn_timeout") {
861
+ await this.interruptCurrentTurn();
862
+ }
863
+ this.maybeEmitAuthRequired(error);
864
+ throw error;
865
+ }
866
+ finally {
867
+ if (this.currentTurn === currentTurn) {
868
+ this.currentTurn = null;
869
+ }
870
+ closeGuard.cleanup();
871
+ turnTimeoutGuard.cleanup();
872
+ }
873
+ }
874
+ async close() {
875
+ if (this.closed) {
876
+ return;
877
+ }
878
+ this.closeRequested = true;
879
+ this.flushCloseWaiters();
880
+ if (this.currentTurn) {
881
+ await this.interruptCurrentTurn();
882
+ }
883
+ await this.transport.close();
884
+ this.closed = true;
885
+ }
886
+ }