remote-codex 0.11.52 → 0.11.54
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/apps/relay-server/dist/index.js +6 -7
- package/apps/supervisor-api/dist/index.js +527 -72
- package/apps/supervisor-web/dist/assets/{index-BBq6mr8o.js → index-Bnury8uP.js} +6 -6
- package/apps/supervisor-web/dist/index.html +1 -1
- package/bin/remote-codex.mjs +17 -3
- package/package.json +1 -1
- package/packages/codex/src/historyItems.test.ts +9 -0
- package/packages/codex/src/historyItems.ts +20 -8
- package/packages/codex/src/local-session-store.test.ts +455 -0
- package/packages/codex/src/local-session-store.ts +505 -40
- package/scripts/windows/build-device-manager.ps1 +0 -7
|
@@ -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-
|
|
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">
|
package/bin/remote-codex.mjs
CHANGED
|
@@ -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}`);
|
|
@@ -685,6 +685,8 @@ function startRelaySupervisorTmux() {
|
|
|
685
685
|
return;
|
|
686
686
|
}
|
|
687
687
|
|
|
688
|
+
fs.mkdirSync(path.dirname(relaySupervisorLogPath), { recursive: true });
|
|
689
|
+
rotateRelaySupervisorLog();
|
|
688
690
|
const command = relaySupervisorTmuxCommand();
|
|
689
691
|
const result = spawnSync('tmux', ['new-session', '-d', '-s', relaySupervisorTmuxSession, command], {
|
|
690
692
|
cwd: packageRoot,
|
|
@@ -702,6 +704,17 @@ function startRelaySupervisorTmux() {
|
|
|
702
704
|
return;
|
|
703
705
|
}
|
|
704
706
|
|
|
707
|
+
const startupDeadline = Date.now() + 1_000;
|
|
708
|
+
while (Date.now() < startupDeadline) {
|
|
709
|
+
if (!tmuxSessionExists(relaySupervisorTmuxSession)) {
|
|
710
|
+
console.error('relay-supervisor exited immediately after tmux launched it.');
|
|
711
|
+
console.error('Restarting in foreground to show the startup error.');
|
|
712
|
+
runRelaySupervisorForeground();
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
await sleep(100);
|
|
716
|
+
}
|
|
717
|
+
|
|
705
718
|
console.log(`Started remote-codex relay-supervisor in tmux session: ${relaySupervisorTmuxSession}`);
|
|
706
719
|
printRelaySupervisorTmuxCommands();
|
|
707
720
|
}
|
|
@@ -710,7 +723,7 @@ function relaySupervisorTmuxCommand() {
|
|
|
710
723
|
const envPrefix = nonEmptyEnv('REMOTE_CODEX_RELAY_SUPERVISOR_CONFIG')
|
|
711
724
|
? `REMOTE_CODEX_RELAY_SUPERVISOR_CONFIG=${shellQuote(process.env.REMOTE_CODEX_RELAY_SUPERVISOR_CONFIG)}`
|
|
712
725
|
: '';
|
|
713
|
-
const command = `${shellQuote(process.execPath)} ${shellQuote(fileURLToPath(import.meta.url))} relay-supervisor run`;
|
|
726
|
+
const command = `${shellQuote(process.execPath)} ${shellQuote(fileURLToPath(import.meta.url))} relay-supervisor run 2>&1 | tee -a ${shellQuote(relaySupervisorLogPath)}`;
|
|
714
727
|
return envPrefix ? `${envPrefix} ${command}` : command;
|
|
715
728
|
}
|
|
716
729
|
|
|
@@ -765,6 +778,7 @@ function stopRelaySupervisorTmux() {
|
|
|
765
778
|
|
|
766
779
|
function printRelaySupervisorTmuxCommands() {
|
|
767
780
|
console.log(`Attach logs: tmux attach -t ${shellQuote(relaySupervisorTmuxSession)}`);
|
|
781
|
+
console.log(`Persistent logs: ${relaySupervisorLogPath}`);
|
|
768
782
|
console.log('Status: remote-codex relay-supervisor status');
|
|
769
783
|
console.log('Stop: remote-codex relay-supervisor stop');
|
|
770
784
|
}
|
package/package.json
CHANGED
|
@@ -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
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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
|
+
});
|