mcp-codex-worker 0.1.0 → 0.1.2

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/src/app.ts CHANGED
@@ -11,6 +11,12 @@ import {
11
11
  type TurnSteerInput,
12
12
  type WaitInput,
13
13
  } from './mcp/tool-definitions.js';
14
+ import { buildRequestBanner, buildThreadBanner } from './mcp/tool-banners.js';
15
+ import {
16
+ renderRequestListMarkdown,
17
+ renderThreadListMarkdown,
18
+ renderThreadMarkdown,
19
+ } from './mcp/task-markdown.js';
14
20
  import { OPERATION_POLL_INTERVAL_MS, REQUEST_TIMEOUT_MS } from './config/defaults.js';
15
21
  import { CodexRuntime } from './services/codex-runtime.js';
16
22
 
@@ -33,9 +39,13 @@ export class CodexWorkerApp {
33
39
  async listTools(): Promise<ToolDefinition[]> {
34
40
  try {
35
41
  const modelIds = await this.runtime.listVisibleModelIds();
36
- return createToolDefinitions(modelIds);
42
+ return createToolDefinitions({
43
+ modelIds,
44
+ threadBanner: buildThreadBanner(this.runtime),
45
+ requestBanner: buildRequestBanner(this.runtime),
46
+ });
37
47
  } catch {
38
- return createToolDefinitions([]);
48
+ return createToolDefinitions({ modelIds: [], threadBanner: '', requestBanner: '' });
39
49
  }
40
50
  }
41
51
 
@@ -44,8 +54,8 @@ export class CodexWorkerApp {
44
54
  {
45
55
  uri: 'codex://threads',
46
56
  name: 'Codex Threads',
47
- description: 'Latest threads from `thread/list`',
48
- mimeType: 'application/json',
57
+ description: 'All tracked threads with status',
58
+ mimeType: 'text/markdown',
49
59
  },
50
60
  {
51
61
  uri: 'codex://models',
@@ -72,8 +82,8 @@ export class CodexWorkerApp {
72
82
  {
73
83
  uri: `codex://thread/${threadId}`,
74
84
  name: `Thread ${threadId}`,
75
- description: 'Thread from `thread/read`',
76
- mimeType: 'application/json',
85
+ description: 'Thread status and guidance',
86
+ mimeType: 'text/markdown',
77
87
  },
78
88
  {
79
89
  uri: `codex://thread/${threadId}/events`,
@@ -98,18 +108,18 @@ export class CodexWorkerApp {
98
108
  }
99
109
  }
100
110
  return {
101
- mimeType: 'application/json',
102
- text: JSON.stringify(response, null, 2),
111
+ mimeType: 'text/markdown',
112
+ text: renderThreadListMarkdown(
113
+ response.data ?? [],
114
+ this.runtime.getPendingServerRequests(false),
115
+ ),
103
116
  };
104
117
  }
105
118
 
106
119
  if (uri === 'codex://models') {
107
- const response = {
108
- data: await this.runtime.listModels(false),
109
- };
110
120
  return {
111
121
  mimeType: 'application/json',
112
- text: JSON.stringify(response, null, 2),
122
+ text: JSON.stringify({ data: await this.runtime.listModels(false) }, null, 2),
113
123
  };
114
124
  }
115
125
 
@@ -153,8 +163,12 @@ export class CodexWorkerApp {
153
163
  includeTurns: true,
154
164
  });
155
165
  return {
156
- mimeType: 'application/json',
157
- text: JSON.stringify(response, null, 2),
166
+ mimeType: 'text/markdown',
167
+ text: renderThreadMarkdown(
168
+ response,
169
+ this.runtime.getOperationsForThread(threadId),
170
+ this.runtime.getPendingServerRequests(false),
171
+ ),
158
172
  };
159
173
  }
160
174
 
@@ -172,47 +186,29 @@ export class CodexWorkerApp {
172
186
  }
173
187
  const validated = tool.validate(args ?? {});
174
188
  switch (name) {
175
- case 'thread-start':
189
+ case 'codex-thread-start':
176
190
  return this.handleThreadStart(validated as ThreadStartInput);
177
- case 'thread-resume':
191
+ case 'codex-thread-resume':
178
192
  return this.handleThreadResume(validated as ThreadResumeInput);
179
- case 'thread-read':
193
+ case 'codex-thread-read':
180
194
  return this.handleThreadRead(validated as ThreadReadInput);
181
- case 'thread-list':
195
+ case 'codex-thread-list':
182
196
  return this.handleThreadList(validated as ThreadListInput);
183
- case 'turn-start':
197
+ case 'codex-turn-start':
184
198
  return this.handleTurnStart(validated as TurnStartInput);
185
- case 'turn-steer':
199
+ case 'codex-turn-steer':
186
200
  return this.handleTurnSteer(validated as TurnSteerInput);
187
- case 'turn-interrupt':
201
+ case 'codex-turn-interrupt':
188
202
  return this.handleTurnInterrupt(validated as TurnInterruptInput);
189
- case 'model-list':
190
- return JSON.stringify({
191
- data: await this.runtime.listModels(false),
192
- }, null, 2);
193
- case 'account-read':
194
- return JSON.stringify(await this.runtime.request('account/read', {}), null, 2);
195
- case 'account-rate-limits-read':
196
- return JSON.stringify(await this.runtime.request('account/rateLimits/read', {}), null, 2);
197
- case 'skills-list':
198
- return JSON.stringify(await this.runtime.request('skills/list', {}), null, 2);
199
- case 'app-list':
200
- return JSON.stringify(await this.runtime.request('app/list', {}), null, 2);
201
- case 'request-list':
202
- return JSON.stringify({
203
- requests: this.runtime.getPendingServerRequests(
203
+ case 'codex-request-list':
204
+ return renderRequestListMarkdown(
205
+ this.runtime.getPendingServerRequests(
204
206
  Boolean((validated as { include_resolved?: boolean }).include_resolved),
205
207
  ),
206
- }, null, 2);
207
- case 'request-read':
208
- return JSON.stringify({
209
- request: this.runtime.getPendingServerRequest(
210
- (validated as { request_id: string | number }).request_id,
211
- ) ?? null,
212
- }, null, 2);
213
- case 'request-respond':
208
+ );
209
+ case 'codex-request-respond':
214
210
  return this.handleRequestRespond(validated as RequestRespondInput);
215
- case 'wait':
211
+ case 'codex-wait':
216
212
  return this.handleWait(validated as WaitInput);
217
213
  default:
218
214
  throw new Error(`Unhandled tool: ${name}`);
@@ -230,11 +226,18 @@ export class CodexWorkerApp {
230
226
  if (threadId) {
231
227
  this.runtime.rememberThreadId(threadId);
232
228
  }
233
- return JSON.stringify({
234
- method: 'thread/start',
235
- remapped_from: built.remappedFrom,
236
- result: response,
237
- }, null, 2);
229
+
230
+ const remapNote = built.remappedFrom ? ` (remapped from ${built.remappedFrom})` : '';
231
+ return [
232
+ `**Thread started**`,
233
+ `thread_id: \`${threadId ?? 'unknown'}\``,
234
+ `model: \`${built.params.model as string}\`${remapNote}`,
235
+ '',
236
+ '**What to do next:**',
237
+ `- Use \`codex-turn-start\` with \`thread_id: "${threadId}"\` to send the agent its task.`,
238
+ '- After starting a turn, use `codex-wait` with `operation_id` to block until it finishes or asks for approval.',
239
+ '- Check `codex-request-list` after starting turns -- agents often need approval for commands or file changes.',
240
+ ].join('\n');
238
241
  }
239
242
 
240
243
  private async handleThreadResume(input: ThreadResumeInput): Promise<string> {
@@ -244,13 +247,17 @@ export class CodexWorkerApp {
244
247
  cwd: input.cwd,
245
248
  developerInstructions: input.developer_instructions,
246
249
  });
247
- const response = await this.runtime.request('thread/resume', built.params);
250
+ await this.runtime.request('thread/resume', built.params);
248
251
  this.runtime.rememberThreadId(input.thread_id);
249
- return JSON.stringify({
250
- method: 'thread/resume',
251
- remapped_from: built.remappedFrom,
252
- result: response,
253
- }, null, 2);
252
+
253
+ return [
254
+ `**Thread resumed**`,
255
+ `thread_id: \`${input.thread_id}\``,
256
+ '',
257
+ '**What to do next:**',
258
+ '- Use `codex-turn-start` to send a new task to this thread.',
259
+ '- The thread\'s previous context is loaded -- the agent remembers prior conversation.',
260
+ ].join('\n');
254
261
  }
255
262
 
256
263
  private async handleThreadRead(input: ThreadReadInput): Promise<string> {
@@ -259,7 +266,11 @@ export class CodexWorkerApp {
259
266
  includeTurns: input.include_turns ?? true,
260
267
  });
261
268
  this.runtime.rememberThreadId(input.thread_id);
262
- return JSON.stringify(response, null, 2);
269
+ return renderThreadMarkdown(
270
+ response,
271
+ this.runtime.getOperationsForThread(input.thread_id),
272
+ this.runtime.getPendingServerRequests(false),
273
+ );
263
274
  }
264
275
 
265
276
  private async handleThreadList(input: ThreadListInput): Promise<string> {
@@ -274,7 +285,10 @@ export class CodexWorkerApp {
274
285
  this.runtime.rememberThreadId(row.id);
275
286
  }
276
287
  }
277
- return JSON.stringify(response, null, 2);
288
+ return renderThreadListMarkdown(
289
+ response.data ?? [],
290
+ this.runtime.getPendingServerRequests(false),
291
+ );
278
292
  }
