remote-codex 0.11.52 → 0.11.53

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.
@@ -10,7 +10,7 @@
10
10
  <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
11
11
  <link rel="manifest" href="/site.webmanifest" />
12
12
  <title>Remote Codex</title>
13
- <script type="module" crossorigin src="/assets/index-BBq6mr8o.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-Bnury8uP.js"></script>
14
14
  <link rel="modulepreload" crossorigin href="/assets/react-vendor-Dfg_6BLf.js">
15
15
  <link rel="modulepreload" crossorigin href="/assets/ui-vendor-CuR8GHb0.js">
16
16
  <link rel="modulepreload" crossorigin href="/assets/graph-vendor-DVQUpZ8C.js">
@@ -238,7 +238,7 @@ async function runRelaySupervisor() {
238
238
  return;
239
239
  }
240
240
  if (action === 'start' && shouldStartRelaySupervisorInTmux()) {
241
- startRelaySupervisorTmux();
241
+ await startRelaySupervisorTmux();
242
242
  return;
243
243
  }
244
244
  runRelaySupervisorForeground();
@@ -677,7 +677,7 @@ function shouldStartRelaySupervisorInTmux() {
677
677
  return commandExists('tmux');
678
678
  }
679
679
 
680
- function startRelaySupervisorTmux() {
680
+ async function startRelaySupervisorTmux() {
681
681
  persistRelaySupervisorRuntimeConfig();
682
682
  if (tmuxSessionExists(relaySupervisorTmuxSession)) {
683
683
  console.log(`remote-codex relay-supervisor is already running in tmux session: ${relaySupervisorTmuxSession}`);
@@ -702,6 +702,17 @@ function startRelaySupervisorTmux() {
702
702
  return;
703
703
  }
704
704
 
705
+ const startupDeadline = Date.now() + 1_000;
706
+ while (Date.now() < startupDeadline) {
707
+ if (!tmuxSessionExists(relaySupervisorTmuxSession)) {
708
+ console.error('relay-supervisor exited immediately after tmux launched it.');
709
+ console.error('Restarting in foreground to show the startup error.');
710
+ runRelaySupervisorForeground();
711
+ return;
712
+ }
713
+ await sleep(100);
714
+ }
715
+
705
716
  console.log(`Started remote-codex relay-supervisor in tmux session: ${relaySupervisorTmuxSession}`);
706
717
  printRelaySupervisorTmuxCommands();
707
718
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-codex",
3
- "version": "0.11.52",
3
+ "version": "0.11.53",
4
4
  "description": "Local web supervisor for Codex workspaces and threads.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -102,6 +102,11 @@ describe('codex history item persistence policy', () => {
102
102
  text: 'done',
103
103
  completedAt: '2026-06-13T22:00:08.456Z',
104
104
  },
105
+ {
106
+ id: 'msg_019d70d5-9dc8-7000-8000-000000000000',
107
+ type: 'agentMessage',
108
+ text: 'inferred from id',
109
+ },
105
110
  ],
106
111
  });
107
112
 
@@ -114,6 +119,10 @@ describe('codex history item persistence policy', () => {
114
119
  id: 'agent-1',
115
120
  createdAt: '2026-06-13T22:00:08.456Z',
116
121
  },
122
+ {
123
+ id: 'msg_019d70d5-9dc8-7000-8000-000000000000',
124
+ createdAt: '2026-04-09T06:02:21.000Z',
125
+ },
117
126
  ]);
118
127
  });
119
128
 
@@ -25,17 +25,29 @@ interface WebSearchSourceRecord {
25
25
  }
26
26
 
27
27
  export function parseUuidV7Timestamp(id: string): string | null {
28
- const normalized = id.replace(/-/g, '');
29
- if (!/^[0-9a-f]{32}$/i.test(normalized) || normalized[12]?.toLowerCase() !== '7') {
30
- return null;
31
- }
28
+ const candidates = [
29
+ id,
30
+ ...(id.match(
31
+ /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-f]{32}/gi,
32
+ ) ?? []),
33
+ ];
32
34
 
33
- const millis = Number.parseInt(normalized.slice(0, 12), 16);
34
- if (!Number.isFinite(millis)) {
35
- return null;
35
+ for (const candidate of candidates) {
36
+ const normalized = candidate.replace(/-/g, '');
37
+ if (
38
+ !/^[0-9a-f]{32}$/i.test(normalized) ||
39
+ normalized[12]?.toLowerCase() !== '7'
40
+ ) {
41
+ continue;
42
+ }
43
+
44
+ const millis = Number.parseInt(normalized.slice(0, 12), 16);
45
+ if (Number.isFinite(millis)) {
46
+ return new Date(millis).toISOString();
47
+ }
36
48
  }
37
49
 
38
- return new Date(millis).toISOString();
50
+ return null;
39
51
  }
