remcodex 0.1.0-beta.1

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +331 -0
  3. package/dist/server/src/app.js +186 -0
  4. package/dist/server/src/cli.js +270 -0
  5. package/dist/server/src/controllers/codex-options.controller.js +199 -0
  6. package/dist/server/src/controllers/message.controller.js +21 -0
  7. package/dist/server/src/controllers/project.controller.js +44 -0
  8. package/dist/server/src/controllers/session.controller.js +175 -0
  9. package/dist/server/src/db/client.js +10 -0
  10. package/dist/server/src/db/migrations.js +32 -0
  11. package/dist/server/src/gateways/ws.gateway.js +60 -0
  12. package/dist/server/src/services/codex-app-server-runner.js +363 -0
  13. package/dist/server/src/services/codex-exec-runner.js +147 -0
  14. package/dist/server/src/services/codex-rollout-sync.js +977 -0
  15. package/dist/server/src/services/codex-runner.js +11 -0
  16. package/dist/server/src/services/codex-stream-events.js +478 -0
  17. package/dist/server/src/services/event-store.js +328 -0
  18. package/dist/server/src/services/project-manager.js +130 -0
  19. package/dist/server/src/services/pty-runner.js +72 -0
  20. package/dist/server/src/services/session-manager.js +1586 -0
  21. package/dist/server/src/services/session-timeline-service.js +181 -0
  22. package/dist/server/src/types/codex-launch.js +2 -0
  23. package/dist/server/src/types/models.js +37 -0
  24. package/dist/server/src/utils/ansi.js +143 -0
  25. package/dist/server/src/utils/codex-launch.js +102 -0
  26. package/dist/server/src/utils/codex-quota.js +179 -0
  27. package/dist/server/src/utils/codex-status.js +163 -0
  28. package/dist/server/src/utils/codex-ui-options.js +114 -0
  29. package/dist/server/src/utils/command.js +46 -0
  30. package/dist/server/src/utils/errors.js +16 -0
  31. package/dist/server/src/utils/ids.js +7 -0
  32. package/dist/server/src/utils/node-pty.js +29 -0
  33. package/package.json +36 -0
  34. package/scripts/fix-node-pty-helper.js +36 -0
  35. package/web/api.js +175 -0
  36. package/web/app.js +8082 -0
  37. package/web/components/composer.js +627 -0
  38. package/web/components/session-workbench.js +173 -0
  39. package/web/i18n/index.js +171 -0
  40. package/web/i18n/locales/de.js +50 -0
  41. package/web/i18n/locales/en.js +320 -0
  42. package/web/i18n/locales/es.js +50 -0
  43. package/web/i18n/locales/fr.js +50 -0
  44. package/web/i18n/locales/ja.js +50 -0
  45. package/web/i18n/locales/ko.js +50 -0
  46. package/web/i18n/locales/pt-BR.js +50 -0
  47. package/web/i18n/locales/ru.js +50 -0
  48. package/web/i18n/locales/zh-CN.js +320 -0
  49. package/web/i18n/locales/zh-Hant.js +53 -0
  50. package/web/index.html +23 -0
  51. package/web/message-rich-text.js +218 -0
  52. package/web/session-command-activity.js +980 -0
  53. package/web/session-event-adapter.js +826 -0
  54. package/web/session-timeline-reducer.js +728 -0
  55. package/web/session-timeline-renderer.js +656 -0
  56. package/web/session-ws.js +31 -0
  57. package/web/styles.css +5665 -0
  58. package/web/vendor/markdown-it.js +6969 -0
