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.
- package/apps/supervisor-api/dist/index.js +511 -71
- 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 +13 -2
- 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
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
unwatchFile,
|
|
3
|
+
watchFile,
|
|
4
|
+
type Dirent,
|
|
5
|
+
type Stats,
|
|
6
|
+
} from 'node:fs';
|
|
2
7
|
import fs from 'node:fs/promises';
|
|
3
8
|
import path from 'node:path';
|
|
4
9
|
|
|
@@ -10,6 +15,15 @@ import {
|
|
|
10
15
|
ThreadTurnDto,
|
|
11
16
|
truncateAutoThreadTitle,
|
|
12
17
|
} from '../../shared/src/index';
|
|
18
|
+
import {
|
|
19
|
+
agentTurnToThreadTurnDto,
|
|
20
|
+
codexTurnToAgentTurn,
|
|
21
|
+
} from './historyItems';
|
|
22
|
+
import type {
|
|
23
|
+
CodexTurnError,
|
|
24
|
+
CodexTurnItem,
|
|
25
|
+
CodexTurnStatus,
|
|
26
|
+
} from './types';
|
|
13
27
|
|
|
14
28
|
interface LocalStateThreadRow {
|
|
15
29
|
id: string;
|
|
@@ -25,6 +39,27 @@ interface ParsedTranscript {
|
|
|
25
39
|
turns: ThreadTurnDto[];
|
|
26
40
|
}
|
|
27
41
|
|
|
42
|
+
interface PaginatedTurnRow {
|
|
43
|
+
turnId: string;
|
|
44
|
+
rolloutOrdinal: number;
|
|
45
|
+
status: string;
|
|
46
|
+
errorJson: string | null;
|
|
47
|
+
startedAt: number | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface PaginatedItemRow {
|
|
51
|
+
turnId: string;
|
|
52
|
+
rolloutOrdinal: number;
|
|
53
|
+
createdAtMs: number;
|
|
54
|
+
itemType: string;
|
|
55
|
+
itemJson: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface LocalCodexSessionStoreOptions {
|
|
59
|
+
watchIntervalMs?: number;
|
|
60
|
+
watchThrottleMs?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
28
63
|
export interface LocalCodexSessionRecord {
|
|
29
64
|
sessionId: string;
|
|
30
65
|
cwd: string;
|
|
@@ -74,6 +109,110 @@ function createHistoryItemId(turnId: string, prefix: string, index: number) {
|
|
|
74
109
|
return `${turnId}-${prefix}-${index}`;
|
|
75
110
|
}
|
|
76
111
|
|
|
112
|
+
function transcriptMessageText(payload: any) {
|
|
113
|
+
if (!Array.isArray(payload?.content)) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const text = payload.content
|
|
118
|
+
.filter(
|
|
119
|
+
(content: any) =>
|
|
120
|
+
(content?.type === 'input_text' || content?.type === 'output_text') &&
|
|
121
|
+
typeof content.text === 'string' &&
|
|
122
|
+
content.text.trim(),
|
|
123
|
+
)
|
|
124
|
+
.map((content: any) => content.text)
|
|
125
|
+
.join('\n\n');
|
|
126
|
+
|
|
127
|
+
return text || null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function camelCaseKey(key: string) {
|
|
131
|
+
return key.replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function camelCaseValue(value: unknown): unknown {
|
|
135
|
+
if (Array.isArray(value)) {
|
|
136
|
+
return value.map(camelCaseValue);
|
|
137
|
+
}
|
|
138
|
+
if (!value || typeof value !== 'object') {
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
return Object.fromEntries(
|
|
142
|
+
Object.entries(value as Record<string, unknown>).map(([key, entry]) => [
|
|
143
|
+
camelCaseKey(key),
|
|
144
|
+
camelCaseValue(entry),
|
|
145
|
+
]),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function completedRolloutHistoryItem(
|
|
150
|
+
payload: any,
|
|
151
|
+
timestamp: string | null | undefined,
|
|
152
|
+
) {
|
|
153
|
+
if (!payload?.item || typeof payload.item !== 'object') {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const normalized = camelCaseValue(payload.item) as Record<string, unknown>;
|
|
157
|
+
if (typeof normalized.id !== 'string' || typeof normalized.type !== 'string') {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const normalizedType =
|
|
161
|
+
normalized.type.charAt(0).toLowerCase() + normalized.type.slice(1);
|
|
162
|
+
if (normalizedType === 'userMessage' || normalizedType === 'agentMessage') {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
if (normalizedType === 'reasoning') {
|
|
166
|
+
const summaryText = normalized.summaryText;
|
|
167
|
+
normalized.summary = Array.isArray(summaryText)
|
|
168
|
+
? summaryText.filter((entry): entry is string => typeof entry === 'string')
|
|
169
|
+
: typeof summaryText === 'string' && summaryText.trim()
|
|
170
|
+
? [summaryText]
|
|
171
|
+
: [];
|
|
172
|
+
const rawContent = normalized.rawContent;
|
|
173
|
+
normalized.text = Array.isArray(rawContent)
|
|
174
|
+
? rawContent
|
|
175
|
+
.map((entry) =>
|
|
176
|
+
typeof entry === 'string'
|
|
177
|
+
? entry
|
|
178
|
+
: entry && typeof entry === 'object' && typeof (entry as any).text === 'string'
|
|
179
|
+
? (entry as any).text
|
|
180
|
+
: '',
|
|
181
|
+
)
|
|
182
|
+
.filter(Boolean)
|
|
183
|
+
.join('\n')
|
|
184
|
+
: '';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const createdAtCandidate =
|
|
188
|
+
typeof payload.started_at_ms === 'number'
|
|
189
|
+
? payload.started_at_ms
|
|
190
|
+
: timestamp ?? null;
|
|
191
|
+
const agentTurn = codexTurnToAgentTurn({
|
|
192
|
+
id: typeof payload.turn_id === 'string' ? payload.turn_id : 'rollout-turn',
|
|
193
|
+
status: 'inProgress',
|
|
194
|
+
error: null,
|
|
195
|
+
items: [
|
|
196
|
+
{
|
|
197
|
+
...normalized,
|
|
198
|
+
id: normalized.id,
|
|
199
|
+
type: normalizedType,
|
|
200
|
+
createdAt: createdAtCandidate,
|
|
201
|
+
} as unknown as CodexTurnItem,
|
|
202
|
+
],
|
|
203
|
+
});
|
|
204
|
+
return agentTurn.items[0] ?? null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function appendUniqueTurnItem(turn: MutableTurn, item: ThreadHistoryItemDto) {
|
|
208
|
+
const existingIndex = turn.items.findIndex((entry) => entry.id === item.id);
|
|
209
|
+
if (existingIndex >= 0) {
|
|
210
|
+
turn.items[existingIndex] = item;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
turn.items.push(item);
|
|
214
|
+
}
|
|
215
|
+
|
|
77
216
|
function finalizeTurn(turn: MutableTurn | null, turns: ThreadTurnDto[]) {
|
|
78
217
|
if (!turn || turn.items.length === 0) {
|
|
79
218
|
return;
|
|
@@ -88,7 +227,129 @@ function finalizeTurn(turn: MutableTurn | null, turns: ThreadTurnDto[]) {
|
|
|
88
227
|
});
|
|
89
228
|
}
|
|
90
229
|
|
|
230
|
+
function isoTimestampFromEpochSeconds(value: number | null) {
|
|
231
|
+
return value === null || !Number.isFinite(value)
|
|
232
|
+
? null
|
|
233
|
+
: new Date(value * 1_000).toISOString();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function normalizePaginatedTurnStatus(status: string): CodexTurnStatus {
|
|
237
|
+
switch (status) {
|
|
238
|
+
case 'completed':
|
|
239
|
+
case 'interrupted':
|
|
240
|
+
case 'failed':
|
|
241
|
+
case 'inProgress':
|
|
242
|
+
return status;
|
|
243
|
+
default:
|
|
244
|
+
return 'inProgress';
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function parsePaginatedTurnError(value: string | null): CodexTurnError | null {
|
|
249
|
+
if (!value) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
const parsed = JSON.parse(value);
|
|
254
|
+
if (
|
|
255
|
+
parsed &&
|
|
256
|
+
typeof parsed === 'object' &&
|
|
257
|
+
typeof (parsed as Record<string, unknown>).message === 'string'
|
|
258
|
+
) {
|
|
259
|
+
return parsed as CodexTurnError;
|
|
260
|
+
}
|
|
261
|
+
return { message: value };
|
|
262
|
+
} catch {
|
|
263
|
+
return { message: value };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function mergeSessionTurns(
|
|
268
|
+
paginatedTurns: ThreadTurnDto[],
|
|
269
|
+
transcriptTurns: ThreadTurnDto[],
|
|
270
|
+
) {
|
|
271
|
+
if (paginatedTurns.length === 0) {
|
|
272
|
+
return transcriptTurns;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const transcriptById = new Map(
|
|
276
|
+
transcriptTurns.map((turn) => [turn.id, turn]),
|
|
277
|
+
);
|
|
278
|
+
const merged = paginatedTurns.map((turn) => {
|
|
279
|
+
const transcriptTurn = transcriptById.get(turn.id);
|
|
280
|
+
if (!transcriptTurn) {
|
|
281
|
+
return turn;
|
|
282
|
+
}
|
|
283
|
+
transcriptById.delete(turn.id);
|
|
284
|
+
|
|
285
|
+
const paginatedItemIds = new Set(turn.items.map((item) => item.id));
|
|
286
|
+
const missingTranscriptItems = transcriptTurn.items.filter(
|
|
287
|
+
(item) => !paginatedItemIds.has(item.id),
|
|
288
|
+
);
|
|
289
|
+
const items = [...turn.items, ...missingTranscriptItems]
|
|
290
|
+
.map((item, index) => ({ item, index }))
|
|
291
|
+
.sort((left, right) => {
|
|
292
|
+
const leftMillis = Date.parse(left.item.createdAt ?? '');
|
|
293
|
+
const rightMillis = Date.parse(right.item.createdAt ?? '');
|
|
294
|
+
if (Number.isFinite(leftMillis) && Number.isFinite(rightMillis)) {
|
|
295
|
+
const delta = leftMillis - rightMillis;
|
|
296
|
+
return delta === 0 ? left.index - right.index : delta;
|
|
297
|
+
}
|
|
298
|
+
if (Number.isFinite(leftMillis)) return -1;
|
|
299
|
+
if (Number.isFinite(rightMillis)) return 1;
|
|
300
|
+
return left.index - right.index;
|
|
301
|
+
})
|
|
302
|
+
.map((entry, transcriptOrder) => ({
|
|
303
|
+
...entry.item,
|
|
304
|
+
transcriptOrder,
|
|
305
|
+
}));
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
...turn,
|
|
309
|
+
startedAt: turn.startedAt ?? transcriptTurn.startedAt,
|
|
310
|
+
status: transcriptTurn.status,
|
|
311
|
+
error: transcriptTurn.error ?? turn.error,
|
|
312
|
+
items,
|
|
313
|
+
};
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
merged.push(...transcriptById.values());
|
|
317
|
+
return merged.sort((left, right) =>
|
|
318
|
+
(left.startedAt ?? '').localeCompare(right.startedAt ?? ''),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
91
322
|
function parseTranscript(contents: string): ParsedTranscript {
|
|
323
|
+
const entries = contents
|
|
324
|
+
.split('\n')
|
|
325
|
+
.filter((line) => line.trim())
|
|
326
|
+
.flatMap((line) => {
|
|
327
|
+
try {
|
|
328
|
+
return [JSON.parse(line)];
|
|
329
|
+
} catch {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
let transcriptSegmentIndex = -1;
|
|
334
|
+
const indexedEntries = entries.map((entry: any) => {
|
|
335
|
+
if (
|
|
336
|
+
entry.type === 'event_msg' &&
|
|
337
|
+
entry.payload?.type === 'task_started'
|
|
338
|
+
) {
|
|
339
|
+
transcriptSegmentIndex += 1;
|
|
340
|
+
}
|
|
341
|
+
return { entry, segmentIndex: transcriptSegmentIndex };
|
|
342
|
+
});
|
|
343
|
+
const legacyMessageSegments = new Set(
|
|
344
|
+
indexedEntries
|
|
345
|
+
.filter(
|
|
346
|
+
({ entry }) =>
|
|
347
|
+
entry.type === 'event_msg' &&
|
|
348
|
+
(entry.payload?.type === 'user_message' ||
|
|
349
|
+
entry.payload?.type === 'agent_message'),
|
|
350
|
+
)
|
|
351
|
+
.map(({ segmentIndex }) => segmentIndex),
|
|
352
|
+
);
|
|
92
353
|
const turns: ThreadTurnDto[] = [];
|
|
93
354
|
let cwd: string | null = null;
|
|
94
355
|
let currentTurn: MutableTurn | null = null;
|
|
@@ -114,18 +375,7 @@ function parseTranscript(contents: string): ParsedTranscript {
|
|
|
114
375
|
return currentTurn;
|
|
115
376
|
};
|
|
116
377
|
|
|
117
|
-
for (const
|
|
118
|
-
if (!line.trim()) {
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
let entry: any;
|
|
123
|
-
try {
|
|
124
|
-
entry = JSON.parse(line);
|
|
125
|
-
} catch {
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
|
|
378
|
+
for (const { entry, segmentIndex } of indexedEntries) {
|
|
129
379
|
if (entry.type === 'session_meta') {
|
|
130
380
|
const payload = entry.payload ?? {};
|
|
131
381
|
if (typeof payload.cwd === 'string' && payload.cwd.trim()) {
|
|
@@ -134,6 +384,45 @@ function parseTranscript(contents: string): ParsedTranscript {
|
|
|
134
384
|
continue;
|
|
135
385
|
}
|
|
136
386
|
|
|
387
|
+
if (
|
|
388
|
+
!legacyMessageSegments.has(segmentIndex) &&
|
|
389
|
+
entry.type === 'response_item' &&
|
|
390
|
+
entry.payload?.type === 'message'
|
|
391
|
+
) {
|
|
392
|
+
const payload = entry.payload;
|
|
393
|
+
const text = transcriptMessageText(payload);
|
|
394
|
+
if (!text || (payload.role !== 'user' && payload.role !== 'assistant')) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const turn = ensureCurrentTurn(entry.timestamp);
|
|
399
|
+
if (payload.role === 'user') {
|
|
400
|
+
userItemCount += 1;
|
|
401
|
+
turn.items.push({
|
|
402
|
+
id:
|
|
403
|
+
typeof payload.id === 'string' && payload.id.trim()
|
|
404
|
+
? payload.id
|
|
405
|
+
: createHistoryItemId(turn.id, 'user', userItemCount),
|
|
406
|
+
kind: 'userMessage',
|
|
407
|
+
text,
|
|
408
|
+
createdAt: entry.timestamp ?? null,
|
|
409
|
+
});
|
|
410
|
+
} else {
|
|
411
|
+
agentItemCount += 1;
|
|
412
|
+
turn.items.push({
|
|
413
|
+
id:
|
|
414
|
+
typeof payload.id === 'string' && payload.id.trim()
|
|
415
|
+
? payload.id
|
|
416
|
+
: createHistoryItemId(turn.id, 'agent', agentItemCount),
|
|
417
|
+
kind: 'agentMessage',
|
|
418
|
+
text,
|
|
419
|
+
status: typeof payload.phase === 'string' ? payload.phase : null,
|
|
420
|
+
createdAt: entry.timestamp ?? null,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
137
426
|
if (entry.type !== 'event_msg') {
|
|
138
427
|
continue;
|
|
139
428
|
}
|
|
@@ -165,6 +454,7 @@ function parseTranscript(contents: string): ParsedTranscript {
|
|
|
165
454
|
id: createHistoryItemId(turn.id, 'user', userItemCount),
|
|
166
455
|
kind: 'userMessage',
|
|
167
456
|
text: payload.message,
|
|
457
|
+
createdAt: entry.timestamp ?? null,
|
|
168
458
|
});
|
|
169
459
|
continue;
|
|
170
460
|
}
|
|
@@ -177,10 +467,22 @@ function parseTranscript(contents: string): ParsedTranscript {
|
|
|
177
467
|
kind: 'agentMessage',
|
|
178
468
|
text: payload.message,
|
|
179
469
|
status: typeof payload.phase === 'string' ? payload.phase : null,
|
|
470
|
+
createdAt: entry.timestamp ?? null,
|
|
180
471
|
});
|
|
181
472
|
continue;
|
|
182
473
|
}
|
|
183
474
|
|
|
475
|
+
if (payloadType === 'item_completed') {
|
|
476
|
+
const item = completedRolloutHistoryItem(
|
|
477
|
+
payload,
|
|
478
|
+
typeof entry.timestamp === 'string' ? entry.timestamp : null,
|
|
479
|
+
);
|
|
480
|
+
if (item) {
|
|
481
|
+
appendUniqueTurnItem(ensureCurrentTurn(entry.timestamp), item);
|
|
482
|
+
}
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
|
|
184
486
|
if (payloadType === 'task_complete') {
|
|
185
487
|
const turn = ensureCurrentTurn(entry.timestamp);
|
|
186
488
|
turn.status = turn.error ? 'failed' : 'completed';
|
|
@@ -218,7 +520,16 @@ async function fileExists(filePath: string) {
|
|
|
218
520
|
}
|
|
219
521
|
|
|
220
522
|
export class LocalCodexSessionStore {
|
|
221
|
-
|
|
523
|
+
private readonly watchIntervalMs: number;
|
|
524
|
+
private readonly watchThrottleMs: number;
|
|
525
|
+
|
|
526
|
+
constructor(
|
|
527
|
+
private readonly codexHome: string,
|
|
528
|
+
options: LocalCodexSessionStoreOptions = {},
|
|
529
|
+
) {
|
|
530
|
+
this.watchIntervalMs = options.watchIntervalMs ?? 500;
|
|
531
|
+
this.watchThrottleMs = options.watchThrottleMs ?? 1_500;
|
|
532
|
+
}
|
|
222
533
|
|
|
223
534
|
async findSession(
|
|
224
535
|
sessionId: string,
|
|
@@ -231,6 +542,7 @@ export class LocalCodexSessionStore {
|
|
|
231
542
|
const transcript = transcriptPath
|
|
232
543
|
? parseTranscript(await fs.readFile(transcriptPath, 'utf8'))
|
|
233
544
|
: null;
|
|
545
|
+
const paginatedTurns = await this.findPaginatedTurns(sessionId);
|
|
234
546
|
const cwd = stateRecord?.cwd ?? transcript?.cwd ?? null;
|
|
235
547
|
|
|
236
548
|
if (!cwd) {
|
|
@@ -246,7 +558,65 @@ export class LocalCodexSessionStore {
|
|
|
246
558
|
basenameFromPath(cwd),
|
|
247
559
|
model: stateRecord?.model ?? null,
|
|
248
560
|
rolloutPath: transcriptPath,
|
|
249
|
-
turns: transcript?.turns ?? [],
|
|
561
|
+
turns: mergeSessionTurns(paginatedTurns, transcript?.turns ?? []),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async watchSession(
|
|
566
|
+
sessionId: string,
|
|
567
|
+
onChange: () => void,
|
|
568
|
+
): Promise<() => void> {
|
|
569
|
+
const stateRecord = await this.findSessionInStateDatabases(sessionId);
|
|
570
|
+
const transcriptPath = await this.resolveTranscriptPath(
|
|
571
|
+
stateRecord?.rolloutPath ?? null,
|
|
572
|
+
sessionId,
|
|
573
|
+
);
|
|
574
|
+
if (!transcriptPath) {
|
|
575
|
+
return () => {};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
let stopped = false;
|
|
579
|
+
let lastEmittedAt = 0;
|
|
580
|
+
let pending: NodeJS.Timeout | null = null;
|
|
581
|
+
const emit = () => {
|
|
582
|
+
pending = null;
|
|
583
|
+
if (stopped) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
lastEmittedAt = Date.now();
|
|
587
|
+
onChange();
|
|
588
|
+
};
|
|
589
|
+
const schedule = () => {
|
|
590
|
+
if (stopped || pending) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const delay = Math.max(
|
|
594
|
+
0,
|
|
595
|
+
this.watchThrottleMs - (Date.now() - lastEmittedAt),
|
|
596
|
+
);
|
|
597
|
+
pending = setTimeout(emit, delay);
|
|
598
|
+
};
|
|
599
|
+
const listener = (current: Stats, previous: Stats) => {
|
|
600
|
+
if (
|
|
601
|
+
current.size !== previous.size ||
|
|
602
|
+
current.mtimeMs !== previous.mtimeMs
|
|
603
|
+
) {
|
|
604
|
+
schedule();
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
watchFile(
|
|
609
|
+
transcriptPath,
|
|
610
|
+
{ persistent: false, interval: this.watchIntervalMs },
|
|
611
|
+
listener,
|
|
612
|
+
);
|
|
613
|
+
return () => {
|
|
614
|
+
stopped = true;
|
|
615
|
+
if (pending) {
|
|
616
|
+
clearTimeout(pending);
|
|
617
|
+
pending = null;
|
|
618
|
+
}
|
|
619
|
+
unwatchFile(transcriptPath, listener);
|
|
250
620
|
};
|
|
251
621
|
}
|
|
252
622
|
|
|
@@ -283,35 +653,44 @@ export class LocalCodexSessionStore {
|
|
|
283
653
|
private async findSessionInStateDatabases(
|
|
284
654
|
sessionId: string,
|
|
285
655
|
): Promise<LocalStateThreadRow | null> {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
656
|
+
const stateFiles = (
|
|
657
|
+
await Promise.all(
|
|
658
|
+
[this.codexHome, path.join(this.codexHome, 'sqlite')].map(
|
|
659
|
+
async (directory) => {
|
|
660
|
+
let entries: string[];
|
|
661
|
+
try {
|
|
662
|
+
entries = await fs.readdir(directory);
|
|
663
|
+
} catch {
|
|
664
|
+
return [];
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return Promise.all(
|
|
668
|
+
entries
|
|
669
|
+
.filter((entry) => /^state_\d+\.sqlite$/i.test(entry))
|
|
670
|
+
.map(async (entry) => {
|
|
671
|
+
const absPath = path.join(directory, entry);
|
|
672
|
+
const stats = await fs.stat(absPath);
|
|
673
|
+
return {
|
|
674
|
+
absPath,
|
|
675
|
+
mtimeMs: stats.mtimeMs,
|
|
676
|
+
};
|
|
677
|
+
}),
|
|
678
|
+
);
|
|
679
|
+
},
|
|
680
|
+
),
|
|
681
|
+
)
|
|
682
|
+
).flat();
|
|
305
683
|
|
|
306
684
|
stateFiles.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
307
685
|
|
|
308
686
|
for (const stateFile of stateFiles) {
|
|
309
|
-
|
|
310
|
-
readonly: true,
|
|
311
|
-
fileMustExist: true,
|
|
312
|
-
});
|
|
687
|
+
let sqlite: Database.Database | null = null;
|
|
313
688
|
|
|
314
689
|
try {
|
|
690
|
+
sqlite = new Database(stateFile.absPath, {
|
|
691
|
+
readonly: true,
|
|
692
|
+
fileMustExist: true,
|
|
693
|
+
});
|
|
315
694
|
const row = sqlite
|
|
316
695
|
.prepare(
|
|
317
696
|
`
|
|
@@ -332,15 +711,101 @@ export class LocalCodexSessionStore {
|
|
|
332
711
|
return row;
|
|
333
712
|
}
|
|
334
713
|
} catch {
|
|
335
|
-
//
|
|
714
|
+
// A corrupt or incompatible index must not block rollout-file recovery.
|
|
336
715
|
} finally {
|
|
337
|
-
sqlite
|
|
716
|
+
sqlite?.close();
|
|
338
717
|
}
|
|
339
718
|
}
|
|
340
719
|
|
|
341
720
|
return null;
|
|
342
721
|
}
|
|
343
722
|
|
|
723
|
+
private async findPaginatedTurns(sessionId: string) {
|
|
724
|
+
const databasePath = path.join(this.codexHome, 'thread_history_1.sqlite');
|
|
725
|
+
let sqlite: Database.Database | null = null;
|
|
726
|
+
try {
|
|
727
|
+
sqlite = new Database(databasePath, {
|
|
728
|
+
readonly: true,
|
|
729
|
+
fileMustExist: true,
|
|
730
|
+
});
|
|
731
|
+
const turnRows = sqlite.prepare(
|
|
732
|
+
`
|
|
733
|
+
SELECT
|
|
734
|
+
turn_id AS turnId,
|
|
735
|
+
rollout_ordinal AS rolloutOrdinal,
|
|
736
|
+
status,
|
|
737
|
+
error_json AS errorJson,
|
|
738
|
+
started_at AS startedAt
|
|
739
|
+
FROM thread_turns
|
|
740
|
+
WHERE thread_id = ?
|
|
741
|
+
ORDER BY rollout_ordinal ASC
|
|
742
|
+
`,
|
|
743
|
+
).all(sessionId) as PaginatedTurnRow[];
|
|
744
|
+
if (turnRows.length === 0) {
|
|
745
|
+
return [];
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const itemRows = sqlite.prepare(
|
|
749
|
+
`
|
|
750
|
+
SELECT
|
|
751
|
+
turn_id AS turnId,
|
|
752
|
+
rollout_ordinal AS rolloutOrdinal,
|
|
753
|
+
created_at_ms AS createdAtMs,
|
|
754
|
+
item_type AS itemType,
|
|
755
|
+
item_json AS itemJson
|
|
756
|
+
FROM thread_items
|
|
757
|
+
WHERE thread_id = ?
|
|
758
|
+
ORDER BY rollout_ordinal ASC
|
|
759
|
+
`,
|
|
760
|
+
).all(sessionId) as PaginatedItemRow[];
|
|
761
|
+
const itemsByTurnId = new Map<string, CodexTurnItem[]>();
|
|
762
|
+
for (const row of itemRows) {
|
|
763
|
+
let parsed: unknown;
|
|
764
|
+
try {
|
|
765
|
+
parsed = JSON.parse(row.itemJson);
|
|
766
|
+
} catch {
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const parsedRecord = parsed as Record<string, unknown>;
|
|
773
|
+
if (typeof parsedRecord.id !== 'string' || !parsedRecord.id.trim()) {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
const item = {
|
|
777
|
+
...parsedRecord,
|
|
778
|
+
id: parsedRecord.id,
|
|
779
|
+
type:
|
|
780
|
+
typeof parsedRecord.type === 'string'
|
|
781
|
+
? parsedRecord.type
|
|
782
|
+
: row.itemType,
|
|
783
|
+
createdAt: row.createdAtMs,
|
|
784
|
+
} as unknown as CodexTurnItem;
|
|
785
|
+
const turnItems = itemsByTurnId.get(row.turnId) ?? [];
|
|
786
|
+
turnItems.push(item);
|
|
787
|
+
itemsByTurnId.set(row.turnId, turnItems);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return turnRows.map((row) => {
|
|
791
|
+
const agentTurn = codexTurnToAgentTurn({
|
|
792
|
+
id: row.turnId,
|
|
793
|
+
status: normalizePaginatedTurnStatus(row.status),
|
|
794
|
+
error: parsePaginatedTurnError(row.errorJson),
|
|
795
|
+
items: itemsByTurnId.get(row.turnId) ?? [],
|
|
796
|
+
});
|
|
797
|
+
return agentTurnToThreadTurnDto({
|
|
798
|
+
...agentTurn,
|
|
799
|
+
startedAt: isoTimestampFromEpochSeconds(row.startedAt),
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
} catch {
|
|
803
|
+
return [];
|
|
804
|
+
} finally {
|
|
805
|
+
sqlite?.close();
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
344
809
|
private async resolveTranscriptPath(
|
|
345
810
|
rolloutPath: string | null,
|
|
346
811
|
sessionId: string,
|
|
@@ -8,18 +8,11 @@ param(
|
|
|
8
8
|
$ErrorActionPreference = 'Stop'
|
|
9
9
|
$repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
|
10
10
|
$projectPath = Join-Path $repositoryRoot 'apps\windows-device-manager\RemoteCodex.DeviceManager.csproj'
|
|
11
|
-
$productManifestPath = Join-Path $repositoryRoot 'apps\windows-device-manager\ProductManifest.cs'
|
|
12
11
|
$dotnet = (Get-Command dotnet.exe -ErrorAction SilentlyContinue)
|
|
13
12
|
if (-not $dotnet) {
|
|
14
13
|
$dotnet = (Get-Command dotnet -ErrorAction Stop)
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
$packageVersion = (Get-Content -LiteralPath (Join-Path $repositoryRoot 'package.json') -Raw | ConvertFrom-Json).version
|
|
18
|
-
$productManifest = Get-Content -LiteralPath $productManifestPath -Raw
|
|
19
|
-
if ($productManifest -notmatch ('RemoteCodexVersion\s*=\s*"{0}"' -f [Regex]::Escape($packageVersion))) {
|
|
20
|
-
throw "ProductManifest.RemoteCodexVersion must match package.json version $packageVersion."
|
|
21
|
-
}
|
|
22
|
-
|
|
23
16
|
& $dotnet.Source publish $projectPath `
|
|
24
17
|
--configuration $Configuration `
|
|
25
18
|
--runtime $Runtime `
|