40
52
 
41
53
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -0,0 +1,455 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import Database from 'better-sqlite3';
6
+ import { afterEach, describe, expect, it } from 'vitest';
7
+
8
+ import { LocalCodexSessionStore } from './local-session-store';
9
+
10
+ describe('LocalCodexSessionStore', () => {
11
+ const tempDirectories: string[] = [];
12
+
13
+ afterEach(async () => {
14
+ await Promise.all(
15
+ tempDirectories.splice(0).map((directory) =>
16
+ fs.rm(directory, { recursive: true, force: true }),
17
+ ),
18
+ );
19
+ });
20
+
21
+ it('recovers current response-item rollouts when the state database is corrupt', async () => {
22
+ const codexHome = await fs.mkdtemp(
23
+ path.join(os.tmpdir(), 'remote-codex-local-session-'),
24
+ );
25
+ tempDirectories.push(codexHome);
26
+ const sessionId = '01a059bf-c303-79b1-8802-f922d3a81968';
27
+ const workspacePath = path.join(codexHome, 'workspace');
28
+ const sessionsDirectory = path.join(codexHome, 'sessions', '2026', '08', '31');
29
+ await fs.mkdir(sessionsDirectory, { recursive: true });
30
+ await fs.writeFile(
31
+ path.join(codexHome, 'state_5.sqlite'),
32
+ 'this is not a valid sqlite database',
33
+ );
34
+ await fs.writeFile(
35
+ path.join(sessionsDirectory, `rollout-${sessionId}.jsonl`),
36
+ [
37
+ {
38
+ timestamp: '2026-08-31T21:36:17.000Z',
39
+ type: 'session_meta',
40
+ payload: { id: sessionId, cwd: workspacePath },
41
+ },
42
+ {
43
+ timestamp: '2026-08-31T21:36:18.000Z',
44
+ type: 'event_msg',
45
+ payload: { type: 'task_started', turn_id: 'turn-1' },
46
+ },
47
+ {
48
+ timestamp: '2026-08-31T21:36:19.000Z',
49
+ type: 'response_item',
50
+ payload: {
51
+ id: 'message-user-1',
52
+ type: 'message',
53
+ role: 'user',
54
+ content: [
55
+ { type: 'input_text', text: 'Recover this session.' },
56
+ { type: 'input_image', image_url: 'data:image/png;base64,ignored' },
57
+ ],
58
+ },
59
+ },
60
+ {
61
+ timestamp: '2026-08-31T21:36:24.000Z',
62
+ type: 'response_item',
63
+ payload: {
64
+ id: 'message-agent-1',
65
+ type: 'message',
66
+ role: 'assistant',
67
+ phase: 'final_answer',
68
+ content: [{ type: 'output_text', text: 'Recovered.' }],
69
+ },
70
+ },
71
+ {
72
+ timestamp: '2026-08-31T21:36:25.000Z',
73
+ type: 'event_msg',
74
+ payload: { type: 'task_complete', turn_id: 'turn-1' },
75
+ },
76
+ ]
77
+ .map((entry) => JSON.stringify(entry))
78
+ .join('\n'),
79
+ );
80
+
81
+ const session = await new LocalCodexSessionStore(codexHome).findSession(
82
+ sessionId,
83
+ );
84
+
85
+ expect(session).toMatchObject({
86
+ sessionId,
87
+ cwd: workspacePath,
88
+ title: 'Recover this se...',
89
+ turns: [
90
+ {
91
+ id: 'turn-1',
92
+ startedAt: '2026-08-31T21:36:18.000Z',
93
+ status: 'completed',
94
+ items: [
95
+ {
96
+ id: 'message-user-1',
97
+ kind: 'userMessage',
98
+ text: 'Recover this session.',
99
+ createdAt: '2026-08-31T21:36:19.000Z',
100
+ },
101
+ {
102
+ id: 'message-agent-1',
103
+ kind: 'agentMessage',
104
+ text: 'Recovered.',
105
+ createdAt: '2026-08-31T21:36:24.000Z',
106
+ },
107
+ ],
108
+ },
109
+ ],
110
+ });
111
+ });
112
+
113
+ it('parses legacy and response-item turns from a session that spans upgrades', async () => {
114
+ const codexHome = await fs.mkdtemp(
115
+ path.join(os.tmpdir(), 'remote-codex-mixed-session-'),
116
+ );
117
+ tempDirectories.push(codexHome);
118
+ const sessionId = '01a059bf-c303-79b1-8802-f922d3a81969';
119
+ const workspacePath = path.join(codexHome, 'workspace');
120
+ const sessionsDirectory = path.join(codexHome, 'sessions');
121
+ await fs.mkdir(sessionsDirectory, { recursive: true });
122
+ await fs.writeFile(
123
+ path.join(sessionsDirectory, `rollout-${sessionId}.jsonl`),
124
+ [
125
+ {
126
+ timestamp: '2026-08-30T00:00:00.000Z',
127
+ type: 'session_meta',
128
+ payload: { id: sessionId, cwd: workspacePath },
129
+ },
130
+ {
131
+ timestamp: '2026-08-30T00:00:01.000Z',
132
+ type: 'event_msg',
133
+ payload: { type: 'task_started', turn_id: 'legacy-turn' },
134
+ },
135
+ {
136
+ timestamp: '2026-08-30T00:00:02.000Z',
137
+ type: 'event_msg',
138
+ payload: { type: 'user_message', message: 'Legacy prompt' },
139
+ },
140
+ {
141
+ timestamp: '2026-08-30T00:00:03.000Z',
142
+ type: 'event_msg',
143
+ payload: { type: 'agent_message', message: 'Legacy reply' },
144
+ },
145
+ {
146
+ timestamp: '2026-08-30T00:00:04.000Z',
147
+ type: 'event_msg',
148
+ payload: { type: 'task_complete' },
149
+ },
150
+ {
151
+ timestamp: '2026-08-31T00:00:01.000Z',
152
+ type: 'event_msg',
153
+ payload: { type: 'task_started', turn_id: 'modern-turn' },
154
+ },
155
+ {
156
+ timestamp: '2026-08-31T00:00:02.000Z',
157
+ type: 'response_item',
158
+ payload: {
159
+ id: 'modern-user',
160
+ type: 'message',
161
+ role: 'user',
162
+ content: [{ type: 'input_text', text: 'Modern prompt' }],
163
+ },
164
+ },
165
+ {
166
+ timestamp: '2026-08-31T00:00:03.000Z',
167
+ type: 'response_item',
168
+ payload: {
169
+ id: 'modern-agent',
170
+ type: 'message',
171
+ role: 'assistant',
172
+ content: [{ type: 'output_text', text: 'Modern reply' }],
173
+ },
174
+ },
175
+ {
176
+ timestamp: '2026-08-31T00:00:04.000Z',
177
+ type: 'event_msg',
178
+ payload: { type: 'task_complete' },
179
+ },
180
+ ]
181
+ .map((entry) => JSON.stringify(entry))
182
+ .join('\n'),
183
+ );
184
+
185
+ const session = await new LocalCodexSessionStore(codexHome).findSession(
186
+ sessionId,
187
+ );
188
+
189
+ expect(session?.turns.map((turn) => turn.items.map((item) => item.text))).toEqual([
190
+ ['Legacy prompt', 'Legacy reply'],
191
+ ['Modern prompt', 'Modern reply'],
192
+ ]);
193
+ });
194
+
195
+ it('merges complete paginated tool history with newer rollout-only turns', async () => {
196
+ const codexHome = await fs.mkdtemp(
197
+ path.join(os.tmpdir(), 'remote-codex-paginated-session-'),
198
+ );
199
+ tempDirectories.push(codexHome);
200
+ const sessionId = '01a059bf-c303-79b1-8802-f922d3a81970';
201
+ const workspacePath = path.join(codexHome, 'workspace');
202
+ const sessionsDirectory = path.join(codexHome, 'sessions');
203
+ await fs.mkdir(sessionsDirectory, { recursive: true });
204
+ await fs.writeFile(
205
+ path.join(sessionsDirectory, `rollout-${sessionId}.jsonl`),
206
+ [
207
+ {
208
+ timestamp: '2026-08-31T00:00:00.000Z',
209
+ type: 'session_meta',
210
+ payload: { id: sessionId, cwd: workspacePath },
211
+ },
212
+ {
213
+ timestamp: '2026-08-31T00:00:01.000Z',
214
+ type: 'event_msg',
215
+ payload: { type: 'task_started', turn_id: 'turn-rich' },
216
+ },
217
+ {
218
+ timestamp: '2026-08-31T00:00:02.000Z',
219
+ type: 'response_item',
220
+ payload: {
221
+ id: 'user-rich',
222
+ type: 'message',
223
+ role: 'user',
224
+ content: [{ type: 'input_text', text: 'Inspect.' }],
225
+ },
226
+ },
227
+ {
228
+ timestamp: '2026-08-31T00:00:03.500Z',
229
+ type: 'response_item',
230
+ payload: {
231
+ id: 'agent-commentary',
232
+ type: 'message',
233
+ role: 'assistant',
234
+ phase: 'commentary',
235
+ content: [{ type: 'output_text', text: 'Running checks.' }],
236
+ },
237
+ },
238
+ {
239
+ timestamp: '2026-08-31T00:00:05.000Z',
240
+ type: 'response_item',
241
+ payload: {
242
+ id: 'agent-rich',
243
+ type: 'message',
244
+ role: 'assistant',
245
+ content: [{ type: 'output_text', text: 'Done.' }],
246
+ },
247
+ },
248
+ {
249
+ timestamp: '2026-08-31T00:00:06.000Z',
250
+ type: 'event_msg',
251
+ payload: { type: 'task_complete' },
252
+ },
253
+ {
254
+ timestamp: '2026-08-31T00:01:00.000Z',
255
+ type: 'event_msg',
256
+ payload: { type: 'task_started', turn_id: 'turn-new' },
257
+ },
258
+ {
259
+ timestamp: '2026-08-31T00:01:01.000Z',
260
+ type: 'response_item',
261
+ payload: {
262
+ id: 'user-new',
263
+ type: 'message',
264
+ role: 'user',
265
+ content: [{ type: 'input_text', text: 'Continue.' }],
266
+ },
267
+ },
268
+ {
269
+ timestamp: '2026-08-31T00:01:02.000Z',
270
+ type: 'event_msg',
271
+ payload: {
272
+ type: 'item_completed',
273
+ turn_id: 'turn-new',
274
+ started_at_ms: 1_788_134_462_000,
275
+ item: {
276
+ id: 'command-new',
277
+ type: 'CommandExecution',
278
+ command: 'git status --short',
279
+ aggregated_output: '',
280
+ status: 'completed',
281
+ exit_code: 0,
282
+ },
283
+ },
284
+ },
285
+ ].map((entry) => JSON.stringify(entry)).join('\n'),
286
+ );
287
+
288
+ const sqlite = new Database(path.join(codexHome, 'thread_history_1.sqlite'));
289
+ sqlite.exec(`
290
+ CREATE TABLE thread_turns (
291
+ thread_id TEXT NOT NULL,
292
+ turn_id TEXT NOT NULL,
293
+ rollout_ordinal INTEGER NOT NULL,
294
+ status TEXT NOT NULL,
295
+ error_json TEXT,
296
+ started_at INTEGER,
297
+ PRIMARY KEY (thread_id, turn_id)
298
+ );
299
+ CREATE TABLE thread_items (
300
+ thread_id TEXT NOT NULL,
301
+ turn_id TEXT NOT NULL,
302
+ item_id TEXT NOT NULL,
303
+ rollout_ordinal INTEGER NOT NULL,
304
+ created_at_ms INTEGER NOT NULL,
305
+ item_type TEXT NOT NULL,
306
+ item_json TEXT NOT NULL,
307
+ PRIMARY KEY (thread_id, turn_id, item_id)
308
+ );
309
+ `);
310
+ sqlite.prepare(
311
+ 'INSERT INTO thread_turns VALUES (?, ?, ?, ?, ?, ?)',
312
+ ).run(sessionId, 'turn-rich', 1, 'completed', null, 1_788_134_401);
313
+ const insertItem = sqlite.prepare(
314
+ 'INSERT INTO thread_items VALUES (?, ?, ?, ?, ?, ?, ?)',
315
+ );
316
+ const rawItems = [
317
+ {
318
+ ordinal: 2,
319
+ createdAtMs: 1_788_134_402_000,
320
+ type: 'userMessage',
321
+ item: {
322
+ id: 'user-rich',
323
+ type: 'userMessage',
324
+ content: [{ type: 'text', text: 'Inspect.' }],
325
+ },
326
+ },
327
+ {
328
+ ordinal: 3,
329
+ createdAtMs: 1_788_134_403_000,
330
+ type: 'reasoning',
331
+ item: {
332
+ id: 'reason-rich',
333
+ type: 'reasoning',
334
+ summary: ['Checking the workspace.'],
335
+ },
336
+ },
337
+ {
338
+ ordinal: 4,
339
+ createdAtMs: 1_788_134_404_000,
340
+ type: 'commandExecution',
341
+ item: {
342
+ id: 'command-rich',
343
+ type: 'commandExecution',
344
+ command: 'pwd',
345
+ aggregatedOutput: workspacePath,
346
+ status: 'completed',
347
+ exitCode: 0,
348
+ },
349
+ },
350
+ {
351
+ ordinal: 5,
352
+ createdAtMs: 1_788_134_405_000,
353
+ type: 'agentMessage',
354
+ item: {
355
+ id: 'agent-rich',
356
+ type: 'agentMessage',
357
+ text: 'Done.',
358
+ },
359
+ },
360
+ ];
361
+ for (const raw of rawItems) {
362
+ insertItem.run(
363
+ sessionId,
364
+ 'turn-rich',
365
+ raw.item.id,
366
+ raw.ordinal,
367
+ raw.createdAtMs,
368
+ raw.type,
369
+ JSON.stringify(raw.item),
370
+ );
371
+ }
372
+ sqlite.close();
373
+
374
+ const session = await new LocalCodexSessionStore(codexHome).findSession(
375
+ sessionId,
376
+ );
377
+
378
+ expect(session?.turns).toHaveLength(2);
379
+ expect(session?.turns[0]).toMatchObject({
380
+ id: 'turn-rich',
381
+ status: 'completed',
382
+ items: [
383
+ { id: 'user-rich', kind: 'userMessage' },
384
+ { id: 'reason-rich', kind: 'reasoning' },
385
+ { id: 'agent-commentary', kind: 'agentMessage' },
386
+ { id: 'command-rich', kind: 'commandExecution' },
387
+ { id: 'agent-rich', kind: 'agentMessage' },
388
+ ],
389
+ });
390
+ expect(session?.turns[1]).toMatchObject({
391
+ id: 'turn-new',
392
+ status: 'inProgress',
393
+ items: [
394
+ { id: 'user-new', kind: 'userMessage' },
395
+ { id: 'command-new', kind: 'commandExecution' },
396
+ ],
397
+ });
398
+ });
399
+
400
+ it('watches rollout changes for externally running imported sessions', async () => {
401
+ const codexHome = await fs.mkdtemp(
402
+ path.join(os.tmpdir(), 'remote-codex-watched-session-'),
403
+ );
404
+ tempDirectories.push(codexHome);
405
+ const sessionId = '01a059bf-c303-79b1-8802-f922d3a81971';
406
+ const sessionsDirectory = path.join(codexHome, 'sessions');
407
+ const transcriptPath = path.join(
408
+ sessionsDirectory,
409
+ `rollout-${sessionId}.jsonl`,
410
+ );
411
+ await fs.mkdir(sessionsDirectory, { recursive: true });
412
+ await fs.writeFile(
413
+ transcriptPath,
414
+ `${JSON.stringify({
415
+ timestamp: '2026-08-31T00:00:00.000Z',
416
+ type: 'session_meta',
417
+ payload: { id: sessionId, cwd: codexHome },
418
+ })}\n`,
419
+ );
420
+ const store = new LocalCodexSessionStore(codexHome, {
421
+ watchIntervalMs: 20,
422
+ watchThrottleMs: 25,
423
+ });
424
+ let changes = 0;
425
+ let resolveChange: (() => void) | null = null;
426
+ const changed = new Promise<void>((resolve) => {
427
+ resolveChange = resolve;
428
+ });
429
+ const stop = await store.watchSession(sessionId, () => {
430
+ changes += 1;
431
+ resolveChange?.();
432
+ });
433
+
434
+ await new Promise((resolve) => setTimeout(resolve, 40));
435
+ await fs.appendFile(
436
+ transcriptPath,
437
+ `${JSON.stringify({
438
+ timestamp: '2026-08-31T00:00:01.000Z',
439
+ type: 'event_msg',
440
+ payload: { type: 'task_started', turn_id: 'turn-1' },
441
+ })}\n`,
442
+ );
443
+ await Promise.race([
444
+ changed,
445
+ new Promise((_, reject) =>
446
+ setTimeout(() => reject(new Error('watcher timed out')), 2_000),
447
+ ),
448
+ ]);
449
+
450
+ stop();
451
+ await fs.appendFile(transcriptPath, '{}\n');
452
+ await new Promise((resolve) => setTimeout(resolve, 100));
453
+ expect(changes).toBe(1);
454
+ });
455
+ });