remote-codex 0.11.10 → 0.11.12

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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Remote Codex Supervisor</title>
7
- <script type="module" crossorigin src="/assets/index-M5xKhovw.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-Dsq8QmDr.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="/assets/react-vendor-CgLzZcV4.js">
9
9
  <link rel="modulepreload" crossorigin href="/assets/ui-vendor-CeKGesq3.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/graph-vendor-DVPtkh3h.js">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-codex",
3
- "version": "0.11.10",
3
+ "version": "0.11.12",
4
4
  "description": "Local web supervisor for Codex workspaces and threads.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -269,6 +269,7 @@ export interface SendAgentInputInput {
269
269
  providerSessionId: string;
270
270
  providerTurnId: string;
271
271
  prompt: string;
272
+ workspacePath?: string | null;
272
273
  }
273
274
 
274
275
  export interface InterruptAgentTurnInput {
@@ -197,6 +197,58 @@ describe('CodexAppServerManager', () => {
197
197
  await manager.stop();
198
198
  });
199
199
 
200
+ it('forwards structured image input when starting a turn', async () => {
201
+ const expectedInput = [
202
+ { type: 'text', text: 'Inspect this ', text_elements: [] },
203
+ { type: 'localImage', path: '/tmp/workspace/photo.png' },
204
+ { type: 'text', text: ' and summarize.', text_elements: [] },
205
+ ];
206
+ const script = [
207
+ "const readline=require('node:readline');",
208
+ "const rl=readline.createInterface({input:process.stdin,crlfDelay:Infinity});",
209
+ `const expectedInput=${JSON.stringify(expectedInput)};`,
210
+ "rl.on('line',(line)=>{",
211
+ " const msg=JSON.parse(line);",
212
+ " if(msg.method==='initialize'){",
213
+ " process.stdout.write(JSON.stringify({id:msg.id,result:{userAgent:'fake',codexHome:'/tmp',platformFamily:'unix',platformOs:'linux'}})+'\\n');",
214
+ " } else if(msg.method==='turn/start'){",
215
+ " if(JSON.stringify(msg.params?.input)===JSON.stringify(expectedInput)){",
216
+ " process.stdout.write(JSON.stringify({id:msg.id,result:{turn:{id:'turn-1',status:'completed',items:[]}}})+'\\n');",
217
+ " } else {",
218
+ " process.stdout.write(JSON.stringify({id:msg.id,error:{code:-32600,message:'bad start input',data:msg.params}})+'\\n');",
219
+ " }",
220
+ " }",
221
+ "});"
222
+ ].join('');
223
+
224
+ const manager = new CodexAppServerManager({
225
+ command: process.execPath,
226
+ startupTimeoutMs: 1000,
227
+ clientInfo: {
228
+ name: 'test',
229
+ title: 'test',
230
+ version: '0.1.0'
231
+ },
232
+ spawnProcess: (command) => {
233
+ return spawn(command, ['-e', script], { stdio: 'pipe' });
234
+ }
235
+ });
236
+
237
+ await manager.start();
238
+ const turn = await manager.startTurn({
239
+ threadId: 'thread-1',
240
+ prompt: 'Inspect this [PHOTO photo.png] and summarize.',
241
+ input: expectedInput,
242
+ });
243
+
244
+ expect(turn).toMatchObject({
245
+ id: 'turn-1',
246
+ status: 'completed',
247
+ });
248
+
249
+ await manager.stop();
250
+ });
251
+
200
252
  it('writes hook trust state through config batch writes', async () => {
201
253
  const requests: any[] = [];
202
254
  const script = [
@@ -72,6 +72,16 @@ function mapTurn(record: any): CodexTurnRecord {
72
72
  };
73
73
  }
74
74
 
75
+ function textOnlyUserInput(prompt: string): NonNullable<TurnStartInput['input']> {
76
+ return [
77
+ {
78
+ type: 'text',
79
+ text: prompt,
80
+ text_elements: []
81
+ }
82
+ ];
83
+ }
84
+
75
85
  function mapModel(record: any): CodexModelRecord {
76
86
  return {
77
87
  id: record.id,
@@ -452,13 +462,7 @@ export class CodexAppServerManager extends EventEmitter {
452
462
  await this.ensureReady();
453
463
  const response = await this.client!.request<{ turn: any }>('turn/start', {
454
464
  threadId: input.threadId,
455
- input: [
456
- {
457
- type: 'text',
458
- text: input.prompt,
459
- text_elements: []
460
- }
461
- ],
465
+ input: input.input ?? textOnlyUserInput(input.prompt),
462
466
  model: input.model ?? null,
463
467
  serviceTier:
464
468
  input.serviceTier === undefined ? undefined : input.serviceTier,
@@ -483,13 +487,7 @@ export class CodexAppServerManager extends EventEmitter {
483
487
  const response = await this.client!.request<{ turn?: any }>('turn/steer', {
484
488
  threadId: input.threadId,
485
489
  expectedTurnId: input.turnId,
486
- input: [
487
- {
488
- type: 'text',
489
- text: input.prompt,
490
- text_elements: []
491
- }
492
- ]
490
+ input: input.input ?? textOnlyUserInput(input.prompt)
493
491
  });
494
492
  return response.turn ? mapTurn(response.turn) : null;
495
493
  }
@@ -84,6 +84,39 @@ describe('codex history item persistence policy', () => {
84
84
  });
85
85
  });
86
86
 
87
+ it('preserves per-item timestamps when mapping Codex turn history', () => {
88
+ const turn = codexTurnToAgentTurn({
89
+ id: 'turn-1',
90
+ status: 'completed',
91
+ error: null,
92
+ items: [
93
+ {
94
+ id: 'user-1',
95
+ type: 'userMessage',
96
+ text: 'start',
97
+ createdAt: '2026-06-13T22:00:01.123Z',
98
+ },
99
+ {
100
+ id: 'agent-1',
101
+ type: 'agentMessage',
102
+ text: 'done',
103
+ completedAt: '2026-06-13T22:00:08.456Z',
104
+ },
105
+ ],
106
+ });
107
+
108
+ expect(turn.items).toMatchObject([
109
+ {
110
+ id: 'user-1',
111
+ createdAt: '2026-06-13T22:00:01.123Z',
112
+ },
113
+ {
114
+ id: 'agent-1',
115
+ createdAt: '2026-06-13T22:00:08.456Z',
116
+ },
117
+ ]);
118
+ });
119
+
87
120
  it('keeps MCP tool result text extractable for plugin artifacts', () => {
88
121
  const artifactText = [
89
122
  'Created a 3D molecule artifact for Methane.',
@@ -46,6 +46,51 @@ function stringOrNull(value: unknown) {
46
46
  return typeof value === 'string' && value.trim() ? value.trim() : null;
47
47
  }
48
48
 
49
+ function isoTimestampOrNull(value: unknown) {
50
+ if (typeof value === 'number' && Number.isFinite(value)) {
51
+ const epochMs = value < 10_000_000_000 ? value * 1000 : value;
52
+ return new Date(epochMs).toISOString();
53
+ }
54
+
55
+ const text = stringOrNull(value);
56
+ if (!text) {
57
+ return null;
58
+ }
59
+
60
+ const parsed = Date.parse(text);
61
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
62
+ }
63
+
64
+ function codexItemCreatedAt(item: CodexTurnItem) {
65
+ const candidates = [
66
+ item.createdAt,
67
+ item.created_at,
68
+ item.startedAt,
69
+ item.started_at,
70
+ item.completedAt,
71
+ item.completed_at,
72
+ ];
73
+
74
+ for (const candidate of candidates) {
75
+ const timestamp = isoTimestampOrNull(candidate);
76
+ if (timestamp) {
77
+ return timestamp;
78
+ }
79
+ }
80
+
81
+ return parseUuidV7Timestamp(item.id);
82
+ }
83
+
84
+ function withCodexItemTimestamp<T extends ThreadHistoryItemDto>(
85
+ item: CodexTurnItem,
86
+ historyItem: T,
87
+ ): T {
88
+ return {
89
+ ...historyItem,
90
+ createdAt: historyItem.createdAt ?? codexItemCreatedAt(item),
91
+ };
92
+ }
93
+
49
94
  function numberOrNull(value: unknown) {
50
95
  if (typeof value === 'number' && Number.isFinite(value)) {
51
96
  return value;
@@ -1319,7 +1364,7 @@ export function liveCodexItemToHistoryItem(
1319
1364
  item: CodexTurnItem,
1320
1365
  phase: 'started' | 'completed',
1321
1366
  ): ThreadHistoryItemDto | null {
1322
- const historyItem = itemToHistoryItem(item);
1367
+ const historyItem = withCodexItemTimestamp(item, itemToHistoryItem(item));
1323
1368
 
1324
1369
  if (
1325
1370
  historyItem.kind !== 'commandExecution' &&
@@ -1365,7 +1410,9 @@ export function codexTurnToAgentTurn(turn: CodexTurnRecord): AgentTurn {
1365
1410
  rawTurnId: turn.id,
1366
1411
  status: turn.status,
1367
1412
  error: turn.error,
1368
- items: turn.items.map((item) => itemToHistoryItem(item)),
1413
+ items: turn.items.map((item) =>
1414
+ withCodexItemTimestamp(item, itemToHistoryItem(item)),
1415
+ ),
1369
1416
  rawTurn: turn,
1370
1417
  };
1371
1418
  }
@@ -0,0 +1,76 @@
1
+ import { EventEmitter } from 'node:events';
2
+
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { CodexRuntimeAdapter } from './runtimeAdapter';
6
+ import type { TurnStartInput } from './types';
7
+
8
+ class FakeCodexManager extends EventEmitter {
9
+ startTurnInput: TurnStartInput | null = null;
10
+
11
+ getStatus() {
12
+ return {
13
+ state: 'ready' as const,
14
+ transport: 'stdio' as const,
15
+ lastStartedAt: null,
16
+ lastError: null,
17
+ restartCount: 0,
18
+ };
19
+ }
20
+
21
+ async startTurn(input: TurnStartInput) {
22
+ this.startTurnInput = input;
23
+ return {
24
+ id: 'turn-1',
25
+ status: 'completed' as const,
26
+ error: null,
27
+ items: [],
28
+ };
29
+ }
30
+ }
31
+
32
+ describe('CodexRuntimeAdapter', () => {
33
+ it('converts prompt photo tokens into structured local image input', async () => {
34
+ const manager = new FakeCodexManager();
35
+ const adapter = new CodexRuntimeAdapter(manager as never);
36
+
37
+ await adapter.startTurn({
38
+ providerSessionId: 'thread-1',
39
+ prompt: 'Inspect this [PHOTO ./.temp/threads/thread-1/photo.png] then summarize.',
40
+ workspacePath: '/tmp/workspace',
41
+ });
42
+
43
+ expect(manager.startTurnInput).toMatchObject({
44
+ threadId: 'thread-1',
45
+ prompt: 'Inspect this [PHOTO ./.temp/threads/thread-1/photo.png] then summarize.',
46
+ input: [
47
+ { type: 'text', text: 'Inspect this ', text_elements: [] },
48
+ {
49
+ type: 'localImage',
50
+ path: '/tmp/workspace/.temp/threads/thread-1/photo.png',
51
+ },
52
+ { type: 'text', text: ' then summarize.', text_elements: [] },
53
+ ],
54
+ });
55
+ });
56
+
57
+ it('keeps mobile photo extensions as structured local image input', async () => {
58
+ const manager = new FakeCodexManager();
59
+ const adapter = new CodexRuntimeAdapter(manager as never);
60
+
61
+ await adapter.startTurn({
62
+ providerSessionId: 'thread-1',
63
+ prompt: 'Inspect [PHOTO ./.temp/threads/thread-1/photo.heic].',
64
+ workspacePath: '/tmp/workspace',
65
+ });
66
+
67
+ expect(manager.startTurnInput?.input).toEqual([
68
+ { type: 'text', text: 'Inspect ', text_elements: [] },
69
+ {
70
+ type: 'localImage',
71
+ path: '/tmp/workspace/.temp/threads/thread-1/photo.heic',
72
+ },
73
+ { type: 'text', text: '.', text_elements: [] },
74
+ ]);
75
+ });
76
+ });
@@ -1,4 +1,5 @@
1
1
  import { EventEmitter } from 'node:events';
2
+ import path from 'node:path';
2
3
 
3
4
  import type {
4
5
  AgentModel,
@@ -57,6 +58,62 @@ import {
57
58
  supportsFastMode,
58
59
  } from './index';
59
60
 
61
+ const promptPhotoTokenPattern = /\[PHOTO\s+([^\]]+)\]/g;
62
+
63
+ function resolvePromptAssetPath(assetPath: string, cwd: string | null | undefined) {
64
+ if (!assetPath) {
65
+ return null;
66
+ }
67
+ if (path.isAbsolute(assetPath)) {
68
+ return path.normalize(assetPath);
69
+ }
70
+ if (!cwd) {
71
+ return null;
72
+ }
73
+ return path.resolve(cwd, assetPath);
74
+ }
75
+
76
+ function codexUserInputFromPrompt(
77
+ prompt: string,
78
+ cwd: string | null | undefined,
79
+ ): TurnStartInput['input'] | undefined {
80
+ const matches = [...prompt.matchAll(promptPhotoTokenPattern)];
81
+ if (matches.length === 0) {
82
+ return undefined;
83
+ }
84
+
85
+ const input: NonNullable<TurnStartInput['input']> = [];
86
+ let cursor = 0;
87
+ let includedImage = false;
88
+
89
+ for (const match of matches) {
90
+ const token = match[0];
91
+ const assetPath = match[1]?.trim() ?? '';
92
+ const start = match.index ?? 0;
93
+ const precedingText = prompt.slice(cursor, start);
94
+ if (precedingText) {
95
+ input.push({ type: 'text', text: precedingText, text_elements: [] });
96
+ }
97
+
98
+ const resolvedPath = resolvePromptAssetPath(assetPath, cwd);
99
+ if (resolvedPath) {
100
+ input.push({ type: 'localImage', path: resolvedPath });
101
+ includedImage = true;
102
+ } else {
103
+ input.push({ type: 'text', text: token, text_elements: [] });
104
+ }
105
+
106
+ cursor = start + token.length;
107
+ }
108
+
109
+ const trailingText = prompt.slice(cursor);
110
+ if (trailingText) {
111
+ input.push({ type: 'text', text: trailingText, text_elements: [] });
112
+ }
113
+
114
+ return includedImage ? input : undefined;
115
+ }
116
+
60
117
  export const codexCapabilities: AgentProviderCapabilities = {
61
118
  sessions: {
62
119
  list: true,
@@ -661,6 +718,13 @@ export class CodexRuntimeAdapter extends EventEmitter implements AgentRuntime {
661
718
  threadId: input.providerSessionId,
662
719
  prompt: input.prompt,
663
720
  };
721
+ const structuredInput = codexUserInputFromPrompt(
722
+ input.prompt,
723
+ input.workspacePath,
724
+ );
725
+ if (structuredInput) {
726
+ turnInput.input = structuredInput;
727
+ }
664
728
  if (input.developerInstructions !== undefined) {
665
729
  turnInput.developerInstructions = input.developerInstructions;
666
730
  }
@@ -689,11 +753,17 @@ export class CodexRuntimeAdapter extends EventEmitter implements AgentRuntime {
689
753
  }
690
754
 
691
755
  async sendInput(input: SendAgentInputInput): Promise<AgentTurn | null> {
692
- const turn = await codexRuntimeCall(() => this.manager.steerTurn({
756
+ const structuredInput = codexUserInputFromPrompt(
757
+ input.prompt,
758
+ input.workspacePath,
759
+ );
760
+ const steerInput = {
693
761
  threadId: input.providerSessionId,
694
762
  turnId: input.providerTurnId,
695
763
  prompt: input.prompt,
696
- }));
764
+ ...(structuredInput ? { input: structuredInput } : {}),
765
+ };
766
+ const turn = await codexRuntimeCall(() => this.manager.steerTurn(steerInput));
697
767
  return turn ? mapTurn(turn) : null;
698
768
  }
699
769
 
@@ -318,6 +318,7 @@ export interface ThreadRollbackInput {
318
318
  export interface TurnStartInput {
319
319
  threadId: string;
320
320
  prompt: string;
321
+ input?: CodexUserInput[];
321
322
  developerInstructions?: string | null;
322
323
  model?: string | null;
323
324
  effort?: ReasoningEffort | null;
@@ -330,8 +331,20 @@ export interface TurnSteerInput {
330
331
  threadId: string;
331
332
  turnId: string;
332
333
  prompt: string;
334
+ input?: CodexUserInput[];
333
335
  }
334
336
 
337
+ export type CodexUserInput =
338
+ | {
339
+ type: 'text';
340
+ text: string;
341
+ text_elements: [];
342
+ }
343
+ | {
344
+ type: 'localImage';
345
+ path: string;
346
+ };
347
+
335
348
  export interface ThreadCompactInput {
336
349
  threadId: string;
337
350
  }
@@ -218,6 +218,7 @@ export interface RelayHttpResponsePayload {
218
218
  statusCode: number;
219
219
  headers: Record<string, string>;
220
220
  body: string;
221
+ bodyEncoding?: 'utf8' | 'base64';
221
222
  }
222
223
 
223
224
  export interface AgentRuntimeStatusDto {
@@ -563,6 +564,7 @@ export interface ThreadDto {
563
564
 
564
565
  export interface ThreadHistoryItemDto {
565
566
  id: string;
567
+ createdAt?: string | null;
566
568
  kind:
567
569
  | 'userMessage'
568
570
  | 'agentMessage'