remote-codex 0.11.51 → 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.
- package/apps/supervisor-api/dist/index.js +511 -71
- package/apps/supervisor-web/dist/assets/index-Bnury8uP.js +22 -0
- package/apps/supervisor-web/dist/assets/{thread-ui-BbtcUwps.js → thread-ui-C5pxOmDJ.js} +33 -33
- package/apps/supervisor-web/dist/index.html +2 -2
- package/bin/remote-codex.mjs +13 -2
- package/package.json +1 -1
- package/packages/acp/src/runtimeAdapter.test.ts +14 -1
- package/packages/codex/src/appServerManager.test.ts +2 -2
- 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
- package/apps/supervisor-web/dist/assets/index-7LiZQ3aJ.js +0 -22
|
@@ -10,13 +10,13 @@
|
|
|
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">
|
|
17
17
|
<link rel="modulepreload" crossorigin href="/assets/terminal-vendor-C5bTa-Ka.js">
|
|
18
18
|
<link rel="modulepreload" crossorigin href="/assets/markdown-vendor-BG6PurxI.js">
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/assets/thread-ui-
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/assets/thread-ui-C5pxOmDJ.js">
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/graph-vendor-C5ap-Sga.css">
|
|
21
21
|
<link rel="stylesheet" crossorigin href="/assets/terminal-vendor-Beg8tuEN.css">
|
|
22
22
|
<link rel="stylesheet" crossorigin href="/assets/index-Bg8FdhS1.css">
|
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}`);
|
|
@@ -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
|
@@ -471,7 +471,7 @@ describe('AcpRuntimeAdapter', () => {
|
|
|
471
471
|
env: {
|
|
472
472
|
REMOTE_CODEX_FAKE_ACP_AGENT_KIND: 'codex',
|
|
473
473
|
REMOTE_CODEX_FAKE_ACP_SKIP_PERMISSION: '1',
|
|
474
|
-
REMOTE_CODEX_FAKE_ACP_STREAM_DELAY_MS: '
|
|
474
|
+
REMOTE_CODEX_FAKE_ACP_STREAM_DELAY_MS: '1000',
|
|
475
475
|
},
|
|
476
476
|
startupTimeoutMs: 5_000,
|
|
477
477
|
});
|
|
@@ -500,10 +500,23 @@ describe('AcpRuntimeAdapter', () => {
|
|
|
500
500
|
});
|
|
501
501
|
},
|
|
502
502
|
);
|
|
503
|
+
const partialStarted = new Promise<void>((resolve, reject) => {
|
|
504
|
+
const timer = setTimeout(() => reject(new Error('Steerable fixture partial timed out.')), 10_000);
|
|
505
|
+
const onEvent = (event: AgentRuntimeEvent) => {
|
|
506
|
+
if (event.type !== 'output.delta' || !event.delta.includes('FAKE_ACP_PARTIAL')) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
clearTimeout(timer);
|
|
510
|
+
adapter.off('event', onEvent);
|
|
511
|
+
resolve();
|
|
512
|
+
};
|
|
513
|
+
adapter.on('event', onEvent);
|
|
514
|
+
});
|
|
503
515
|
const running = await adapter.startTurn({
|
|
504
516
|
providerSessionId: session.providerSessionId,
|
|
505
517
|
prompt: 'Start a steerable fixture turn.',
|
|
506
518
|
});
|
|
519
|
+
await partialStarted;
|
|
507
520
|
await expect(adapter.sendInput({
|
|
508
521
|
providerSessionId: session.providerSessionId,
|
|
509
522
|
providerTurnId: running.providerTurnId,
|
|
@@ -315,7 +315,7 @@ describe('CodexAppServerManager', () => {
|
|
|
315
315
|
|
|
316
316
|
const manager = new CodexAppServerManager({
|
|
317
317
|
command: process.execPath,
|
|
318
|
-
startupTimeoutMs:
|
|
318
|
+
startupTimeoutMs: 5_000,
|
|
319
319
|
clientInfo: {
|
|
320
320
|
name: 'test',
|
|
321
321
|
title: 'test',
|
|
@@ -388,7 +388,7 @@ describe('CodexAppServerManager', () => {
|
|
|
388
388
|
]);
|
|
389
389
|
|
|
390
390
|
await manager.stop();
|
|
391
|
-
});
|
|
391
|
+
}, 15_000);
|
|
392
392
|
|
|
393
393
|
it('does not let a stale child exit close the replacement app-server client during restart', async () => {
|
|
394
394
|
const firstChild = new ScriptedChild({
|
|
@@ -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
|
+
});
|