279
293
 
280
294
  private async handleTurnStart(input: TurnStartInput): Promise<string> {
@@ -287,11 +301,48 @@ export class CodexWorkerApp {
287
301
  const bridged = await this.runtime.requestWithBridge('turn/start', built.params, {
288
302
  threadId: input.thread_id,
289
303
  });
290
- return JSON.stringify({
291
- method: 'turn/start',
292
- remapped_from: built.remappedFrom,
293
- ...bridged,
294
- }, null, 2);
304
+
305
+ const remapNote = built.remappedFrom ? ` (remapped from ${built.remappedFrom})` : '';
306
+ const header = [
307
+ `thread_id: \`${input.thread_id}\``,
308
+ `operation_id: \`${bridged.operationId}\`${remapNote}`,
309
+ ];
310
+
311
+ if (bridged.status === 'pending_request') {
312
+ const reqCount = bridged.pendingRequestIds?.length ?? 0;
313
+ return [
314
+ `**Turn started -- approval needed**`,
315
+ ...header,
316
+ `pending_requests: ${reqCount}`,
317
+ '',
318
+ '**ACTION REQUIRED:**',
319
+ 'The agent needs approval before it can proceed. Use `codex-request-list` to see what it needs, then `codex-request-respond` to approve or decline.',
320
+ '',
321
+ '**After approving:**',
322
+ `Use \`codex-wait\` with \`operation_id: "${bridged.operationId}"\` to block until the turn completes.`,
323
+ ].join('\n');
324
+ }
325
+
326
+ if (bridged.status === 'completed') {
327
+ return [
328
+ `**Turn completed**`,
329
+ ...header,
330
+ '',
331
+ '**What to do next:**',
332
+ `- Use \`codex-thread-read\` with \`thread_id: "${input.thread_id}"\` to inspect the agent's output and file changes.`,
333
+ '- Use `codex-turn-start` to send follow-up instructions.',
334
+ ].join('\n');
335
+ }
336
+
337
+ return [
338
+ `**Turn started -- agent working**`,
339
+ ...header,
340
+ '',
341
+ '**What to do next:**',
342
+ `- Use \`codex-wait\` with \`operation_id: "${bridged.operationId}"\` to block until the agent finishes or asks for approval.`,
343
+ '- Check `codex-request-list` for any pending approvals.',
344
+ '- For parallel work, start turns on other threads now.',
345
+ ].join('\n');
295
346
  }
296
347
 
297
348
  private async handleTurnSteer(input: TurnSteerInput): Promise<string> {
@@ -303,18 +354,30 @@ export class CodexWorkerApp {
303
354
  const bridged = await this.runtime.requestWithBridge('turn/steer', params, {
304
355
  threadId: input.thread_id,
305
356
  });
306
- return JSON.stringify({
307
- method: 'turn/steer',
308
- ...bridged,
309
- }, null, 2);
357
+
358
+ return [
359
+ `**Turn steered**`,
360
+ `thread_id: \`${input.thread_id}\``,
361
+ `operation_id: \`${bridged.operationId}\``,
362
+ '',
363
+ 'The agent is adjusting course with your new instructions.',
364
+ `Use \`codex-wait\` with \`operation_id: "${bridged.operationId}"\` to block until it finishes.`,
365
+ ].join('\n');
310
366
  }
311
367
 
312
368
  private async handleTurnInterrupt(input: TurnInterruptInput): Promise<string> {
313
- const response = await this.runtime.request('turn/interrupt', {
369
+ await this.runtime.request('turn/interrupt', {
314
370
  threadId: input.thread_id,
315
371
  turnId: input.turn_id,
316
372
  });
317
- return JSON.stringify(response, null, 2);
373
+
374
+ return [
375
+ `**Turn interrupted**`,
376
+ `thread_id: \`${input.thread_id}\``,
377
+ `turn_id: \`${input.turn_id}\``,
378
+ '',
379
+ 'The turn has been stopped. Use `codex-turn-start` to begin a new turn with corrected instructions.',
380
+ ].join('\n');
318
381
  }
319
382
 
320
383
  private async handleRequestRespond(input: RequestRespondInput): Promise<string> {
@@ -325,12 +388,16 @@ export class CodexWorkerApp {
325
388
 
326
389
  const payload = this.buildServerRequestPayload(pending.method, input);
327
390
  await this.runtime.respondToServerRequest(input.request_id, payload);
328
- return JSON.stringify({
329
- request_id: input.request_id,
330
- method: pending.method,
331
- payload,
332
- status: 'responded',
333
- }, null, 2);
391
+
392
+ return [
393
+ `**Request responded**`,
394
+ `request_id: \`${String(input.request_id)}\``,
395
+ `method: \`${pending.method}\``,
396
+ '',
397
+ '**What to do next:**',
398
+ '- The agent will continue working. Use `codex-wait` to block until it finishes or needs another approval.',
399
+ '- Check `codex-request-list` for any remaining pending requests.',
400
+ ].join('\n');
334
401
  }
335
402
 
336
403
  private async handleWait(input: WaitInput): Promise<string> {
@@ -339,7 +406,41 @@ export class CodexWorkerApp {
339
406
 
340
407
  if (input.operation_id) {
341
408
  const operation = await this.runtime.waitForOperation(input.operation_id, timeoutMs, pollIntervalMs);
342
- return JSON.stringify(operation, null, 2);
409
+ const threadId = operation.threadId ?? 'unknown';
410
+
411
+ if (operation.status === 'failed') {
412
+ return [
413
+ `**Operation failed**`,
414
+ `operation_id: \`${operation.operationId}\``,
415
+ `error: ${operation.error ?? 'unknown error'}`,
416
+ '',
417
+ '**What to do next:**',
418
+ `- Use \`codex-thread-read\` with \`thread_id: "${threadId}"\` to inspect what happened.`,
419
+ '- Fix the issue and use `codex-turn-start` to retry.',
420
+ ].join('\n');
421
+ }
422
+
423
+ if (operation.pendingRequestIds.length > 0) {
424
+ return [
425
+ `**Operation paused -- approval needed**`,
426
+ `operation_id: \`${operation.operationId}\``,
427
+ `pending_requests: ${operation.pendingRequestIds.length}`,
428
+ '',
429
+ '**ACTION REQUIRED:**',
430
+ 'Use `codex-request-list` to see what the agent needs, then `codex-request-respond` to approve.',
431
+ `After approving, call \`codex-wait\` again with \`operation_id: "${operation.operationId}"\`.`,
432
+ ].join('\n');
433
+ }
434
+
435
+ return [
436
+ `**Operation completed**`,
437
+ `operation_id: \`${operation.operationId}\``,
438
+ `thread_id: \`${threadId}\``,
439
+ '',
440
+ '**What to do next:**',
441
+ `- Use \`codex-thread-read\` with \`thread_id: "${threadId}"\` to inspect the agent's output.`,
442
+ '- Use `codex-turn-start` to send follow-up instructions.',
443
+ ].join('\n');
343
444
  }
344
445
 
345
446
  if (input.thread_id) {
@@ -355,21 +456,22 @@ export class CodexWorkerApp {
355
456
  };
356
457
  const statusType = response.thread?.status?.type;
357
458
  if (statusType !== 'active') {
358
- return JSON.stringify({
359
- thread_id: input.thread_id,
360
- status: statusType ?? 'unknown',
361
- thread: response.thread ?? null,
362
- }, null, 2);
459
+ return [
460
+ `**Thread idle**`,
461
+ `thread_id: \`${input.thread_id}\``,
462
+ `status: \`${statusType ?? 'unknown'}\``,
463
+ '',
464
+ '**What to do next:**',
465
+ `- Use \`codex-thread-read\` with \`thread_id: "${input.thread_id}"\` to review the agent's work.`,
466
+ '- Use `codex-turn-start` to send another task.',
467
+ ].join('\n');
363
468
  }
364
469
  await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
365
470
  }
366
471
  throw new Error(`Timed out waiting for thread ${input.thread_id}`);
367
472
  }
368
473
 
369
- return JSON.stringify({
370
- requests: this.runtime.getPendingServerRequests(false),
371
- operations: [],
372
- }, null, 2);
474
+ return renderRequestListMarkdown(this.runtime.getPendingServerRequests(false));
373
475
  }
374
476
 
375
477
  private buildServerRequestPayload(method: string, input: RequestRespondInput): unknown {
@@ -0,0 +1,136 @@
1
+ import type { PendingServerRequest, RuntimeOperation } from '../types/codex.js';
2
+
3
+ interface ThreadData {
4
+ id: string | undefined;
5
+ status: { type?: string } | undefined;
6
+ model: string | undefined;
7
+ createdAt: string | undefined;
8
+ }
9
+
10
+ function extractThread(raw: unknown): ThreadData {
11
+ const obj = (raw ?? {}) as Record<string, unknown>;
12
+ const thread = (obj.thread ?? obj) as Record<string, unknown>;
13
+ return {
14
+ id: typeof thread.id === 'string' ? thread.id : undefined,
15
+ status: thread.status as ThreadData['status'],
16
+ model: typeof thread.model === 'string' ? thread.model : undefined,
17
+ createdAt: typeof thread.createdAt === 'string' ? thread.createdAt : undefined,
18
+ };
19
+ }
20
+
21
+ function requestsForThread(requests: PendingServerRequest[], threadId: string | undefined): PendingServerRequest[] {
22
+ if (!threadId) return requests;
23
+ return requests.filter((r) => {
24
+ const params = r.params as Record<string, unknown> | undefined;
25
+ return params?.threadId === threadId;
26
+ });
27
+ }
28
+
29
+ export function renderThreadMarkdown(
30
+ raw: unknown,
31
+ operations: RuntimeOperation[],
32
+ pendingRequests: PendingServerRequest[],
33
+ ): string {
34
+ const thread = extractThread(raw);
35
+ const threadId = thread.id ?? 'unknown';
36
+ const statusType = thread.status?.type ?? 'unknown';
37
+ const threadRequests = requestsForThread(pendingRequests, thread.id);
38
+
39
+ const lines: string[] = [
40
+ `# Thread: ${threadId}`,
41
+ '',
42
+ '| Field | Value |',
43
+ '|---|---|',
44
+ `| **Status** | \`${statusType}\` |`,
45
+ ...(thread.model ? [`| **Model** | \`${thread.model}\` |`] : []),
46
+ ...(thread.createdAt ? [`| **Created** | ${thread.createdAt} |`] : []),
47
+ '',
48
+ ];
49
+
50
+ if (operations.length > 0) {
51
+ lines.push('## Operations', '');
52
+ for (const op of operations) {
53
+ const time = op.completedAt ?? op.startedAt;
54
+ lines.push(`- \`${op.operationId}\` [${op.status}] ${op.method} -- ${time}`);
55
+ }
56
+ lines.push('');
57
+ }
58
+
59
+ if (threadRequests.length > 0) {
60
+ lines.push(`## Pending Requests (${threadRequests.length})`, '');
61
+ for (const req of threadRequests) {
62
+ lines.push(`### ${String(req.id)} (${req.method})`);
63
+ lines.push(`Use \`codex-request-respond\` with \`request_id: "${String(req.id)}"\` and \`decision: "accept"\`.`);
64
+ lines.push('');
65
+ }
66
+ }
67
+
68
+ lines.push('## What to do next', '');
69
+
70
+ if (threadRequests.length > 0) {
71
+ lines.push(`**ACTION REQUIRED** -- ${threadRequests.length} pending request(s) need your response. Use \`codex-request-list\` to see details, then \`codex-request-respond\` to approve or decline.`);
72
+ } else if (statusType === 'active') {
73
+ lines.push(`The agent is still working. Use \`codex-wait\` with \`thread_id: "${threadId}"\` to block until it finishes or needs approval.`);
74
+ } else if (statusType === 'idle' || statusType === 'completed') {
75
+ lines.push(`Thread is idle. Use \`codex-turn-start\` to send a new task, or \`codex-thread-read\` with \`include_turns: true\` for full conversation history.`);
76
+ } else {
77
+ lines.push(`Use \`codex-thread-resume\` to reload this thread before starting a turn.`);
78
+ }
79
+
80
+ return lines.join('\n');
81
+ }
82
+
83
+ export function renderThreadListMarkdown(
84
+ rawThreads: unknown[],
85
+ pendingRequests: PendingServerRequest[],
86
+ ): string {
87
+ const threads = rawThreads.map(extractThread);
88
+
89
+ const lines: string[] = [
90
+ `# Threads (${threads.length} total)`,
91
+ '',
92
+ '| ID | Status |',
93
+ '|---|---|',
94
+ ...threads.map((t) => `| ${t.id ?? 'unknown'} | ${t.status?.type ?? 'unknown'} |`),
95
+ ];
96
+
97
+ const pending = pendingRequests.filter((r) => r.status === 'pending');
98
+ if (pending.length > 0) {
99
+ lines.push('', `## Pending Requests (${pending.length})`, '');
100
+ for (const req of pending) {
101
+ const params = req.params as Record<string, unknown> | undefined;
102
+ const tid = typeof params?.threadId === 'string' ? params.threadId : '?';
103
+ lines.push(`- ${tid} / ${String(req.id)}: ${req.method}`);
104
+ }
105
+ }
106
+
107
+ return lines.join('\n');
108
+ }
109
+
110
+ export function renderRequestListMarkdown(
111
+ requests: PendingServerRequest[],
112
+ ): string {
113
+ const pending = requests.filter((r) => r.status === 'pending');
114
+
115
+ if (pending.length === 0) {
116
+ return [
117
+ '**No pending requests**',
118
+ '',
119
+ 'All agents are running without needing approval.',
120
+ ].join('\n');
121
+ }
122
+
123
+ const lines: string[] = [
124
+ `# Pending Requests (${pending.length})`,
125
+ '',
126
+ '| Request ID | Method | Created |',
127
+ '|---|---|---|',
128
+ ...pending.map((r) => `| ${String(r.id)} | ${r.method} | ${r.createdAt} |`),
129
+ '',
130
+ '## How to respond',
131
+ '',
132
+ 'Use `codex-request-respond` with the `request_id` and `decision: "accept"` to approve, or `decision: "decline"` to reject.',
133
+ ];
134
+
135
+ return lines.join('\n');
136
+ }
@@ -0,0 +1,53 @@
1
+ import type { CodexRuntime } from '../services/codex-runtime.js';
2
+
3
+ export function buildThreadBanner(runtime: CodexRuntime): string {
4
+ const threadIds = runtime.listKnownThreadIds();
5
+ if (threadIds.length === 0) {
6
+ return '';
7
+ }
8
+
9
+ const operations = runtime.getAllOperations();
10
+ const requests = runtime.getPendingServerRequests(false);
11
+ const active = operations.filter((op) => op.status === 'running').length;
12
+ const pending = requests.length;
13
+
14
+ const lines = [
15
+ '---',
16
+ `AGENT STATUS: ${active} active | ${pending} pending requests`,
17
+ ];
18
+
19
+ for (const threadId of threadIds.slice(-3)) {
20
+ const threadOps = operations.filter((op) => op.threadId === threadId);
21
+ const threadReqs = requests.filter((r) => {
22
+ const params = r.params as Record<string, unknown> | undefined;
23
+ return params?.threadId === threadId;
24
+ });
25
+ const status = threadOps.some((op) => op.status === 'running') ? 'active' : 'idle';
26
+ const reqNote = threadReqs.length > 0 ? ` -- ${threadReqs.length} pending requests` : '';
27
+ lines.push(`- ${threadId} [${status}]${reqNote}`);
28
+ }
29
+
30
+ lines.push('Read codex://threads for full details.');
31
+ return lines.join('\n');
32
+ }
33
+
34
+ export function buildRequestBanner(runtime: CodexRuntime): string {
35
+ const requests = runtime.getPendingServerRequests(false);
36
+ if (requests.length === 0) {
37
+ return '';
38
+ }
39
+
40
+ const lines = [
41
+ '---',
42
+ `ACTION REQUIRED -- ${requests.length} request(s) waiting for your response:`,
43
+ ];
44
+
45
+ for (const req of requests.slice(0, 5)) {
46
+ const params = req.params as Record<string, unknown> | undefined;
47
+ const threadId = typeof params?.threadId === 'string' ? params.threadId : '?';
48
+ lines.push(`- ${String(req.id)} (${threadId}): ${req.method}`);
49
+ }
50
+
51
+ lines.push('Use codex-request-respond { "request_id": "<id>", "decision": "accept" }');
52
+ return lines.join('\n');
53
+ }