@@ -0,0 +1,977 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CodexRolloutSyncService = void 0;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const errors_1 = require("../utils/errors");
11
+ const ids_1 = require("../utils/ids");
12
+ function computeSourceRolloutHasOpenTurnFromRecords(records) {
13
+ const openTurnIds = new Set();
14
+ for (const record of records) {
15
+ const payload = record.payload && typeof record.payload === "object"
16
+ ? record.payload
17
+ : {};
18
+ if (record.type === "event_msg" && payload.type === "task_started") {
19
+ const turnId = typeof payload.turn_id === "string" && payload.turn_id.trim() ? payload.turn_id.trim() : "";
20
+ if (turnId) {
21
+ openTurnIds.add(turnId);
22
+ }
23
+ }
24
+ if (record.type === "event_msg" && (payload.type === "task_complete" || payload.type === "turn_aborted")) {
25
+ const turnId = typeof payload.turn_id === "string" && payload.turn_id.trim() ? payload.turn_id.trim() : "";
26
+ if (turnId) {
27
+ openTurnIds.delete(turnId);
28
+ }
29
+ }
30
+ }
31
+ return openTurnIds.size > 0;
32
+ }
33
+ function nowIso() {
34
+ return new Date().toISOString();
35
+ }
36
+ function resolveCodexHomeDir() {
37
+ const override = process.env.CODEX_HOME?.trim();
38
+ if (override) {
39
+ return node_path_1.default.resolve(override);
40
+ }
41
+ return node_path_1.default.join(node_os_1.default.homedir(), ".codex");
42
+ }
43
+ function readJsonlLines(filePath) {
44
+ return (0, node_fs_1.readFileSync)(filePath, "utf8")
45
+ .split(/\r?\n/)
46
+ .map((line) => line.trimEnd())
47
+ .filter(Boolean);
48
+ }
49
+ function parseJsonlRecords(filePath) {
50
+ return readJsonlLines(filePath).map((line) => JSON.parse(line));
51
+ }
52
+ function safeJsonParse(value) {
53
+ if (typeof value !== "string" || !value.trim()) {
54
+ return null;
55
+ }
56
+ try {
57
+ const parsed = JSON.parse(value);
58
+ return parsed && typeof parsed === "object" ? parsed : null;
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ function shorten(text, max = 72) {
65
+ const normalized = text.replace(/\s+/g, " ").trim();
66
+ if (normalized.length <= max) {
67
+ return normalized;
68
+ }
69
+ return `${normalized.slice(0, max - 1)}…`;
70
+ }
71
+ function extractMessageText(content) {
72
+ if (!Array.isArray(content)) {
73
+ return "";
74
+ }
75
+ return content
76
+ .map((part) => {
77
+ if (!part || typeof part !== "object") {
78
+ return "";
79
+ }
80
+ return typeof part.text === "string"
81
+ ? String(part.text)
82
+ : "";
83
+ })
84
+ .join("")
85
+ .trim();
86
+ }
87
+ function extractReasoningSummary(summary) {
88
+ if (!Array.isArray(summary)) {
89
+ return "";
90
+ }
91
+ return summary
92
+ .map((part) => {
93
+ if (!part || typeof part !== "object") {
94
+ return "";
95
+ }
96
+ return typeof part.text === "string"
97
+ ? String(part.text)
98
+ : "";
99
+ })
100
+ .join("")
101
+ .trim();
102
+ }
103
+ function parseExecOutput(output) {
104
+ const text = String(output || "");
105
+ const exitMatch = text.match(/Process exited with code (-?\d+)/);
106
+ const durationMatch = text.match(/Wall time: ([0-9.]+) seconds/);
107
+ const commandMatch = text.match(/^Command:\s+(.+)$/m);
108
+ const splitMarker = "\nOutput:\n";
109
+ const splitIndex = text.indexOf(splitMarker);
110
+ const outputText = splitIndex >= 0 ? text.slice(splitIndex + splitMarker.length) : text;
111
+ return {
112
+ commandLine: commandMatch?.[1] ?? null,
113
+ exitCode: exitMatch ? Number.parseInt(exitMatch[1], 10) : null,
114
+ durationMs: durationMatch
115
+ ? Math.round(Number.parseFloat(durationMatch[1]) * 1000)
116
+ : null,
117
+ outputText,
118
+ };
119
+ }
120
+ function buildCommandPayload(toolName, args) {
121
+ if (toolName === "exec_command") {
122
+ return {
123
+ command: typeof args.cmd === "string" ? args.cmd : "exec_command",
124
+ cwd: typeof args.workdir === "string" ? args.workdir : null,
125
+ justification: typeof args.justification === "string" ? args.justification : null,
126
+ sandboxMode: typeof args.sandbox_permissions === "string" ? args.sandbox_permissions : null,
127
+ approvalRequired: args.sandbox_permissions === "require_escalated",
128
+ grantRoot: null,
129
+ };
130
+ }
131
+ return {
132
+ command: `${toolName} ${shorten(JSON.stringify(args || {}), 160)}`.trim(),
133
+ cwd: typeof args.workdir === "string" ? args.workdir : null,
134
+ justification: null,
135
+ sandboxMode: null,
136
+ approvalRequired: false,
137
+ grantRoot: null,
138
+ };
139
+ }
140
+ function buildPatchStartPayload(toolName, input) {
141
+ return {
142
+ summary: `${toolName} ${shorten(String(input || ""), 160)}`.trim(),
143
+ target: null,
144
+ };
145
+ }
146
+ function buildTokenPayload(payload, timestamp) {
147
+ const info = payload.info && typeof payload.info === "object"
148
+ ? payload.info
149
+ : {};
150
+ return {
151
+ rateLimits: payload.rate_limits && typeof payload.rate_limits === "object"
152
+ ? payload.rate_limits
153
+ : payload.rateLimits && typeof payload.rateLimits === "object"
154
+ ? payload.rateLimits
155
+ : {},
156
+ totalTokenUsage: info.total_token_usage && typeof info.total_token_usage === "object"
157
+ ? info.total_token_usage
158
+ : {},
159
+ lastTokenUsage: info.last_token_usage && typeof info.last_token_usage === "object"
160
+ ? info.last_token_usage
161
+ : {},
162
+ modelContextWindow: typeof info.model_context_window === "number" ? info.model_context_window : undefined,
163
+ receivedAt: timestamp,
164
+ rawPayload: payload,
165
+ source: "rollout",
166
+ };
167
+ }
168
+ function runInTransaction(db, callback) {
169
+ db.exec("BEGIN");
170
+ try {
171
+ callback();
172
+ db.exec("COMMIT");
173
+ }
174
+ catch (error) {
175
+ db.exec("ROLLBACK");
176
+ throw error;
177
+ }
178
+ }
179
+ function scanRolloutPaths(root) {
180
+ if (!(0, node_fs_1.existsSync)(root)) {
181
+ return [];
182
+ }
183
+ const results = [];
184
+ const visit = (currentPath) => {
185
+ const entries = (0, node_fs_1.readdirSync)(currentPath, { withFileTypes: true });
186
+ for (const entry of entries) {
187
+ const entryPath = node_path_1.default.join(currentPath, entry.name);
188
+ if (entry.isDirectory()) {
189
+ visit(entryPath);
190
+ continue;
191
+ }
192
+ if (entry.isFile() && /^rollout-.*\.jsonl$/i.test(entry.name)) {
193
+ results.push(entryPath);
194
+ }
195
+ }
196
+ };
197
+ visit(root);
198
+ return results.sort((left, right) => {
199
+ const leftMtime = (0, node_fs_1.statSync)(left).mtimeMs;
200
+ const rightMtime = (0, node_fs_1.statSync)(right).mtimeMs;
201
+ return rightMtime - leftMtime;
202
+ });
203
+ }
204
+ function translateRolloutRecords(records, emitFromRecordIndex = 0) {
205
+ const sessionMeta = records.find((record) => record.type === "session_meta")?.payload;
206
+ const codexSessionId = String(sessionMeta?.id || "").trim();
207
+ if (!codexSessionId) {
208
+ throw new errors_1.AppError(400, "Unable to find session_meta.id in rollout.");
209
+ }
210
+ const workspacePath = String(sessionMeta?.cwd || process.cwd()).trim() || process.cwd();
211
+ const semanticEvents = [];
212
+ let currentTurnId = null;
213
+ let activeTurnId = null;
214
+ let assistantCounter = 0;
215
+ let reasoningCounter = 0;
216
+ const commandStarts = new Map();
217
+ const lastFinalAssistantMessageIdByTurn = new Map();
218
+ let firstUserMessage = "";
219
+ function appendSemantic(recordIndex, event) {
220
+ if (recordIndex < emitFromRecordIndex) {
221
+ return;
222
+ }
223
+ semanticEvents.push(event);
224
+ }
225
+ function downgradePreviousFinalAssistant(turnId) {
226
+ if (!turnId) {
227
+ return;
228
+ }
229
+ const previousMessageId = lastFinalAssistantMessageIdByTurn.get(turnId);
230
+ if (!previousMessageId) {
231
+ return;
232
+ }
233
+ for (const event of semanticEvents) {
234
+ if (event.turnId === turnId &&
235
+ event.messageId === previousMessageId &&
236
+ (event.type === "message.assistant.start" ||
237
+ event.type === "message.assistant.delta" ||
238
+ event.type === "message.assistant.end")) {
239
+ event.phase = "commentary";
240
+ }
241
+ }
242
+ }
243
+ for (let index = 0; index < records.length; index += 1) {
244
+ const record = records[index];
245
+ const timestamp = typeof record.timestamp === "string" && record.timestamp.trim()
246
+ ? record.timestamp.trim()
247
+ : nowIso();
248
+ if (record.type === "turn_context") {
249
+ const payload = record.payload && typeof record.payload === "object"
250
+ ? record.payload
251
+ : {};
252
+ currentTurnId =
253
+ typeof payload.turn_id === "string" && payload.turn_id.trim()
254
+ ? payload.turn_id.trim()
255
+ : currentTurnId;
256
+ continue;
257
+ }
258
+ if (record.type === "event_msg") {
259
+ const payload = record.payload && typeof record.payload === "object"
260
+ ? record.payload
261
+ : {};
262
+ switch (payload.type) {
263
+ case "task_started": {
264
+ const nextTurnId = typeof payload.turn_id === "string" && payload.turn_id.trim()
265
+ ? payload.turn_id.trim()
266
+ : currentTurnId;
267
+ if (activeTurnId && nextTurnId && activeTurnId !== nextTurnId) {
268
+ appendSemantic(index, {
269
+ type: "turn.completed",
270
+ turnId: activeTurnId,
271
+ messageId: null,
272
+ callId: null,
273
+ requestId: null,
274
+ phase: null,
275
+ stream: null,
276
+ payload: {
277
+ completedAt: timestamp,
278
+ reason: "implicit_rollover",
279
+ },
280
+ timestamp,
281
+ });
282
+ }
283
+ currentTurnId = nextTurnId;
284
+ activeTurnId = nextTurnId;
285
+ appendSemantic(index, {
286
+ type: "turn.started",
287
+ turnId: currentTurnId,
288
+ messageId: null,
289
+ callId: null,
290
+ requestId: null,
291
+ phase: null,
292
+ stream: null,
293
+ payload: {
294
+ createdAt: timestamp,
295
+ },
296
+ timestamp,
297
+ });
298
+ break;
299
+ }
300
+ case "user_message": {
301
+ const text = typeof payload.message === "string" ? payload.message.trim() : "";
302
+ if (!text) {
303
+ break;
304
+ }
305
+ if (!firstUserMessage) {
306
+ firstUserMessage = text;
307
+ }
308
+ appendSemantic(index, {
309
+ type: "message.user",
310
+ turnId: currentTurnId,
311
+ messageId: null,
312
+ callId: null,
313
+ requestId: null,
314
+ phase: null,
315
+ stream: null,
316
+ payload: { text },
317
+ timestamp,
318
+ });
319
+ break;
320
+ }
321
+ case "token_count": {
322
+ appendSemantic(index, {
323
+ type: "token_count",
324
+ turnId: currentTurnId,
325
+ messageId: null,
326
+ callId: null,
327
+ requestId: null,
328
+ phase: null,
329
+ stream: null,
330
+ payload: buildTokenPayload(payload, timestamp),
331
+ timestamp,
332
+ });
333
+ break;
334
+ }
335
+ case "task_complete": {
336
+ const turnId = typeof payload.turn_id === "string" && payload.turn_id.trim()
337
+ ? payload.turn_id.trim()
338
+ : currentTurnId;
339
+ if (turnId && activeTurnId === turnId) {
340
+ activeTurnId = null;
341
+ }
342
+ appendSemantic(index, {
343
+ type: "turn.completed",
344
+ turnId,
345
+ messageId: null,
346
+ callId: null,
347
+ requestId: null,
348
+ phase: null,
349
+ stream: null,
350
+ payload: {
351
+ completedAt: timestamp,
352
+ reason: null,
353
+ },
354
+ timestamp,
355
+ });
356
+ break;
357
+ }
358
+ case "turn_aborted": {
359
+ const turnId = typeof payload.turn_id === "string" && payload.turn_id.trim()
360
+ ? payload.turn_id.trim()
361
+ : currentTurnId;
362
+ if (turnId && activeTurnId === turnId) {
363
+ activeTurnId = null;
364
+ }
365
+ appendSemantic(index, {
366
+ type: "turn.aborted",
367
+ turnId,
368
+ messageId: null,
369
+ callId: null,
370
+ requestId: null,
371
+ phase: null,
372
+ stream: null,
373
+ payload: {
374
+ abortedAt: timestamp,
375
+ reason: typeof payload.reason === "string" ? payload.reason : null,
376
+ },
377
+ timestamp,
378
+ });
379
+ break;
380
+ }
381
+ case "agent_reasoning": {
382
+ const text = typeof payload.text === "string" ? payload.text.trim() : "";
383
+ if (!text) {
384
+ break;
385
+ }
386
+ const messageId = `msg_reasoning_${String((reasoningCounter += 1)).padStart(4, "0")}`;
387
+ appendSemantic(index, {
388
+ type: "reasoning.start",
389
+ turnId: currentTurnId,
390
+ messageId,
391
+ callId: null,
392
+ requestId: null,
393
+ phase: null,
394
+ stream: null,
395
+ payload: {
396
+ summary: "",
397
+ },
398
+ timestamp,
399
+ });
400
+ appendSemantic(index, {
401
+ type: "reasoning.delta",
402
+ turnId: currentTurnId,
403
+ messageId,
404
+ callId: null,
405
+ requestId: null,
406
+ phase: null,
407
+ stream: null,
408
+ payload: {
409
+ textDelta: text,
410
+ summary: text,
411
+ },
412
+ timestamp,
413
+ });
414
+ appendSemantic(index, {
415
+ type: "reasoning.end",
416
+ turnId: currentTurnId,
417
+ messageId,
418
+ callId: null,
419
+ requestId: null,
420
+ phase: null,
421
+ stream: null,
422
+ payload: {
423
+ summary: text,
424
+ },
425
+ timestamp,
426
+ });
427
+ break;
428
+ }
429
+ default:
430
+ break;
431
+ }
432
+ continue;
433
+ }
434
+ if (record.type !== "response_item") {
435
+ continue;
436
+ }
437
+ const payload = record.payload && typeof record.payload === "object"
438
+ ? record.payload
439
+ : {};
440
+ switch (payload.type) {
441
+ case "message": {
442
+ if (payload.role !== "assistant") {
443
+ break;
444
+ }
445
+ const text = extractMessageText(payload.content);
446
+ if (!text) {
447
+ break;
448
+ }
449
+ const phase = payload.phase === "commentary" ? "commentary" : "final_answer";
450
+ const messageId = `msg_assistant_${String((assistantCounter += 1)).padStart(4, "0")}`;
451
+ if (phase === "final_answer") {
452
+ downgradePreviousFinalAssistant(currentTurnId);
453
+ if (currentTurnId) {
454
+ lastFinalAssistantMessageIdByTurn.set(currentTurnId, messageId);
455
+ }
456
+ }
457
+ appendSemantic(index, {
458
+ type: "message.assistant.start",
459
+ turnId: currentTurnId,
460
+ messageId,
461
+ callId: null,
462
+ requestId: null,
463
+ phase,
464
+ stream: null,
465
+ payload: { text: "" },
466
+ timestamp,
467
+ });
468
+ appendSemantic(index, {
469
+ type: "message.assistant.delta",
470
+ turnId: currentTurnId,
471
+ messageId,
472
+ callId: null,
473
+ requestId: null,
474
+ phase,
475
+ stream: null,
476
+ payload: { textDelta: text },
477
+ timestamp,
478
+ });
479
+ appendSemantic(index, {
480
+ type: "message.assistant.end",
481
+ turnId: currentTurnId,
482
+ messageId,
483
+ callId: null,
484
+ requestId: null,
485
+ phase,
486
+ stream: null,
487
+ payload: {
488
+ text,
489
+ finishReason: null,
490
+ },
491
+ timestamp,
492
+ });
493
+ break;
494
+ }
495
+ case "reasoning": {
496
+ const summary = extractReasoningSummary(payload.summary);
497
+ if (!summary) {
498
+ break;
499
+ }
500
+ const messageId = `msg_reasoning_${String((reasoningCounter += 1)).padStart(4, "0")}`;
501
+ appendSemantic(index, {
502
+ type: "reasoning.start",
503
+ turnId: currentTurnId,
504
+ messageId,
505
+ callId: null,
506
+ requestId: null,
507
+ phase: null,
508
+ stream: null,
509
+ payload: {
510
+ summary: "",
511
+ },
512
+ timestamp,
513
+ });
514
+ appendSemantic(index, {
515
+ type: "reasoning.delta",
516
+ turnId: currentTurnId,
517
+ messageId,
518
+ callId: null,
519
+ requestId: null,
520
+ phase: null,
521
+ stream: null,
522
+ payload: {
523
+ textDelta: summary,
524
+ summary,
525
+ },
526
+ timestamp,
527
+ });
528
+ appendSemantic(index, {
529
+ type: "reasoning.end",
530
+ turnId: currentTurnId,
531
+ messageId,
532
+ callId: null,
533
+ requestId: null,
534
+ phase: null,
535
+ stream: null,
536
+ payload: {
537
+ summary,
538
+ },
539
+ timestamp,
540
+ });
541
+ break;
542
+ }
543
+ case "function_call": {
544
+ const callId = typeof payload.call_id === "string" && payload.call_id.trim()
545
+ ? payload.call_id.trim()
546
+ : `call_${index}`;
547
+ const args = safeJsonParse(payload.arguments) || {};
548
+ const commandPayload = buildCommandPayload(typeof payload.name === "string" ? payload.name : "tool_call", args);
549
+ commandStarts.set(callId, {
550
+ commandPayload: {
551
+ command: commandPayload.command,
552
+ cwd: commandPayload.cwd,
553
+ },
554
+ });
555
+ appendSemantic(index, {
556
+ type: "command.start",
557
+ turnId: currentTurnId,
558
+ messageId: null,
559
+ callId,
560
+ requestId: null,
561
+ phase: null,
562
+ stream: null,
563
+ payload: commandPayload,
564
+ timestamp,
565
+ });
566
+ break;
567
+ }
568
+ case "function_call_output": {
569
+ const callId = typeof payload.call_id === "string" && payload.call_id.trim()
570
+ ? payload.call_id.trim()
571
+ : `call_${index}`;
572
+ const started = commandStarts.get(callId) || null;
573
+ const parsed = parseExecOutput(payload.output);
574
+ if (parsed.outputText) {
575
+ appendSemantic(index, {
576
+ type: "command.output.delta",
577
+ turnId: currentTurnId,
578
+ messageId: null,
579
+ callId,
580
+ requestId: null,
581
+ phase: null,
582
+ stream: "stdout",
583
+ payload: {
584
+ stream: "stdout",
585
+ textDelta: parsed.outputText,
586
+ },
587
+ timestamp,
588
+ });
589
+ }
590
+ appendSemantic(index, {
591
+ type: "command.end",
592
+ turnId: currentTurnId,
593
+ messageId: null,
594
+ callId,
595
+ requestId: null,
596
+ phase: null,
597
+ stream: null,
598
+ payload: {
599
+ command: parsed.commandLine || started?.commandPayload.command || null,
600
+ cwd: started?.commandPayload.cwd || null,
601
+ status: parsed.exitCode == null
602
+ ? "completed"
603
+ : parsed.exitCode === 0
604
+ ? "completed"
605
+ : "failed",
606
+ exitCode: parsed.exitCode,
607
+ durationMs: parsed.durationMs,
608
+ rejected: false,
609
+ },
610
+ timestamp,
611
+ });
612
+ break;
613
+ }
614
+ case "custom_tool_call": {
615
+ const callId = typeof payload.call_id === "string" && payload.call_id.trim()
616
+ ? payload.call_id.trim()
617
+ : `patch_${index}`;
618
+ appendSemantic(index, {
619
+ type: "patch.start",
620
+ turnId: currentTurnId,
621
+ messageId: null,
622
+ callId,
623
+ requestId: null,
624
+ phase: null,
625
+ stream: null,
626
+ payload: buildPatchStartPayload(typeof payload.name === "string" ? payload.name : "custom_tool", payload.input),
627
+ timestamp,
628
+ });
629
+ break;
630
+ }
631
+ case "custom_tool_call_output": {
632
+ const callId = typeof payload.call_id === "string" && payload.call_id.trim()
633
+ ? payload.call_id.trim()
634
+ : `patch_${index}`;
635
+ const outputPayload = safeJsonParse(payload.output) || {};
636
+ const text = typeof outputPayload.output === "string"
637
+ ? outputPayload.output
638
+ : typeof payload.output === "string"
639
+ ? payload.output
640
+ : "";
641
+ const metadata = outputPayload.metadata && typeof outputPayload.metadata === "object"
642
+ ? outputPayload.metadata
643
+ : {};
644
+ const durationSeconds = typeof metadata.duration_seconds === "number"
645
+ ? metadata.duration_seconds
646
+ : Number(metadata.duration_seconds);
647
+ const exitCode = typeof metadata.exit_code === "number"
648
+ ? metadata.exit_code
649
+ : Number.isFinite(Number(metadata.exit_code))
650
+ ? Number(metadata.exit_code)
651
+ : null;
652
+ if (text) {
653
+ appendSemantic(index, {
654
+ type: "patch.output.delta",
655
+ turnId: currentTurnId,
656
+ messageId: null,
657
+ callId,
658
+ requestId: null,
659
+ phase: null,
660
+ stream: null,
661
+ payload: { textDelta: text },
662
+ timestamp,
663
+ });
664
+ }
665
+ appendSemantic(index, {
666
+ type: "patch.end",
667
+ turnId: currentTurnId,
668
+ messageId: null,
669
+ callId,
670
+ requestId: null,
671
+ phase: null,
672
+ stream: null,
673
+ payload: {
674
+ status: typeof exitCode === "number"
675
+ ? exitCode === 0
676
+ ? "completed"
677
+ : "failed"
678
+ : "completed",
679
+ durationMs: Number.isFinite(durationSeconds) && durationSeconds >= 0
680
+ ? Math.round(durationSeconds * 1000)
681
+ : null,
682
+ success: typeof exitCode === "number" ? exitCode === 0 : true,
683
+ },
684
+ timestamp,
685
+ });
686
+ break;
687
+ }
688
+ default:
689
+ break;
690
+ }
691
+ }
692
+ const firstTimestamp = semanticEvents[0]?.timestamp || nowIso();
693
+ const lastTimestamp = semanticEvents[semanticEvents.length - 1]?.timestamp || firstTimestamp || nowIso();
694
+ const sourceRolloutHasOpenTurn = computeSourceRolloutHasOpenTurnFromRecords(records);
695
+ return {
696
+ codexSessionId,
697
+ workspacePath,
698
+ sessionTitle: firstUserMessage
699
+ ? `Imported Codex: ${shorten(firstUserMessage, 60)}`
700
+ : `Imported Codex Session ${codexSessionId.slice(0, 8)}`,
701
+ sessionStatus: "waiting_input",
702
+ sourceRolloutHasOpenTurn,
703
+ firstTimestamp,
704
+ lastTimestamp,
705
+ events: semanticEvents,
706
+ rawCursor: records.length,
707
+ };
708
+ }
709
+ class CodexRolloutSyncService {
710
+ db;
711
+ constructor(db) {
712
+ this.db = db;
713
+ }
714
+ listImportableSessions(limit = 20) {
715
+ const rolloutRoot = node_path_1.default.join(resolveCodexHomeDir(), "sessions");
716
+ const rolloutPaths = scanRolloutPaths(rolloutRoot).slice(0, Math.max(1, limit));
717
+ const importedByPath = new Map(this.db
718
+ .prepare(`
719
+ SELECT id AS session_id, source_rollout_path, source_last_synced_at AS imported_at
720
+ FROM sessions
721
+ WHERE source_kind = 'imported_rollout'
722
+ AND source_rollout_path IS NOT NULL
723
+ `)
724
+ .all()
725
+ .filter((row) => typeof row.source_rollout_path === "string" && row.source_rollout_path)
726
+ .map((row) => [
727
+ node_path_1.default.resolve(String(row.source_rollout_path)),
728
+ { session_id: row.session_id, imported_at: row.imported_at },
729
+ ]));
730
+ const nativeThreadIds = new Set(this.db
731
+ .prepare(`
732
+ SELECT codex_thread_id
733
+ FROM sessions
734
+ WHERE source_kind = 'native'
735
+ AND codex_thread_id IS NOT NULL
736
+ AND codex_thread_id != ''
737
+ `)
738
+ .all()
739
+ .map((row) => String(row.codex_thread_id || "").trim())
740
+ .filter(Boolean));
741
+ return rolloutPaths
742
+ .map((rolloutPath) => {
743
+ const records = parseJsonlRecords(rolloutPath);
744
+ const sessionMeta = records.find((record) => record.type === "session_meta")?.payload;
745
+ const codexSessionId = String(sessionMeta?.id || "").trim();
746
+ const cwd = typeof sessionMeta?.cwd === "string" && sessionMeta.cwd.trim()
747
+ ? sessionMeta.cwd.trim()
748
+ : null;
749
+ const firstUserEvent = records.find((record) => record.type === "event_msg" &&
750
+ record.payload &&
751
+ typeof record.payload === "object" &&
752
+ record.payload.type === "user_message");
753
+ const firstUserMessage = firstUserEvent &&
754
+ typeof firstUserEvent.payload.message === "string"
755
+ ? String(firstUserEvent.payload.message).trim()
756
+ : "";
757
+ const resolvedPath = node_path_1.default.resolve(rolloutPath);
758
+ const imported = importedByPath.get(resolvedPath);
759
+ return {
760
+ codexSessionId,
761
+ rolloutPath: resolvedPath,
762
+ cwd,
763
+ updatedAt: (0, node_fs_1.statSync)(rolloutPath).mtime.toISOString(),
764
+ title: firstUserMessage ? shorten(firstUserMessage, 80) : null,
765
+ importedSessionId: imported?.session_id ?? null,
766
+ importedAt: imported?.imported_at ?? null,
767
+ };
768
+ })
769
+ .filter((item) => !item.codexSessionId || !nativeThreadIds.has(item.codexSessionId));
770
+ }
771
+ importRollout(rolloutPathInput) {
772
+ const rolloutPath = node_path_1.default.resolve(rolloutPathInput.trim());
773
+ if (!rolloutPathInput.trim()) {
774
+ throw new errors_1.AppError(400, "rolloutPath is required.");
775
+ }
776
+ if (!(0, node_fs_1.existsSync)(rolloutPath)) {
777
+ throw new errors_1.AppError(404, "Rollout file not found.");
778
+ }
779
+ const existing = this.db
780
+ .prepare(`
781
+ SELECT id
782
+ FROM sessions
783
+ WHERE source_kind = 'imported_rollout'
784
+ AND source_rollout_path = ?
785
+ LIMIT 1
786
+ `)
787
+ .get(rolloutPath);
788
+ if (existing) {
789
+ const sync = this.syncImportedSession(existing.id);
790
+ return {
791
+ sessionId: existing.id,
792
+ imported: false,
793
+ syncedEvents: sync.appendedEvents,
794
+ };
795
+ }
796
+ const translated = translateRolloutRecords(parseJsonlRecords(rolloutPath), 0);
797
+ const sessionId = (0, ids_1.createId)("sess");
798
+ const projectId = this.findOrCreateProjectForImportedRollout(translated.workspacePath, translated.firstTimestamp);
799
+ const insertEvent = this.db.prepare(`
800
+ INSERT INTO session_events (
801
+ id,
802
+ session_id,
803
+ turn_id,
804
+ seq,
805
+ event_type,
806
+ message_id,
807
+ call_id,
808
+ request_id,
809
+ phase,
810
+ stream,
811
+ payload_json,
812
+ created_at
813
+ )
814
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
815
+ `);
816
+ runInTransaction(this.db, () => {
817
+ this.db
818
+ .prepare(`
819
+ INSERT INTO sessions (
820
+ id,
821
+ title,
822
+ project_id,
823
+ status,
824
+ pid,
825
+ codex_thread_id,
826
+ source_kind,
827
+ source_rollout_path,
828
+ source_thread_id,
829
+ source_sync_cursor,
830
+ source_last_synced_at,
831
+ source_rollout_has_open_turn,
832
+ created_at,
833
+ updated_at
834
+ )
835
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
836
+ `)
837
+ .run(sessionId, translated.sessionTitle, projectId, translated.sessionStatus, null, translated.codexSessionId, "imported_rollout", rolloutPath, translated.codexSessionId, translated.rawCursor, translated.lastTimestamp, translated.sourceRolloutHasOpenTurn ? 1 : 0, translated.firstTimestamp, translated.lastTimestamp);
838
+ translated.events.forEach((event, index) => {
839
+ insertEvent.run(`${sessionId}_import_${String(index + 1).padStart(6, "0")}`, sessionId, event.turnId, index + 1, event.type, event.messageId, event.callId, event.requestId, event.phase, event.stream, JSON.stringify(event.payload ?? {}), event.timestamp);
840
+ });
841
+ });
842
+ return {
843
+ sessionId,
844
+ imported: true,
845
+ syncedEvents: translated.events.length,
846
+ };
847
+ }
848
+ syncImportedSession(sessionId) {
849
+ const session = this.db
850
+ .prepare(`
851
+ SELECT *
852
+ FROM sessions
853
+ WHERE id = ?
854
+ LIMIT 1
855
+ `)
856
+ .get(sessionId);
857
+ if (!session) {
858
+ throw new errors_1.AppError(404, "Session not found.");
859
+ }
860
+ if (session.source_kind !== "imported_rollout" || !session.source_rollout_path) {
861
+ return {
862
+ sessionId,
863
+ synced: false,
864
+ appendedEvents: 0,
865
+ reason: "not-imported",
866
+ };
867
+ }
868
+ if (session.status === "starting" || session.status === "running" || session.status === "stopping") {
869
+ return {
870
+ sessionId,
871
+ synced: false,
872
+ appendedEvents: 0,
873
+ reason: "live-runtime",
874
+ };
875
+ }
876
+ const rolloutPath = node_path_1.default.resolve(session.source_rollout_path);
877
+ if (!(0, node_fs_1.existsSync)(rolloutPath)) {
878
+ throw new errors_1.AppError(404, "Imported rollout source no longer exists.");
879
+ }
880
+ const records = parseJsonlRecords(rolloutPath);
881
+ const cursor = Math.max(0, session.source_sync_cursor ?? 0);
882
+ if (records.length <= cursor) {
883
+ return {
884
+ sessionId,
885
+ synced: false,
886
+ appendedEvents: 0,
887
+ reason: "up-to-date",
888
+ };
889
+ }
890
+ const translated = translateRolloutRecords(records, cursor);
891
+ if (translated.events.length === 0) {
892
+ this.db
893
+ .prepare(`
894
+ UPDATE sessions
895
+ SET
896
+ source_sync_cursor = ?,
897
+ source_last_synced_at = ?,
898
+ source_rollout_has_open_turn = ?,
899
+ updated_at = ?
900
+ WHERE id = ?
901
+ `)
902
+ .run(records.length, translated.lastTimestamp, translated.sourceRolloutHasOpenTurn ? 1 : 0, nowIso(), sessionId);
903
+ return {
904
+ sessionId,
905
+ synced: true,
906
+ appendedEvents: 0,
907
+ };
908
+ }
909
+ const currentMaxSeq = this.db
910
+ .prepare("SELECT COALESCE(MAX(seq), 0) AS value FROM session_events WHERE session_id = ?")
911
+ .get(sessionId).value;
912
+ const insertEvent = this.db.prepare(`
913
+ INSERT INTO session_events (
914
+ id,
915
+ session_id,
916
+ turn_id,
917
+ seq,
918
+ event_type,
919
+ message_id,
920
+ call_id,
921
+ request_id,
922
+ phase,
923
+ stream,
924
+ payload_json,
925
+ created_at
926
+ )
927
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
928
+ `);
929
+ runInTransaction(this.db, () => {
930
+ translated.events.forEach((event, index) => {
931
+ const seq = currentMaxSeq + index + 1;
932
+ insertEvent.run(`${sessionId}_sync_${String(seq).padStart(6, "0")}`, sessionId, event.turnId, seq, event.type, event.messageId, event.callId, event.requestId, event.phase, event.stream, JSON.stringify(event.payload ?? {}), event.timestamp);
933
+ });
934
+ this.db
935
+ .prepare(`
936
+ UPDATE sessions
937
+ SET
938
+ title = COALESCE(title, ?),
939
+ codex_thread_id = COALESCE(codex_thread_id, ?),
940
+ source_thread_id = COALESCE(source_thread_id, ?),
941
+ source_sync_cursor = ?,
942
+ source_last_synced_at = ?,
943
+ source_rollout_has_open_turn = ?,
944
+ updated_at = ?
945
+ WHERE id = ?
946
+ `)
947
+ .run(translated.sessionTitle, translated.codexSessionId, translated.codexSessionId, translated.rawCursor, translated.lastTimestamp, translated.sourceRolloutHasOpenTurn ? 1 : 0, nowIso(), sessionId);
948
+ });
949
+ return {
950
+ sessionId,
951
+ synced: true,
952
+ appendedEvents: translated.events.length,
953
+ };
954
+ }
955
+ findOrCreateProjectForImportedRollout(workspacePath, createdAt) {
956
+ const existing = this.db
957
+ .prepare(`
958
+ SELECT id
959
+ FROM projects
960
+ WHERE path = ?
961
+ LIMIT 1
962
+ `)
963
+ .get(workspacePath);
964
+ if (existing?.id) {
965
+ return existing.id;
966
+ }
967
+ const projectId = (0, ids_1.createId)("proj");
968
+ this.db
969
+ .prepare(`
970
+ INSERT INTO projects (id, name, path, created_at)
971
+ VALUES (?, ?, ?, ?)
972
+ `)
973
+ .run(projectId, `Imported Rollout: ${node_path_1.default.basename(workspacePath)}`, workspacePath, createdAt);
974
+ return projectId;
975
+ }
976
+ }
977
+ exports.CodexRolloutSyncService = CodexRolloutSyncService;