@supen-ai/cli 1.3.3 → 1.4.0

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 (138) hide show
  1. package/README.md +1 -1
  2. package/daemon/dist/agent-sdk/driver-output-ui.d.ts +1 -1
  3. package/daemon/dist/agent-sdk/driver-output-ui.d.ts.map +1 -1
  4. package/daemon/dist/agent-sdk/driver-output-ui.js +10 -275
  5. package/daemon/dist/agent-sdk/driver-output-ui.js.map +1 -1
  6. package/daemon/dist/agent-sdk/drivers/codex-app-server-driver.d.ts.map +1 -1
  7. package/daemon/dist/agent-sdk/drivers/codex-app-server-driver.js +0 -2
  8. package/daemon/dist/agent-sdk/drivers/codex-app-server-driver.js.map +1 -1
  9. package/daemon/dist/agent-sdk/drivers/registry.d.ts +2 -8
  10. package/daemon/dist/agent-sdk/drivers/registry.d.ts.map +1 -1
  11. package/daemon/dist/agent-sdk/drivers/registry.js +7 -138
  12. package/daemon/dist/agent-sdk/drivers/registry.js.map +1 -1
  13. package/daemon/dist/agent-sdk/index.d.ts +0 -11
  14. package/daemon/dist/agent-sdk/index.d.ts.map +1 -1
  15. package/daemon/dist/agent-sdk/index.js +0 -11
  16. package/daemon/dist/agent-sdk/index.js.map +1 -1
  17. package/daemon/dist/agent-sdk/session-manager.d.ts +1 -1
  18. package/daemon/dist/agent-sdk/session-manager.d.ts.map +1 -1
  19. package/daemon/dist/agent-sdk/session-manager.js +1 -2
  20. package/daemon/dist/agent-sdk/session-manager.js.map +1 -1
  21. package/daemon/dist/channels/acp.d.ts.map +1 -1
  22. package/daemon/dist/channels/acp.js +2 -3
  23. package/daemon/dist/channels/acp.js.map +1 -1
  24. package/daemon/dist/channels/http-routes.js +6 -6
  25. package/daemon/dist/channels/http-routes.js.map +1 -1
  26. package/daemon/dist/core/config.d.ts +1 -1
  27. package/daemon/dist/core/config.d.ts.map +1 -1
  28. package/daemon/dist/core/config.js +1 -1
  29. package/daemon/dist/core/config.js.map +1 -1
  30. package/daemon/dist/core/cortex.d.ts.map +1 -1
  31. package/daemon/dist/core/cortex.js +90 -222
  32. package/daemon/dist/core/cortex.js.map +1 -1
  33. package/daemon/dist/core/env.js +1 -1
  34. package/daemon/dist/core/gateway.d.ts.map +1 -1
  35. package/daemon/dist/core/gateway.js +1 -11
  36. package/daemon/dist/core/gateway.js.map +1 -1
  37. package/daemon/dist/core/loop-guard.d.ts +1 -1
  38. package/daemon/dist/core/loop-guard.js +1 -1
  39. package/daemon/dist/core/protocol-adapter.d.ts +1 -3
  40. package/daemon/dist/core/protocol-adapter.d.ts.map +1 -1
  41. package/daemon/dist/core/protocol-adapter.js +1 -3
  42. package/daemon/dist/core/protocol-adapter.js.map +1 -1
  43. package/daemon/dist/core/thread-runtime-state.d.ts.map +1 -1
  44. package/daemon/dist/core/thread-runtime-state.js +1 -37
  45. package/daemon/dist/core/thread-runtime-state.js.map +1 -1
  46. package/daemon/dist/http/routes/rpc.d.ts.map +1 -1
  47. package/daemon/dist/http/routes/rpc.js +16 -1
  48. package/daemon/dist/http/routes/rpc.js.map +1 -1
  49. package/daemon/dist/http/routes/system.d.ts +2 -0
  50. package/daemon/dist/http/routes/system.d.ts.map +1 -1
  51. package/daemon/dist/http/routes/system.js +487 -482
  52. package/daemon/dist/http/routes/system.js.map +1 -1
  53. package/daemon/dist/mcp/index.d.ts +2 -6
  54. package/daemon/dist/mcp/index.d.ts.map +1 -1
  55. package/daemon/dist/mcp/index.js +2 -6
  56. package/daemon/dist/mcp/index.js.map +1 -1
  57. package/daemon/dist/mcp/tools.d.ts +5 -7
  58. package/daemon/dist/mcp/tools.d.ts.map +1 -1
  59. package/daemon/dist/mcp/tools.js +4 -8
  60. package/daemon/dist/mcp/tools.js.map +1 -1
  61. package/daemon/dist/tools/built-ins.d.ts +9 -84
  62. package/daemon/dist/tools/built-ins.d.ts.map +1 -1
  63. package/daemon/dist/tools/built-ins.js +3 -3
  64. package/daemon/dist/tools/built-ins.js.map +1 -1
  65. package/daemon/dist/tools/local-tool.d.ts +7 -0
  66. package/daemon/dist/tools/local-tool.d.ts.map +1 -0
  67. package/daemon/dist/tools/local-tool.js +4 -0
  68. package/daemon/dist/tools/local-tool.js.map +1 -0
  69. package/daemon/dist/tools/skill-tools.d.ts +5 -8
  70. package/daemon/dist/tools/skill-tools.d.ts.map +1 -1
  71. package/daemon/dist/tools/skill-tools.js +6 -10
  72. package/daemon/dist/tools/skill-tools.js.map +1 -1
  73. package/daemon/package.json +6 -9
  74. package/dist/backend.js +5 -5
  75. package/dist/backend.js.map +1 -1
  76. package/dist/bootstrap.js +2 -6
  77. package/dist/bootstrap.js.map +1 -1
  78. package/dist/computer.js +1 -1
  79. package/dist/doctor.js +1 -1
  80. package/dist/doctor.js.map +1 -1
  81. package/dist/index.js +1 -1
  82. package/dist/repl.js +6 -66
  83. package/dist/repl.js.map +1 -1
  84. package/dist/service.js +2 -3
  85. package/dist/service.js.map +1 -1
  86. package/package.json +3 -6
  87. package/daemon/dist/agent-sdk/drivers/acpx-driver.d.ts +0 -21
  88. package/daemon/dist/agent-sdk/drivers/acpx-driver.d.ts.map +0 -1
  89. package/daemon/dist/agent-sdk/drivers/acpx-driver.js +0 -488
  90. package/daemon/dist/agent-sdk/drivers/acpx-driver.js.map +0 -1
  91. package/daemon/dist/agent-sdk/drivers/claude-acpx-driver.d.ts +0 -5
  92. package/daemon/dist/agent-sdk/drivers/claude-acpx-driver.d.ts.map +0 -1
  93. package/daemon/dist/agent-sdk/drivers/claude-acpx-driver.js +0 -7
  94. package/daemon/dist/agent-sdk/drivers/claude-acpx-driver.js.map +0 -1
  95. package/daemon/dist/agent-sdk/drivers/claude-cli-direct-driver.d.ts +0 -20
  96. package/daemon/dist/agent-sdk/drivers/claude-cli-direct-driver.d.ts.map +0 -1
  97. package/daemon/dist/agent-sdk/drivers/claude-cli-direct-driver.js +0 -264
  98. package/daemon/dist/agent-sdk/drivers/claude-cli-direct-driver.js.map +0 -1
  99. package/daemon/dist/agent-sdk/drivers/claude-code-driver.d.ts +0 -29
  100. package/daemon/dist/agent-sdk/drivers/claude-code-driver.d.ts.map +0 -1
  101. package/daemon/dist/agent-sdk/drivers/claude-code-driver.js +0 -24
  102. package/daemon/dist/agent-sdk/drivers/claude-code-driver.js.map +0 -1
  103. package/daemon/dist/agent-sdk/drivers/claude-code-events.d.ts +0 -25
  104. package/daemon/dist/agent-sdk/drivers/claude-code-events.d.ts.map +0 -1
  105. package/daemon/dist/agent-sdk/drivers/claude-code-events.js +0 -58
  106. package/daemon/dist/agent-sdk/drivers/claude-code-events.js.map +0 -1
  107. package/daemon/dist/agent-sdk/drivers/claude-code-items.d.ts +0 -41
  108. package/daemon/dist/agent-sdk/drivers/claude-code-items.d.ts.map +0 -1
  109. package/daemon/dist/agent-sdk/drivers/claude-code-items.js +0 -77
  110. package/daemon/dist/agent-sdk/drivers/claude-code-items.js.map +0 -1
  111. package/daemon/dist/agent-sdk/drivers/codex-acpx-driver.d.ts +0 -5
  112. package/daemon/dist/agent-sdk/drivers/codex-acpx-driver.d.ts.map +0 -1
  113. package/daemon/dist/agent-sdk/drivers/codex-acpx-driver.js +0 -7
  114. package/daemon/dist/agent-sdk/drivers/codex-acpx-driver.js.map +0 -1
  115. package/daemon/dist/agent-sdk/drivers/codex-exec-driver.d.ts +0 -28
  116. package/daemon/dist/agent-sdk/drivers/codex-exec-driver.d.ts.map +0 -1
  117. package/daemon/dist/agent-sdk/drivers/codex-exec-driver.js +0 -219
  118. package/daemon/dist/agent-sdk/drivers/codex-exec-driver.js.map +0 -1
  119. package/daemon/dist/agent-sdk/drivers/codex-exec-events.d.ts +0 -9
  120. package/daemon/dist/agent-sdk/drivers/codex-exec-events.d.ts.map +0 -1
  121. package/daemon/dist/agent-sdk/drivers/codex-exec-events.js +0 -82
  122. package/daemon/dist/agent-sdk/drivers/codex-exec-events.js.map +0 -1
  123. package/daemon/dist/agent-sdk/drivers/codex-exec-items.d.ts +0 -9
  124. package/daemon/dist/agent-sdk/drivers/codex-exec-items.d.ts.map +0 -1
  125. package/daemon/dist/agent-sdk/drivers/codex-exec-items.js +0 -86
  126. package/daemon/dist/agent-sdk/drivers/codex-exec-items.js.map +0 -1
  127. package/daemon/dist/agent-sdk/drivers/gemini-cli-driver.d.ts +0 -17
  128. package/daemon/dist/agent-sdk/drivers/gemini-cli-driver.d.ts.map +0 -1
  129. package/daemon/dist/agent-sdk/drivers/gemini-cli-driver.js +0 -255
  130. package/daemon/dist/agent-sdk/drivers/gemini-cli-driver.js.map +0 -1
  131. package/daemon/dist/core/sdk-wrapper.d.ts +0 -36
  132. package/daemon/dist/core/sdk-wrapper.d.ts.map +0 -1
  133. package/daemon/dist/core/sdk-wrapper.js +0 -533
  134. package/daemon/dist/core/sdk-wrapper.js.map +0 -1
  135. package/daemon/dist/skills/claude_code.d.ts +0 -25
  136. package/daemon/dist/skills/claude_code.d.ts.map +0 -1
  137. package/daemon/dist/skills/claude_code.js +0 -49
  138. package/daemon/dist/skills/claude_code.js.map +0 -1
@@ -6,7 +6,7 @@ import { createRequire } from 'module';
6
6
  import { spawn, spawnSync } from 'child_process';
7
7
  import { DAEMON_HOSTNAME, DEFAULT_MODEL, MODELS_REGISTRY, SUPEN_HOME, reloadModelsRegistry, } from '../../core/config.js';
8
8
  import { readConfigSummary, readConfigYamlFile, writeConfigYamlFile, } from '../../core/env.js';
9
- import { readSpaceEnvMap, updateSpaceEnvMap, withSupenLlmEnv } from '../../core/space-env.js';
9
+ import { readSpaceEnvMap, updateSpaceEnvMap, withRuntimeSpaceEnv, withSupenLlmEnv } from '../../core/space-env.js';
10
10
  import { readCodexSubscription } from '../../core/codex-subscription.js';
11
11
  import { buildHubSnapshotForSpace } from '../../core/hub-snapshot.js';
12
12
  import { readEnrollmentState, createEnrollmentToken, verifyEnrollmentToken, setTrustState } from '../../core/enrollment.js';
@@ -22,6 +22,7 @@ import { logger } from '../../core/logger.js';
22
22
  import { buildDaemonOpenApiSpec } from '../../channels/http-routes.js';
23
23
  import { writeJson, writeProtocolError, readJsonBody } from '../response.js';
24
24
  import { listRecentThreadEventsAfter, readThreadEventLogHead, } from '../../core/thread-event-log.js';
25
+ import { projectThreadRuntimeState } from '../../core/thread-runtime-state.js';
25
26
  import { addThreadStreamClient, removeThreadStreamClient, writeThreadStreamEvent } from '../thread-stream.js';
26
27
  const DEFAULT_TOKEN_TTL_MINUTES = 20;
27
28
  const SPACE_LOG_EVENT_LIMIT = 200;
@@ -29,22 +30,23 @@ const SPACE_LOG_STREAM_POLL_MS = 1000;
29
30
  const SPACE_LOG_STREAM_PING_MS = 15000;
30
31
  const MIRRORED_THREAD_LIMIT = 80;
31
32
  const MIRRORED_THREAD_HISTORY_LIMIT = 100;
32
- const MIRRORED_THREAD_HISTORY_MAX_BYTES = 64 * 1024 * 1024;
33
+ const MIRRORED_THREAD_HISTORY_MAX_BYTES = 256 * 1024 * 1024;
34
+ const MIRRORED_THREAD_PROJECTION_VERSION = 2;
33
35
  const MIRRORED_THREAD_TEXT_LIMIT = 48_000;
34
- const MIRRORED_THREAD_TOOL_STRING_LIMIT = 24_000;
35
- const MIRRORED_THREAD_OBJECT_DEPTH_LIMIT = 4;
36
- const MIRRORED_THREAD_OBJECT_KEY_LIMIT = 80;
37
- const MIRRORED_THREAD_OBJECT_ARRAY_LIMIT = 80;
38
36
  const MIRRORED_THREAD_INLINE_DATA_URL_LIMIT = 120_000;
39
37
  const MIRRORED_THREAD_STREAM_REPLAY_LIMIT = 500;
40
38
  const MIRRORED_THREAD_STREAM_REPLAY_MAX_BYTES = 8 * 1024 * 1024;
39
+ const MIRRORED_THREAD_STREAM_JSONL_POLL_MS = 500;
40
+ const MIRRORED_THREAD_STREAM_JSONL_READ_MAX_BYTES = 2 * 1024 * 1024;
41
+ const MIRRORED_THREAD_STATUS_TAIL_MAX_BYTES = 256 * 1024;
41
42
  const MIRRORED_THREAD_RUNNING_LEASE_MS = 30 * 60 * 1000;
42
43
  const MIRRORED_THREAD_HISTORY_CHUNK_BYTES = 1024 * 1024;
44
+ const MIRRORED_THREAD_SESSION_FILE_INDEX_TTL_MS = 2_000;
43
45
  const require = createRequire(import.meta.url);
44
46
  const HOST_DAEMON_INSTALL_DIR = path.join(SUPEN_HOME, 'daemon');
45
47
  const HOST_DAEMON_CLI_PACKAGE_ROOT = path.join(HOST_DAEMON_INSTALL_DIR, 'node_modules', '@supen-ai', 'cli');
46
48
  const HOST_DAEMON_BIN_PATH = path.join(HOST_DAEMON_CLI_PACKAGE_ROOT, 'daemon', 'scripts', 'supen-daemon.js');
47
- const HOST_DAEMON_PACKAGE_SPEC = '@supen-ai/cli';
49
+ const HOST_DAEMON_PACKAGE_SPEC = '@supen-ai/cli@latest';
48
50
  function readSqliteRows(dbPath, query) {
49
51
  try {
50
52
  const { DatabaseSync } = require('node:sqlite');
@@ -437,6 +439,9 @@ export function readRecentJsonlLines(filePath, maxBytes = MIRRORED_THREAD_HISTOR
437
439
  }
438
440
  }
439
441
  function isCodexHistoryMessageLine(line) {
442
+ if (!line.includes('"type":"message"') && !line.includes('"type":"user_message"')) {
443
+ return false;
444
+ }
440
445
  const parsed = parseJsonLine(line);
441
446
  const payload = parsed?.payload && typeof parsed.payload === 'object'
442
447
  ? parsed.payload
@@ -444,8 +449,78 @@ function isCodexHistoryMessageLine(line) {
444
449
  return ((parsed?.type === 'event_msg' && payload.type === 'user_message') ||
445
450
  (parsed?.type === 'response_item' && payload.type === 'message'));
446
451
  }
452
+ function isCodexVisibleUserHistoryLine(line) {
453
+ const parsed = parseJsonLine(line);
454
+ const payload = parsed?.payload && typeof parsed.payload === 'object'
455
+ ? parsed.payload
456
+ : {};
457
+ if (parsed?.type === 'event_msg' && payload.type === 'user_message') {
458
+ const text = typeof payload.message === 'string' ? payload.message.trim() : '';
459
+ return Boolean(text && !isInternalMirroredUserMessage(text));
460
+ }
461
+ if (parsed?.type !== 'response_item' || payload.type !== 'message' || payload.role !== 'user') {
462
+ return false;
463
+ }
464
+ const text = mirroredMessageContentParts(payload.content).text;
465
+ return Boolean(text && !isInternalMirroredUserMessage(sanitizeMirroredUserText(text)));
466
+ }
467
+ function isCodexHistoryTranscriptEventLine(line) {
468
+ if (!line.includes('"type":"function_call"') &&
469
+ !line.includes('"type":"custom_tool_call"') &&
470
+ !line.includes('"type":"patch_apply_end"') &&
471
+ !line.includes('"type":"tool_search_call"') &&
472
+ !line.includes('"type":"web_search_call"') &&
473
+ !line.includes('"type":"mcp_tool_call"')) {
474
+ return false;
475
+ }
476
+ const parsed = parseJsonLine(line);
477
+ const payload = parsed?.payload && typeof parsed.payload === 'object'
478
+ ? parsed.payload
479
+ : {};
480
+ if (parsed?.type === 'event_msg') {
481
+ return payload.type === 'patch_apply_end';
482
+ }
483
+ if (parsed?.type !== 'response_item')
484
+ return false;
485
+ return (payload.type === 'function_call' ||
486
+ payload.type === 'custom_tool_call' ||
487
+ payload.type === 'tool_search_call' ||
488
+ payload.type === 'web_search_call' ||
489
+ payload.type === 'mcp_tool_call');
490
+ }
491
+ function isCodexHistoryTranscriptLine(line) {
492
+ return isCodexHistoryMessageLine(line) || isCodexHistoryTranscriptEventLine(line);
493
+ }
494
+ function isCodexJsonlMessageRecord(parsed) {
495
+ const payload = parsed?.payload && typeof parsed.payload === 'object'
496
+ ? parsed.payload
497
+ : {};
498
+ return parsed?.type === 'response_item' && payload.type === 'message';
499
+ }
500
+ function codexJsonlEventChunk(parsed) {
501
+ const payload = parsed.payload && typeof parsed.payload === 'object'
502
+ ? parsed.payload
503
+ : {};
504
+ const payloadType = typeof payload.type === 'string' ? payload.type : '';
505
+ return {
506
+ type: 'data-codex-event',
507
+ data: {
508
+ eventType: `codex-jsonl/${parsed.type || 'record'}${payloadType ? `/${payloadType}` : ''}`,
509
+ raw: parsed,
510
+ },
511
+ };
512
+ }
513
+ function codexAppServerEventChunk(method, params = {}) {
514
+ return {
515
+ type: 'data-codex-event',
516
+ data: {
517
+ eventType: method,
518
+ raw: { method, params },
519
+ },
520
+ };
521
+ }
447
522
  function readRecentCodexHistoryLines(filePath, targetMessages, maxBytes = MIRRORED_THREAD_HISTORY_MAX_BYTES) {
448
- const safeTargetMessages = Math.max(1, Math.min(Math.floor(targetMessages), 501));
523
+ const safeTargetMessages = Math.max(1, Math.min(Math.floor(targetMessages), 5_000));
449
524
  const safeMaxBytes = Math.max(1, Math.min(Math.floor(maxBytes), MIRRORED_THREAD_HISTORY_MAX_BYTES));
450
525
  const size = fs.statSync(filePath).size;
451
526
  const fd = fs.openSync(filePath, 'r');
@@ -468,12 +543,9 @@ function readRecentCodexHistoryLines(filePath, targetMessages, maxBytes = MIRROR
468
543
  lines.unshift(leadingPartial);
469
544
  leadingPartial = '';
470
545
  }
471
- const completeLines = lines.filter(Boolean);
472
- for (const line of completeLines) {
473
- if (isCodexHistoryMessageLine(line))
474
- messageCount += 1;
475
- }
476
- collected = [...completeLines, ...collected];
546
+ const historyLines = lines.filter((line) => line && isCodexHistoryTranscriptLine(line));
547
+ messageCount += historyLines.filter(isCodexVisibleUserHistoryLine).length;
548
+ collected = [...historyLines, ...collected];
477
549
  }
478
550
  return collected.filter(Boolean);
479
551
  }
@@ -578,7 +650,8 @@ function readThreadStateEntries(limit) {
578
650
  return [];
579
651
  }
580
652
  }
581
- function walkSessionFiles(dir, out = new Map()) {
653
+ let mirroredSessionFileIndexCache = null;
654
+ function walkSessionFilesUncached(dir, out = new Map()) {
582
655
  if (!fs.existsSync(dir))
583
656
  return out;
584
657
  let entries;
@@ -591,7 +664,7 @@ function walkSessionFiles(dir, out = new Map()) {
591
664
  for (const entry of entries) {
592
665
  const fullPath = path.join(dir, entry.name);
593
666
  if (entry.isDirectory()) {
594
- walkSessionFiles(fullPath, out);
667
+ walkSessionFilesUncached(fullPath, out);
595
668
  continue;
596
669
  }
597
670
  if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
@@ -602,6 +675,21 @@ function walkSessionFiles(dir, out = new Map()) {
602
675
  }
603
676
  return out;
604
677
  }
678
+ function walkSessionFiles(dir) {
679
+ const now = Date.now();
680
+ if (mirroredSessionFileIndexCache &&
681
+ mirroredSessionFileIndexCache.root === dir &&
682
+ now - mirroredSessionFileIndexCache.indexedAt <= MIRRORED_THREAD_SESSION_FILE_INDEX_TTL_MS) {
683
+ return new Map(mirroredSessionFileIndexCache.files);
684
+ }
685
+ const files = walkSessionFilesUncached(dir);
686
+ mirroredSessionFileIndexCache = {
687
+ root: dir,
688
+ indexedAt: now,
689
+ files: new Map(files),
690
+ };
691
+ return files;
692
+ }
605
693
  function readFileHead(filePath, maxBytes = 64 * 1024) {
606
694
  const fd = fs.openSync(filePath, 'r');
607
695
  try {
@@ -626,6 +714,34 @@ function readFileTail(filePath, maxBytes = 256 * 1024) {
626
714
  fs.closeSync(fd);
627
715
  }
628
716
  }
717
+ function readAppendedJsonlLines(filePath, offset) {
718
+ if (!fs.existsSync(filePath))
719
+ return { offset, lines: [], partial: '' };
720
+ const stat = fs.statSync(filePath);
721
+ if (stat.size <= offset)
722
+ return { offset: stat.size, lines: [], partial: '' };
723
+ const bytesToRead = Math.min(stat.size - offset, MIRRORED_THREAD_STREAM_JSONL_READ_MAX_BYTES);
724
+ const readStart = stat.size - offset > MIRRORED_THREAD_STREAM_JSONL_READ_MAX_BYTES
725
+ ? stat.size - MIRRORED_THREAD_STREAM_JSONL_READ_MAX_BYTES
726
+ : offset;
727
+ const fd = fs.openSync(filePath, 'r');
728
+ try {
729
+ const buffer = Buffer.alloc(bytesToRead);
730
+ const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, readStart);
731
+ const text = buffer.subarray(0, bytesRead).toString('utf-8');
732
+ const parts = text.split(/\r?\n/);
733
+ const partial = text.endsWith('\n') || text.endsWith('\r') ? '' : (parts.pop() || '');
734
+ const completeLines = parts.filter(Boolean);
735
+ return {
736
+ offset: readStart + bytesRead - Buffer.byteLength(partial),
737
+ lines: readStart === offset ? completeLines : completeLines.slice(1),
738
+ partial,
739
+ };
740
+ }
741
+ finally {
742
+ fs.closeSync(fd);
743
+ }
744
+ }
629
745
  function readThreadWorkspacePath(filePath) {
630
746
  if (!filePath)
631
747
  return null;
@@ -649,6 +765,91 @@ function readThreadWorkspacePath(filePath) {
649
765
  }
650
766
  return null;
651
767
  }
768
+ function startCodexThreadObserver(input) {
769
+ const child = spawn('codex', ['app-server', '--listen', 'stdio://'], {
770
+ cwd: input.cwd,
771
+ stdio: ['pipe', 'pipe', 'pipe'],
772
+ env: withRuntimeSpaceEnv(process.env),
773
+ });
774
+ let buffer = '';
775
+ let nextId = 1;
776
+ let observerEventIndex = 0;
777
+ let initialized = false;
778
+ const send = (message) => {
779
+ if (!child.stdin.writable)
780
+ return;
781
+ child.stdin.write(`${JSON.stringify(message)}\n`);
782
+ };
783
+ const request = (method, params) => {
784
+ send({ id: nextId++, method, ...(params ? { params } : {}) });
785
+ };
786
+ child.stdout.on('data', (chunk) => {
787
+ buffer += String(chunk);
788
+ while (true) {
789
+ const newlineIndex = buffer.indexOf('\n');
790
+ if (newlineIndex < 0)
791
+ break;
792
+ const line = buffer.slice(0, newlineIndex).trim();
793
+ buffer = buffer.slice(newlineIndex + 1);
794
+ if (!line.startsWith('{'))
795
+ continue;
796
+ let parsed;
797
+ try {
798
+ parsed = JSON.parse(line);
799
+ }
800
+ catch {
801
+ continue;
802
+ }
803
+ if (parsed.id !== undefined && typeof parsed.method === 'string') {
804
+ send({ id: parsed.id, result: {} });
805
+ continue;
806
+ }
807
+ if (parsed.id !== undefined) {
808
+ if (!initialized && parsed.id === 1) {
809
+ initialized = true;
810
+ send({ method: 'initialized' });
811
+ request('thread/resume', {
812
+ threadId: input.threadId,
813
+ cwd: input.cwd,
814
+ experimentalRawEvents: true,
815
+ });
816
+ }
817
+ continue;
818
+ }
819
+ const method = typeof parsed.method === 'string' ? parsed.method : '';
820
+ if (!method)
821
+ continue;
822
+ const params = parsed.params && typeof parsed.params === 'object'
823
+ ? parsed.params
824
+ : {};
825
+ const eventThreadId = typeof params.threadId === 'string' ? params.threadId : '';
826
+ if (eventThreadId !== input.threadId)
827
+ continue;
828
+ observerEventIndex += 1;
829
+ input.write(codexAppServerEventChunk(method, params), `observer-${Date.now()}-${observerEventIndex}`);
830
+ }
831
+ });
832
+ child.stderr.on('data', () => { });
833
+ child.once('error', (error) => {
834
+ logger.debug({ err: error, threadId: input.threadId }, 'Codex thread observer failed');
835
+ });
836
+ request('initialize', {
837
+ clientInfo: {
838
+ name: 'supen-daemon-thread-observer',
839
+ title: 'Supen',
840
+ version: '0.1.0',
841
+ },
842
+ capabilities: { experimentalApi: true },
843
+ });
844
+ return () => {
845
+ try {
846
+ child.kill('SIGTERM');
847
+ }
848
+ catch {
849
+ // Observer cleanup is best-effort; the HTTP stream is already closing.
850
+ }
851
+ };
852
+ }
652
853
  function normalizeThreadStatusToken(value) {
653
854
  return typeof value === 'string'
654
855
  ? value.trim().toLowerCase().replace(/[-_\s]/g, '')
@@ -701,7 +902,7 @@ function readThreadStatus(filePath) {
701
902
  let sawUserTurn = false;
702
903
  let completedAfterLastUser = false;
703
904
  let lastRunningActivityMs = null;
704
- const tail = readFileTail(filePath, MIRRORED_THREAD_STREAM_REPLAY_MAX_BYTES);
905
+ const tail = readFileTail(filePath, MIRRORED_THREAD_STATUS_TAIL_MAX_BYTES);
705
906
  for (const line of tail.split(/\r?\n/)) {
706
907
  if (!line.trim())
707
908
  continue;
@@ -711,7 +912,9 @@ function readThreadStatus(filePath) {
711
912
  ? parsed.payload
712
913
  : {};
713
914
  if (parsed?.type === 'event_msg') {
714
- if (payload.type === 'task_started' || payload.type === 'user_message') {
915
+ if (payload.type === 'task_started' ||
916
+ payload.type === 'user_message' ||
917
+ payload.type === 'thread_goal_updated') {
715
918
  sawUserTurn = true;
716
919
  completedAfterLastUser = false;
717
920
  lastRunningActivityMs = timestampMs;
@@ -734,6 +937,26 @@ function readThreadStatus(filePath) {
734
937
  lastRunningActivityMs = timestampMs;
735
938
  continue;
736
939
  }
940
+ if (parsed?.type === 'response_item' &&
941
+ payload.type === 'message' &&
942
+ payload.role === 'assistant') {
943
+ if (payload.phase === 'final_answer') {
944
+ completedAfterLastUser = true;
945
+ continue;
946
+ }
947
+ if (sawUserTurn) {
948
+ completedAfterLastUser = false;
949
+ lastRunningActivityMs = timestampMs;
950
+ }
951
+ continue;
952
+ }
953
+ if (parsed?.type === 'response_item' &&
954
+ sawUserTurn &&
955
+ !completedAfterLastUser &&
956
+ payload.type !== 'reasoning') {
957
+ lastRunningActivityMs = timestampMs;
958
+ continue;
959
+ }
737
960
  const raw = payload.raw && typeof payload.raw === 'object'
738
961
  ? payload.raw
739
962
  : null;
@@ -778,6 +1001,17 @@ function readThreadStatus(filePath) {
778
1001
  return 'idle';
779
1002
  }
780
1003
  }
1004
+ function readMirroredThreadStatus(threadId, filePath) {
1005
+ try {
1006
+ const runtimeState = projectThreadRuntimeState(threadId);
1007
+ if (isRunningThreadStatusToken(runtimeState.runtime.status))
1008
+ return 'running';
1009
+ }
1010
+ catch {
1011
+ // Fall back to the Codex session file status below.
1012
+ }
1013
+ return readThreadStatus(filePath);
1014
+ }
781
1015
  function projectNameFromPath(projectPath) {
782
1016
  if (!projectPath)
783
1017
  return 'No workspace';
@@ -896,7 +1130,7 @@ function readMirroredTaskProjects(limit = MIRRORED_THREAD_LIMIT) {
896
1130
  id: entry.id,
897
1131
  title,
898
1132
  updated_at: entry.updated_at,
899
- status: readThreadStatus(sessionPath),
1133
+ status: readMirroredThreadStatus(entry.id, sessionPath),
900
1134
  session_path: sessionPath,
901
1135
  });
902
1136
  projects.set(projectId, project);
@@ -927,7 +1161,7 @@ function readMirroredTaskProjects(limit = MIRRORED_THREAD_LIMIT) {
927
1161
  id: entry.id,
928
1162
  title: entry.thread_name,
929
1163
  updated_at: entry.updated_at,
930
- status: readThreadStatus(sessionPath),
1164
+ status: readMirroredThreadStatus(entry.id, sessionPath),
931
1165
  session_path: sessionPath,
932
1166
  });
933
1167
  projects.set(projectId, project);
@@ -953,34 +1187,6 @@ function truncateMirroredHistoryString(value, limit, label) {
953
1187
  const omitted = value.length - limit;
954
1188
  return `${value.slice(0, limit)}\n\n[${label} truncated: ${omitted.toLocaleString()} more characters omitted]`;
955
1189
  }
956
- function sanitizeMirroredHistoryPayload(value, depth = 0) {
957
- if (typeof value === 'string') {
958
- return truncateMirroredHistoryString(value, MIRRORED_THREAD_TOOL_STRING_LIMIT, 'Tool output');
959
- }
960
- if (value === null || typeof value !== 'object')
961
- return value;
962
- if (depth >= MIRRORED_THREAD_OBJECT_DEPTH_LIMIT) {
963
- return Array.isArray(value) ? '[Nested array omitted]' : '[Nested object omitted]';
964
- }
965
- if (Array.isArray(value)) {
966
- const visibleItems = value
967
- .slice(0, MIRRORED_THREAD_OBJECT_ARRAY_LIMIT)
968
- .map((item) => sanitizeMirroredHistoryPayload(item, depth + 1));
969
- if (value.length > MIRRORED_THREAD_OBJECT_ARRAY_LIMIT) {
970
- visibleItems.push(`[${value.length - MIRRORED_THREAD_OBJECT_ARRAY_LIMIT} more items omitted]`);
971
- }
972
- return visibleItems;
973
- }
974
- const result = {};
975
- const entries = Object.entries(value);
976
- for (const [key, entryValue] of entries.slice(0, MIRRORED_THREAD_OBJECT_KEY_LIMIT)) {
977
- result[key] = sanitizeMirroredHistoryPayload(entryValue, depth + 1);
978
- }
979
- if (entries.length > MIRRORED_THREAD_OBJECT_KEY_LIMIT) {
980
- result.__truncated_keys = `${entries.length - MIRRORED_THREAD_OBJECT_KEY_LIMIT} more keys omitted`;
981
- }
982
- return result;
983
- }
984
1190
  function safeMirroredHistoryAttachmentUrl(url) {
985
1191
  const trimmed = url.trim();
986
1192
  if (trimmed.startsWith('data:') &&
@@ -1082,117 +1288,15 @@ function isInternalMirroredUserMessage(text) {
1082
1288
  const trimmed = text.trimStart();
1083
1289
  return (trimmed.startsWith('<environment_context>') ||
1084
1290
  trimmed.startsWith('<developer_context>') ||
1291
+ trimmed.startsWith('<codex_internal_context') ||
1292
+ trimmed.startsWith('<goal_context>') ||
1293
+ trimmed.startsWith('<turn_aborted>') ||
1085
1294
  trimmed.startsWith('[plugin:vite:'));
1086
1295
  }
1087
1296
  function isMirroredGoalContextMessage(text) {
1088
1297
  const trimmed = text.trim();
1089
1298
  return trimmed.startsWith('<goal_context>') && trimmed.endsWith('</goal_context>');
1090
1299
  }
1091
- function parseJsonObject(value) {
1092
- if (typeof value !== 'string')
1093
- return value;
1094
- try {
1095
- return JSON.parse(value);
1096
- }
1097
- catch {
1098
- return value;
1099
- }
1100
- }
1101
- function readableOutputFromMcpResult(result) {
1102
- if (!result || typeof result !== 'object')
1103
- return result;
1104
- const ok = result.Ok;
1105
- const error = result.Err;
1106
- const payload = ok && typeof ok === 'object' ? ok : error;
1107
- if (!payload || typeof payload !== 'object')
1108
- return result;
1109
- const content = payload.content;
1110
- if (!Array.isArray(content))
1111
- return payload;
1112
- const text = content
1113
- .map((entry) => {
1114
- if (typeof entry === 'string')
1115
- return entry;
1116
- if (entry && typeof entry === 'object' && typeof entry.text === 'string') {
1117
- return entry.text;
1118
- }
1119
- return '';
1120
- })
1121
- .filter(Boolean)
1122
- .join('\n');
1123
- return text || payload;
1124
- }
1125
- function fileNameFromPath(value) {
1126
- const trimmed = value.trim();
1127
- if (!trimmed)
1128
- return '';
1129
- return path.basename(trimmed);
1130
- }
1131
- function displayPatchPath(filePath, workspacePath) {
1132
- const trimmed = filePath.trim();
1133
- if (!trimmed || !workspacePath || !path.isAbsolute(trimmed))
1134
- return trimmed;
1135
- const relativePath = path.relative(workspacePath, trimmed);
1136
- if (!relativePath || relativePath.startsWith('..') || path.isAbsolute(relativePath))
1137
- return trimmed;
1138
- return relativePath;
1139
- }
1140
- function diffStatsFromText(value) {
1141
- let additions = 0;
1142
- let deletions = 0;
1143
- for (const line of value.split(/\r?\n/)) {
1144
- if (line.startsWith('+++') || line.startsWith('---'))
1145
- continue;
1146
- if (line.startsWith('+'))
1147
- additions += 1;
1148
- if (line.startsWith('-'))
1149
- deletions += 1;
1150
- }
1151
- return { additions, deletions };
1152
- }
1153
- function extractPatchFiles(input) {
1154
- if (typeof input !== 'string')
1155
- return [];
1156
- const files = [];
1157
- const seen = new Set();
1158
- for (const line of input.split(/\r?\n/)) {
1159
- const match = line.match(/^\*\*\* (?:Update|Add|Delete) File:\s+(.+?)\s*$/);
1160
- if (!match)
1161
- continue;
1162
- const filePath = match[1]?.trim();
1163
- if (!filePath || seen.has(filePath))
1164
- continue;
1165
- seen.add(filePath);
1166
- files.push({ path: filePath, name: fileNameFromPath(filePath) || filePath });
1167
- }
1168
- return files;
1169
- }
1170
- function extractPatchEndFiles(changes, workspacePath) {
1171
- if (!changes || typeof changes !== 'object')
1172
- return [];
1173
- return Object.entries(changes).map(([filePath, change]) => {
1174
- const diff = typeof change?.unified_diff === 'string' ? change.unified_diff : '';
1175
- const displayPath = displayPatchPath(filePath, workspacePath);
1176
- return {
1177
- path: displayPath,
1178
- name: fileNameFromPath(displayPath) || displayPath,
1179
- ...(diff ? { diff } : {}),
1180
- ...diffStatsFromText(diff),
1181
- };
1182
- });
1183
- }
1184
- function codexReplayEvent(eventId, timestamp, taskId, raw) {
1185
- return {
1186
- id: eventId,
1187
- timestamp,
1188
- task_id: taskId,
1189
- chunk: {
1190
- type: 'data-codex-event',
1191
- id: eventId,
1192
- data: { raw },
1193
- },
1194
- };
1195
- }
1196
1300
  function mirroredThreadWorkspacePath(sessionPath, stateEntry) {
1197
1301
  const stateCwd = typeof stateEntry?.cwd === 'string' ? stateEntry.cwd : null;
1198
1302
  const sessionWorkspace = readThreadWorkspacePath(sessionPath);
@@ -1202,6 +1306,53 @@ function mirroredThreadWorkspacePath(sessionPath, stateEntry) {
1202
1306
  }
1203
1307
  return null;
1204
1308
  }
1309
+ function latestRawThreadEventTimestampMs(events) {
1310
+ let latest = null;
1311
+ for (const event of events) {
1312
+ const timestampMs = Date.parse(event.received_at);
1313
+ if (!Number.isFinite(timestampMs))
1314
+ continue;
1315
+ latest = latest === null ? timestampMs : Math.max(latest, timestampMs);
1316
+ }
1317
+ return latest;
1318
+ }
1319
+ function rawThreadEventsAreStaleForHistory(events, latestHistoryTimestamp) {
1320
+ const latestEventTimestampMs = latestRawThreadEventTimestampMs(events);
1321
+ if (latestEventTimestampMs === null)
1322
+ return false;
1323
+ const latestHistoryTimestampMs = Date.parse(latestHistoryTimestamp);
1324
+ if (!Number.isFinite(latestHistoryTimestampMs))
1325
+ return false;
1326
+ return latestEventTimestampMs + 30_000 < latestHistoryTimestampMs;
1327
+ }
1328
+ function collapseCompletedAssistantProgressMessages(input) {
1329
+ const output = [];
1330
+ let assistantRun = [];
1331
+ const flushAssistantRun = () => {
1332
+ if (assistantRun.length === 0)
1333
+ return;
1334
+ const finalAnswers = assistantRun.filter((message) => message.phase === 'final_answer');
1335
+ if (finalAnswers.length > 0) {
1336
+ output.push(...finalAnswers);
1337
+ }
1338
+ else {
1339
+ output.push(...assistantRun);
1340
+ }
1341
+ assistantRun = [];
1342
+ };
1343
+ for (const message of input) {
1344
+ if (message.role === 'assistant') {
1345
+ assistantRun.push(message);
1346
+ continue;
1347
+ }
1348
+ flushAssistantRun();
1349
+ output.push(message);
1350
+ }
1351
+ flushAssistantRun();
1352
+ return output
1353
+ .filter((message) => message.internal !== true)
1354
+ .map(({ phase: _phase, internal: _internal, ...message }) => message);
1355
+ }
1205
1356
  export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY_LIMIT, options = {}) {
1206
1357
  const safeLimit = Math.max(1, Math.min(limit, 500));
1207
1358
  const sessionPath = walkSessionFiles(path.join(localAgentHome(), 'sessions')).get(threadId);
@@ -1212,24 +1363,50 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1212
1363
  const workspacePath = mirroredThreadWorkspacePath(sessionPath, stateEntry);
1213
1364
  const indexEntry = readThreadIndexEntry(threadId);
1214
1365
  const eventLogHead = readThreadEventLogHead(eventLogThreadId);
1215
- const lines = readRecentCodexHistoryLines(sessionPath, safeLimit + 1);
1366
+ const historyLineTarget = Math.min(120, Math.max(3, Math.ceil(safeLimit / 4)));
1367
+ const lines = readRecentCodexHistoryLines(sessionPath, historyLineTarget);
1216
1368
  const messages = [];
1217
1369
  const events = [];
1218
1370
  let messageIndex = 0;
1219
1371
  let eventIndex = 0;
1220
1372
  let latestTimestamp = indexEntry?.updated_at || '';
1221
1373
  let recentGoalContext = null;
1222
- const toolCalls = new Map();
1223
1374
  const mirroredUserMessageKey = (text) => sanitizeMirroredUserText(text)
1224
1375
  .replace(/\s+/g, ' ')
1225
1376
  .trim();
1226
1377
  const pushUserMessage = (text, timestamp, messageId, attachments = []) => {
1227
1378
  text = sanitizeMirroredUserText(text);
1379
+ const pushInternalBoundary = () => {
1380
+ let lastVisibleMessage;
1381
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
1382
+ if (messages[index].internal === true)
1383
+ continue;
1384
+ lastVisibleMessage = messages[index];
1385
+ break;
1386
+ }
1387
+ if (lastVisibleMessage?.role !== 'assistant' || lastVisibleMessage.phase !== 'final_answer') {
1388
+ return;
1389
+ }
1390
+ messages.push({
1391
+ id: messageId || `${threadId}-internal-${messageIndex + 1}`,
1392
+ role: 'system',
1393
+ content: '',
1394
+ timestamp,
1395
+ task_id: threadId,
1396
+ internal: true,
1397
+ });
1398
+ };
1228
1399
  if (isMirroredGoalContextMessage(text)) {
1229
1400
  recentGoalContext = { text, timestamp };
1401
+ pushInternalBoundary();
1402
+ return;
1230
1403
  }
1231
1404
  text = truncateMirroredHistoryString(text, MIRRORED_THREAD_TEXT_LIMIT, 'Message');
1232
- if ((!text && attachments.length === 0) || isInternalMirroredUserMessage(text))
1405
+ if (isInternalMirroredUserMessage(text)) {
1406
+ pushInternalBoundary();
1407
+ return;
1408
+ }
1409
+ if (!text && attachments.length === 0)
1233
1410
  return;
1234
1411
  const key = mirroredUserMessageKey(text);
1235
1412
  const timestampMs = Date.parse(timestamp);
@@ -1309,37 +1486,13 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1309
1486
  const messageId = typeof payload.id === 'string' ? payload.id : `${threadId}-web-user-${event.sequence}`;
1310
1487
  pushUserMessage(text, timestamp, messageId, attachments);
1311
1488
  };
1312
- const pushEvent = (timestamp, chunk) => {
1489
+ const pushCodexJsonlEvent = (parsed, timestamp) => {
1313
1490
  eventIndex += 1;
1314
1491
  events.push({
1315
- id: `${threadId}-event-${eventIndex}`,
1492
+ id: `${threadId}-jsonl-event-${eventIndex}`,
1316
1493
  timestamp,
1317
1494
  task_id: threadId,
1318
- chunk,
1319
- });
1320
- };
1321
- const pushToolInput = (timestamp, toolCallId, toolName, input) => {
1322
- const sanitizedInput = sanitizeMirroredHistoryPayload(input);
1323
- toolCalls.set(toolCallId, { toolName, input: sanitizedInput });
1324
- pushEvent(timestamp, {
1325
- type: 'tool-input-available',
1326
- toolCallId,
1327
- toolName,
1328
- input: sanitizedInput,
1329
- });
1330
- };
1331
- const pushToolOutput = (timestamp, toolCallId, output, fallbackToolName, fallbackInput, failed = false) => {
1332
- const toolCall = toolCalls.get(toolCallId);
1333
- const toolName = toolCall?.toolName || fallbackToolName;
1334
- const input = toolCall?.input ?? (fallbackInput === undefined ? undefined : sanitizeMirroredHistoryPayload(fallbackInput));
1335
- pushEvent(timestamp, {
1336
- type: failed ? 'tool-output-error' : 'tool-output-available',
1337
- toolCallId,
1338
- ...(toolName ? { toolName } : {}),
1339
- ...(input !== undefined ? { input } : {}),
1340
- ...(failed
1341
- ? { errorText: typeof output === 'string' ? output : 'Tool failed' }
1342
- : { output: sanitizeMirroredHistoryPayload(parseJsonObject(output)) }),
1495
+ chunk: codexJsonlEventChunk(parsed),
1343
1496
  });
1344
1497
  };
1345
1498
  for (const line of lines) {
@@ -1356,148 +1509,20 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1356
1509
  if (payload.type === 'user_message') {
1357
1510
  const text = typeof payload.message === 'string' ? payload.message.trim() : '';
1358
1511
  pushUserMessage(text, timestamp, undefined, imageAttachmentsFromUnknown(payload.images));
1359
- continue;
1360
- }
1361
- if (payload.type === 'turn_aborted') {
1362
- if (eventLogThreadId !== threadId) {
1363
- events.push(codexReplayEvent(`${threadId}-turn-aborted-${eventIndex + 1}`, timestamp, threadId, {
1364
- method: 'turn/completed',
1365
- params: {
1366
- turn: {
1367
- id: payload.turn_id || payload.turnId || 'turn',
1368
- status: 'interrupted',
1369
- },
1370
- },
1371
- }));
1372
- eventIndex += 1;
1373
- }
1374
- continue;
1375
- }
1376
- if (payload.type === 'context_compacted') {
1377
- events.push(codexReplayEvent(`${threadId}-context-compacted-${eventIndex + 1}`, timestamp, threadId, {
1378
- method: 'contextCompaction/completed',
1379
- params: {
1380
- threadId,
1381
- messageCount: payload.message_count || payload.messageCount,
1382
- tokensSaved: payload.tokens_saved || payload.tokensSaved,
1383
- },
1384
- }));
1385
- eventIndex += 1;
1386
- continue;
1387
- }
1388
- if (payload.type === 'web_search_end') {
1389
- const toolCallId = typeof payload.call_id === 'string' ? payload.call_id : `${threadId}-web-search-${eventIndex + 1}`;
1390
- const input = {
1391
- query: payload.query,
1392
- action: payload.action,
1393
- };
1394
- pushToolInput(timestamp, toolCallId, 'web_search', input);
1395
- pushToolOutput(timestamp, toolCallId, payload.action || payload.query || 'completed');
1396
- continue;
1397
1512
  }
1398
- if (payload.type === 'mcp_tool_call_end') {
1399
- const toolCallId = typeof payload.call_id === 'string' ? payload.call_id : `${threadId}-mcp-tool-${eventIndex + 1}`;
1400
- const invocation = payload.invocation && typeof payload.invocation === 'object'
1401
- ? payload.invocation
1402
- : {};
1403
- const server = typeof invocation.server === 'string' ? invocation.server : '';
1404
- const tool = typeof invocation.tool === 'string' ? invocation.tool : '';
1405
- const toolName = [server, tool].filter(Boolean).join('.') || 'mcpToolCall';
1406
- const input = invocation.arguments;
1407
- const result = payload.result && typeof payload.result === 'object'
1408
- ? payload.result
1409
- : payload.result;
1410
- const failed = Boolean(result && typeof result === 'object' && 'Err' in result);
1411
- pushToolInput(timestamp, toolCallId, toolName, input);
1412
- pushToolOutput(timestamp, toolCallId, readableOutputFromMcpResult(result), toolName, input, failed);
1413
- continue;
1414
- }
1415
- if (payload.type === 'patch_apply_end') {
1416
- const files = extractPatchEndFiles(payload.changes, workspacePath);
1417
- const additions = files.reduce((sum, file) => sum + file.additions, 0);
1418
- const deletions = files.reduce((sum, file) => sum + file.deletions, 0);
1419
- events.push(codexReplayEvent(`${threadId}-patch-end-${eventIndex + 1}`, timestamp, threadId, {
1420
- method: 'codex/patchApply/completed',
1421
- params: {
1422
- callId: payload.call_id,
1423
- success: payload.success !== false,
1424
- files,
1425
- fileCount: files.length,
1426
- additions,
1427
- deletions,
1428
- status: payload.status,
1429
- },
1430
- }));
1431
- eventIndex += 1;
1432
- continue;
1513
+ else if (payload.type === 'patch_apply_end') {
1514
+ pushCodexJsonlEvent(parsed, timestamp);
1433
1515
  }
1434
- }
1435
- if (parsed?.type === 'compacted') {
1436
- events.push(codexReplayEvent(`${threadId}-context-compacted-${eventIndex + 1}`, timestamp, threadId, {
1437
- method: 'contextCompaction/completed',
1438
- params: { threadId },
1439
- }));
1440
- eventIndex += 1;
1441
1516
  continue;
1442
1517
  }
1443
1518
  if (parsed?.type !== 'response_item')
1444
1519
  continue;
1445
- if (payload.type === 'function_call') {
1446
- const toolCallId = typeof payload.call_id === 'string' ? payload.call_id : `${threadId}-tool-${eventIndex + 1}`;
1447
- const toolName = typeof payload.name === 'string' ? payload.name : 'tool';
1448
- const input = sanitizeMirroredHistoryPayload(parseJsonObject(payload.arguments));
1449
- pushToolInput(timestamp, toolCallId, toolName, input);
1450
- continue;
1451
- }
1452
- if (payload.type === 'function_call_output') {
1453
- const toolCallId = typeof payload.call_id === 'string' ? payload.call_id : `${threadId}-tool-${eventIndex + 1}`;
1454
- pushToolOutput(timestamp, toolCallId, payload.output);
1455
- continue;
1456
- }
1457
- if (payload.type === 'tool_search_call') {
1458
- const toolCallId = typeof payload.call_id === 'string' ? payload.call_id : `${threadId}-tool-search-${eventIndex + 1}`;
1459
- pushToolInput(timestamp, toolCallId, 'tool_search', payload.arguments);
1460
- continue;
1461
- }
1462
- if (payload.type === 'tool_search_output') {
1463
- const toolCallId = typeof payload.call_id === 'string' ? payload.call_id : `${threadId}-tool-search-${eventIndex + 1}`;
1464
- pushToolOutput(timestamp, toolCallId, {
1465
- status: payload.status,
1466
- tools: payload.tools,
1467
- }, 'tool_search');
1468
- continue;
1469
- }
1470
- if (payload.type === 'custom_tool_call') {
1471
- const toolName = typeof payload.name === 'string' ? payload.name : 'custom_tool';
1472
- const toolCallId = typeof payload.call_id === 'string' ? payload.call_id : `${threadId}-tool-${eventIndex + 1}`;
1473
- if (toolName === 'apply_patch') {
1474
- const input = typeof payload.input === 'string' ? payload.input : '';
1475
- const files = extractPatchFiles(input);
1476
- events.push(codexReplayEvent(`${threadId}-patch-start-${eventIndex + 1}`, timestamp, threadId, {
1477
- method: 'codex/patchApply/started',
1478
- params: {
1479
- callId: payload.call_id,
1480
- files,
1481
- fileCount: files.length,
1482
- ...diffStatsFromText(input),
1483
- },
1484
- }));
1485
- eventIndex += 1;
1486
- }
1487
- else {
1488
- pushToolInput(timestamp, toolCallId, toolName, parseJsonObject(payload.input));
1520
+ if (payload.type !== 'message') {
1521
+ if (isCodexHistoryTranscriptEventLine(line)) {
1522
+ pushCodexJsonlEvent(parsed, timestamp);
1489
1523
  }
1490
1524
  continue;
1491
1525
  }
1492
- if (payload.type === 'custom_tool_call_output') {
1493
- const toolCallId = typeof payload.call_id === 'string' ? payload.call_id : `${threadId}-tool-${eventIndex + 1}`;
1494
- if (toolCalls.has(toolCallId)) {
1495
- pushToolOutput(timestamp, toolCallId, payload.output);
1496
- }
1497
- continue;
1498
- }
1499
- if (payload.type !== 'message')
1500
- continue;
1501
1526
  messageIndex += 1;
1502
1527
  const messageId = `${threadId}-message-${messageIndex}`;
1503
1528
  const role = payload.role;
@@ -1517,16 +1542,22 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1517
1542
  content: truncateMirroredHistoryString(text, MIRRORED_THREAD_TEXT_LIMIT, 'Message'),
1518
1543
  timestamp,
1519
1544
  task_id: threadId,
1545
+ ...(typeof payload.phase === 'string' && payload.phase.trim()
1546
+ ? { phase: payload.phase.trim() }
1547
+ : {}),
1520
1548
  });
1521
1549
  }
1522
1550
  const eventLogEvents = listRecentThreadEventsAfter(eventLogThreadId, 0, {
1523
1551
  limit: MIRRORED_THREAD_STREAM_REPLAY_LIMIT,
1524
1552
  maxBytes: MIRRORED_THREAD_STREAM_REPLAY_MAX_BYTES,
1525
1553
  });
1526
- for (const event of eventLogEvents) {
1554
+ const usableEventLogEvents = rawThreadEventsAreStaleForHistory(eventLogEvents, latestTimestamp)
1555
+ ? []
1556
+ : eventLogEvents;
1557
+ for (const event of usableEventLogEvents) {
1527
1558
  pushWebUserMessage(event);
1528
1559
  }
1529
- for (const event of eventLogEvents) {
1560
+ for (const event of usableEventLogEvents) {
1530
1561
  const chunk = threadStreamChunkForEvent(event);
1531
1562
  if (!chunk)
1532
1563
  continue;
@@ -1538,14 +1569,15 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1538
1569
  chunk,
1539
1570
  });
1540
1571
  }
1541
- const slicedMessages = messages.slice(-safeLimit);
1572
+ const collapsedMessages = collapseCompletedAssistantProgressMessages(messages);
1573
+ const slicedMessages = collapsedMessages.slice(-safeLimit);
1542
1574
  const slicedEvents = events.slice(-safeLimit * 8);
1543
- const status = readThreadStatus(sessionPath);
1575
+ const status = readMirroredThreadStatus(eventLogThreadId, sessionPath);
1544
1576
  const latestGoalContext = recentGoalContext;
1545
1577
  return {
1546
1578
  session: {
1547
1579
  task_id: threadId,
1548
- agent_id: 'local-agent-mirror',
1580
+ agent_id: 'codex',
1549
1581
  channel: 'local-agent',
1550
1582
  source_ref: `local-agent:${threadId}`,
1551
1583
  title: indexEntry?.thread_name || stateEntry?.title || slicedMessages.find((message) => message.role === 'user')?.content || 'New Task',
@@ -1561,14 +1593,14 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1561
1593
  items: [],
1562
1594
  runtimeState: {
1563
1595
  event_log_head: eventLogHead,
1564
- projection_version: 1,
1596
+ projection_version: MIRRORED_THREAD_PROJECTION_VERSION,
1565
1597
  event_log: { status: 'current' },
1566
1598
  runtime: {
1567
1599
  status: status === 'running' ? 'running' : 'idle',
1568
1600
  updated_at: latestTimestamp || undefined,
1569
1601
  },
1570
1602
  },
1571
- has_more: messages.length > safeLimit,
1603
+ has_more: collapsedMessages.length > safeLimit || lines.length >= historyLineTarget,
1572
1604
  ...(latestGoalContext ? { goal_context: latestGoalContext } : {}),
1573
1605
  };
1574
1606
  }
@@ -1616,14 +1648,14 @@ function coerceSingleQueryParam(value) {
1616
1648
  return value || null;
1617
1649
  }
1618
1650
  function codexThreadsListRoute(pathname) {
1619
- return /^\/api\/computers\/\{computer_id\}\/agents\/[^/]+\/codex\/threads$/.test(pathname);
1651
+ return /^\/api\/computers\/\{computer_id\}\/agents\/codex\/threads$/.test(pathname);
1620
1652
  }
1621
1653
  function codexThreadActionRoute(pathname, action) {
1622
1654
  const suffix = action === 'messages' ? 'messages' : action;
1623
- return pathname.match(new RegExp(`^/api/computers/\\{computer_id\\}/agents/[^/]+/codex/threads/([^/]+)/${suffix}$`));
1655
+ return pathname.match(new RegExp(`^/api/computers/\\{computer_id\\}/agents/codex/threads/([^/]+)/${suffix}$`));
1624
1656
  }
1625
1657
  function codexProjectsOpenRoute(pathname) {
1626
- return /^\/api\/computers\/\{computer_id\}\/agents\/[^/]+\/codex\/projects\/open$/.test(pathname);
1658
+ return /^\/api\/computers\/\{computer_id\}\/agents\/codex\/projects\/open$/.test(pathname);
1627
1659
  }
1628
1660
  function collectSpaceLogEntries(filters = {}) {
1629
1661
  const spaceId = currentSpaceId();
@@ -1708,7 +1740,7 @@ function threadStreamChunkForEvent(event) {
1708
1740
  !Array.isArray(event.raw_payload) &&
1709
1741
  typeof event.raw_payload.type === 'string' &&
1710
1742
  String(event.raw_payload.type).trim()) {
1711
- return normalizeStoredCodexThreadChunk(event.raw_payload);
1743
+ return event.raw_payload;
1712
1744
  }
1713
1745
  return {
1714
1746
  type: 'data-codex-event',
@@ -1718,28 +1750,6 @@ function threadStreamChunkForEvent(event) {
1718
1750
  },
1719
1751
  };
1720
1752
  }
1721
- function normalizeStoredCodexThreadChunk(chunk) {
1722
- if (chunk.type !== 'data-supen-event')
1723
- return chunk;
1724
- const data = chunk.data && typeof chunk.data === 'object' && !Array.isArray(chunk.data)
1725
- ? chunk.data
1726
- : {};
1727
- const raw = data.raw && typeof data.raw === 'object' && !Array.isArray(data.raw)
1728
- ? data.raw
1729
- : {};
1730
- return {
1731
- ...chunk,
1732
- type: 'data-codex-event',
1733
- data: {
1734
- ...data,
1735
- eventType: typeof data.eventType === 'string' && data.eventType.trim()
1736
- ? data.eventType
1737
- : typeof raw.method === 'string' && raw.method.trim()
1738
- ? raw.method
1739
- : 'codex-event',
1740
- },
1741
- };
1742
- }
1743
1753
  function buildMcpSettingsResponse() {
1744
1754
  const overrides = getMcpEnvOverrides();
1745
1755
  const runtimeEnv = { ...process.env };
@@ -1768,11 +1778,9 @@ function buildMcpSettingsResponse() {
1768
1778
  }
1769
1779
  const CODING_CLI_INSTALL_COMMANDS = {
1770
1780
  codex: { command: 'npm', args: ['install', '-g', '@openai/codex'] },
1771
- gemini: { command: 'npm', args: ['install', '-g', '@google/gemini-cli'] },
1772
1781
  };
1773
1782
  const MANAGED_CODING_CLI_INSTALL_COMMANDS = {
1774
1783
  codex: CODING_CLI_INSTALL_COMMANDS.codex,
1775
- gemini: CODING_CLI_INSTALL_COMMANDS.gemini,
1776
1784
  };
1777
1785
  function resolveManagedCodingCliInstallCommand(cli, current) {
1778
1786
  if (cli === 'codex' && current.install_source === 'homebrew') {
@@ -1783,7 +1791,7 @@ function resolveManagedCodingCliInstallCommand(cli, current) {
1783
1791
  }
1784
1792
  return MANAGED_CODING_CLI_INSTALL_COMMANDS[cli];
1785
1793
  }
1786
- const CODING_CLI_NAMES = ['codex', 'gemini', 'claude'];
1794
+ const CODING_CLI_NAMES = ['codex'];
1787
1795
  const DETECTABLE_SPACE_APPS = [
1788
1796
  { id: 'libreoffice', command: 'libreoffice', managed: false },
1789
1797
  { id: 'dotnet', command: 'dotnet', managed: false },
@@ -1806,18 +1814,18 @@ function nowIso() {
1806
1814
  }
1807
1815
  function normalizeCliName(raw) {
1808
1816
  const value = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
1809
- if (value === 'codex' || value === 'gemini' || value === 'claude')
1817
+ if (value === 'codex')
1810
1818
  return value;
1811
1819
  return null;
1812
1820
  }
1813
1821
  function normalizeCodexTransport(raw) {
1814
1822
  const value = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
1815
- if (value === 'app-server' || value === 'exec' || value === 'acpx')
1823
+ if (value === 'app-server')
1816
1824
  return value;
1817
1825
  return null;
1818
1826
  }
1819
1827
  function isManagedCliName(name) {
1820
- return name === 'codex' || name === 'gemini';
1828
+ return name === 'codex';
1821
1829
  }
1822
1830
  function trimLogNoise(value) {
1823
1831
  const line = value.trim();
@@ -1902,7 +1910,6 @@ function readPackageVersion(packageRoot) {
1902
1910
  }
1903
1911
  }
1904
1912
  function detectDaemonPackageStatus() {
1905
- const envVersion = process.env.SUPEN_DAEMON_VERSION || process.env.npm_package_version || null;
1906
1913
  let packageRoot = null;
1907
1914
  try {
1908
1915
  packageRoot = findPackageRootFrom(require.resolve('@supen-ai/daemon'), '@supen-ai/daemon');
@@ -1945,7 +1952,7 @@ function detectDaemonPackageStatus() {
1945
1952
  ? 'local-package'
1946
1953
  : null;
1947
1954
  return {
1948
- version: envVersion || bundledDaemonVersion || packageVersion,
1955
+ version: bundledDaemonVersion || packageVersion,
1949
1956
  package_root: packageRoot || bundledDaemonRoot,
1950
1957
  install_source: installSource,
1951
1958
  update_supported: installSource === 'supen-cli-bundle' || installSource === 'npm-global',
@@ -2002,16 +2009,6 @@ function inferCliInstallSource(name, executablePath, resolvedPath) {
2002
2009
  return { install_source: 'node-package', update_supported: false };
2003
2010
  }
2004
2011
  }
2005
- if (name === 'gemini') {
2006
- const npmRoot = detectGlobalNpmRoot();
2007
- const geminiPackagePath = resolvedPath || executablePath || '';
2008
- if (npmRoot && geminiPackagePath.startsWith(`${npmRoot}${path.sep}@google${path.sep}gemini-cli${path.sep}`)) {
2009
- return { install_source: 'npm-global', update_supported: true };
2010
- }
2011
- if (normalized.includes('/node_modules/@google/gemini-cli/')) {
2012
- return { install_source: 'node-package', update_supported: false };
2013
- }
2014
- }
2015
2012
  return { install_source: null, update_supported: false };
2016
2013
  }
2017
2014
  function detectCliInstalled(name) {
@@ -2234,13 +2231,6 @@ function readCodingCliStatusPayload() {
2234
2231
  });
2235
2232
  const codexInstalled = clis.find((cli) => cli.name === 'codex')?.installed ?? false;
2236
2233
  const codexVersion = clis.find((cli) => cli.name === 'codex')?.version ?? null;
2237
- const gemini = clis.find((cli) => cli.name === 'gemini');
2238
- const claude = clis.find((cli) => cli.name === 'claude');
2239
- const acpxDetected = detectCliInstalled('acpx');
2240
- const selected_cli = process.env.SUPEN_CODING_CLI || readConfigSummary().coding_cli || 'codex';
2241
- const codexTransport = normalizeCodexTransport(process.env.SUPEN_CODEX_TRANSPORT) ||
2242
- normalizeCodexTransport(readConfigSummary().codex_transport) ||
2243
- 'app-server';
2244
2234
  const codexDefaultModel = readCodexDefaultModel();
2245
2235
  const codexModelOptions = readCodexModelOptions(codexDefaultModel);
2246
2236
  const installed_apps = DETECTABLE_SPACE_APPS.map((app) => {
@@ -2257,9 +2247,9 @@ function readCodingCliStatusPayload() {
2257
2247
  };
2258
2248
  });
2259
2249
  return {
2260
- selected_cli,
2261
- default_model: selected_cli === 'codex' ? codexDefaultModel : null,
2262
- model_options: selected_cli === 'codex' ? codexModelOptions : [],
2250
+ selected_cli: 'codex',
2251
+ default_model: codexDefaultModel,
2252
+ model_options: codexModelOptions,
2263
2253
  clis,
2264
2254
  agent_runtimes: [
2265
2255
  {
@@ -2271,53 +2261,9 @@ function readCodingCliStatusPayload() {
2271
2261
  model_options: codexModelOptions,
2272
2262
  transport: 'app-server',
2273
2263
  recommended: true,
2274
- default: selected_cli === 'codex' && codexTransport === 'app-server',
2264
+ default: true,
2275
2265
  description: 'Daemon-managed codex app-server process with protocol-native streaming.',
2276
2266
  },
2277
- {
2278
- id: 'codex-exec',
2279
- label: 'Codex CLI Exec',
2280
- installed: codexInstalled,
2281
- version: codexVersion,
2282
- default_model: codexDefaultModel,
2283
- model_options: codexModelOptions,
2284
- transport: 'exec',
2285
- recommended: false,
2286
- default: selected_cli === 'codex' && codexTransport === 'exec',
2287
- description: 'Direct codex exec fallback, typically one process per turn.',
2288
- },
2289
- {
2290
- id: 'codex-acpx',
2291
- label: 'Codex via ACPX',
2292
- installed: codexInstalled && acpxDetected.installed,
2293
- version: acpxDetected.version || codexVersion,
2294
- default_model: codexDefaultModel,
2295
- model_options: codexModelOptions,
2296
- transport: 'acpx',
2297
- recommended: false,
2298
- default: selected_cli === 'codex' && codexTransport === 'acpx',
2299
- description: 'Compatibility bridge through ACPX for hosts that still need it.',
2300
- },
2301
- {
2302
- id: 'claude-code',
2303
- label: 'Claude Code',
2304
- installed: Boolean(claude?.installed),
2305
- version: claude?.version ?? null,
2306
- transport: 'cli',
2307
- recommended: false,
2308
- default: selected_cli === 'claude',
2309
- description: 'Claude Code CLI runtime.',
2310
- },
2311
- {
2312
- id: 'gemini-cli',
2313
- label: 'Gemini CLI',
2314
- installed: Boolean(gemini?.installed),
2315
- version: gemini?.version ?? null,
2316
- transport: 'cli',
2317
- recommended: false,
2318
- default: selected_cli === 'gemini',
2319
- description: 'Gemini CLI runtime.',
2320
- },
2321
2267
  ],
2322
2268
  installed_apps,
2323
2269
  codex_auth: detectCodexAuthStatus(codexInstalled),
@@ -2341,6 +2287,47 @@ function readInstalledAppsPayload() {
2341
2287
  }),
2342
2288
  };
2343
2289
  }
2290
+ const CODEX_SUBSCRIPTION_STATUS_WAIT_MS = 1800;
2291
+ const CODEX_SUBSCRIPTION_CACHE_TTL_MS = 5 * 60 * 1000;
2292
+ let cachedCodexSubscription = null;
2293
+ let codexSubscriptionRefreshInFlight = null;
2294
+ function refreshCodexSubscriptionStatus() {
2295
+ if (codexSubscriptionRefreshInFlight)
2296
+ return codexSubscriptionRefreshInFlight;
2297
+ codexSubscriptionRefreshInFlight = readCodexSubscription()
2298
+ .then((payload) => {
2299
+ cachedCodexSubscription = { payload, cachedAt: Date.now() };
2300
+ return payload;
2301
+ })
2302
+ .finally(() => {
2303
+ codexSubscriptionRefreshInFlight = null;
2304
+ });
2305
+ return codexSubscriptionRefreshInFlight;
2306
+ }
2307
+ async function readCodexSubscriptionForStatus() {
2308
+ if (cachedCodexSubscription &&
2309
+ Date.now() - cachedCodexSubscription.cachedAt < CODEX_SUBSCRIPTION_CACHE_TTL_MS) {
2310
+ return { payload: cachedCodexSubscription.payload, error: null };
2311
+ }
2312
+ const refresh = refreshCodexSubscriptionStatus();
2313
+ const timeout = new Promise((resolve) => {
2314
+ setTimeout(() => resolve(null), CODEX_SUBSCRIPTION_STATUS_WAIT_MS).unref?.();
2315
+ });
2316
+ try {
2317
+ const payload = await Promise.race([refresh, timeout]);
2318
+ if (payload)
2319
+ return { payload, error: null };
2320
+ }
2321
+ catch (err) {
2322
+ if (cachedCodexSubscription)
2323
+ return { payload: cachedCodexSubscription.payload, error: null };
2324
+ return { payload: null, error: err?.message || 'Unable to read Codex subscription details.' };
2325
+ }
2326
+ if (cachedCodexSubscription)
2327
+ return { payload: cachedCodexSubscription.payload, error: null };
2328
+ refresh.catch(() => undefined);
2329
+ return { payload: null, error: null };
2330
+ }
2344
2331
  function readCachedCodingCliStatusPayload(options) {
2345
2332
  startCodingCliStatusCache({
2346
2333
  build: () => ({
@@ -2415,14 +2402,22 @@ function codexSubscriptionSummary(subscription) {
2415
2402
  : {};
2416
2403
  return {
2417
2404
  plan_type: typeof rateLimits.planType === 'string' && rateLimits.planType.trim() ? rateLimits.planType.trim() : null,
2418
- credits_balance: typeof credits.balance === 'string' && credits.balance.trim() ? credits.balance.trim() : null,
2405
+ credits_balance: typeof credits.balance === 'string' && credits.balance.trim()
2406
+ ? credits.balance.trim()
2407
+ : typeof credits.balance === 'number' && Number.isFinite(credits.balance)
2408
+ ? String(credits.balance)
2409
+ : null,
2419
2410
  };
2420
2411
  }
2421
2412
  async function readCodexAgentStatusPayload() {
2422
- const status = readCachedCodingCliStatusPayload();
2413
+ const status = {
2414
+ ...readCachedCodingCliStatusPayload(),
2415
+ daemon: detectDaemonPackageStatus(),
2416
+ };
2423
2417
  const quotaStatus = readLatestSpaceQuotaStatus();
2424
- try {
2425
- const subscription = await readCodexSubscription();
2418
+ const subscriptionResult = await readCodexSubscriptionForStatus();
2419
+ const subscription = subscriptionResult.payload;
2420
+ if (subscription) {
2426
2421
  const subscriptionWindows = mergeQuotaWindows(normalizeCodexSubscriptionQuotaWindows(subscription), normalizeQuotaWindows(subscription.rate_limits), normalizeQuotaWindows(subscription.rate_limits_by_limit_id));
2427
2422
  return {
2428
2423
  ...status,
@@ -2437,71 +2432,37 @@ async function readCodexAgentStatusPayload() {
2437
2432
  quota_windows: mergeQuotaWindows(quotaStatus.windows, subscriptionWindows),
2438
2433
  };
2439
2434
  }
2440
- catch (err) {
2441
- return {
2442
- ...status,
2443
- subscription: {
2444
- ok: false,
2445
- fetched_at: null,
2446
- payload: null,
2447
- error: err?.message || 'Unable to read Codex subscription details.',
2448
- },
2449
- subscription_summary: {
2450
- plan_type: null,
2451
- credits_balance: null,
2452
- },
2453
- quota_status: quotaStatus,
2454
- quota_windows: quotaStatus.windows,
2455
- };
2456
- }
2435
+ return {
2436
+ ...status,
2437
+ subscription: {
2438
+ ok: false,
2439
+ fetched_at: null,
2440
+ payload: null,
2441
+ error: subscriptionResult.error,
2442
+ },
2443
+ subscription_summary: {
2444
+ plan_type: null,
2445
+ credits_balance: null,
2446
+ },
2447
+ quota_status: quotaStatus,
2448
+ quota_windows: quotaStatus.windows,
2449
+ };
2457
2450
  }
2458
2451
  function readRuntimeModelStatusPayload() {
2459
- const selected_cli = process.env.SUPEN_CODING_CLI || readConfigSummary().coding_cli || 'codex';
2460
- const codexTransport = normalizeCodexTransport(process.env.SUPEN_CODEX_TRANSPORT) ||
2461
- normalizeCodexTransport(readConfigSummary().codex_transport) ||
2462
- 'app-server';
2463
2452
  const codexDefaultModel = readCodexDefaultModel();
2464
2453
  const codexModelOptions = readCodexModelOptions(codexDefaultModel);
2465
2454
  return {
2466
- selected_cli,
2467
- default_model: selected_cli === 'codex' ? codexDefaultModel : null,
2468
- model_options: selected_cli === 'codex' ? codexModelOptions : [],
2455
+ selected_cli: 'codex',
2456
+ default_model: codexDefaultModel,
2457
+ model_options: codexModelOptions,
2469
2458
  agent_runtimes: [
2470
2459
  {
2471
2460
  id: 'codex-app-server',
2472
- default: selected_cli === 'codex' && codexTransport === 'app-server',
2461
+ default: true,
2473
2462
  default_model: codexDefaultModel,
2474
2463
  model_options: codexModelOptions,
2475
2464
  transport: 'app-server',
2476
2465
  },
2477
- {
2478
- id: 'codex-exec',
2479
- default: selected_cli === 'codex' && codexTransport === 'exec',
2480
- default_model: codexDefaultModel,
2481
- model_options: codexModelOptions,
2482
- transport: 'exec',
2483
- },
2484
- {
2485
- id: 'codex-acpx',
2486
- default: selected_cli === 'codex' && codexTransport === 'acpx',
2487
- default_model: codexDefaultModel,
2488
- model_options: codexModelOptions,
2489
- transport: 'acpx',
2490
- },
2491
- {
2492
- id: 'claude-code',
2493
- default: selected_cli === 'claude',
2494
- default_model: null,
2495
- model_options: [],
2496
- transport: 'cli',
2497
- },
2498
- {
2499
- id: 'gemini-cli',
2500
- default: selected_cli === 'gemini',
2501
- default_model: null,
2502
- model_options: [],
2503
- transport: 'cli',
2504
- },
2505
2466
  ],
2506
2467
  };
2507
2468
  }
@@ -2787,7 +2748,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2787
2748
  try {
2788
2749
  const transport = normalizeCodexTransport(decodeURIComponent(codexTransportDefaultMatch[1] || '').trim());
2789
2750
  if (!transport) {
2790
- writeProtocolError(res, 400, 'validation_error', 'invalid_codex_transport', 'transport must be one of: app-server, exec, acpx');
2751
+ writeProtocolError(res, 400, 'validation_error', 'invalid_codex_transport', 'transport must be app-server');
2791
2752
  return true;
2792
2753
  }
2793
2754
  const result = setDefaultCodexTransport(transport);
@@ -2804,11 +2765,20 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2804
2765
  }
2805
2766
  if (pathname === '/api/computers/{computer_id}/agents/codex/default' && method === 'PUT') {
2806
2767
  try {
2768
+ const parsed = await readJsonBody(req);
2769
+ const model = parsed && typeof parsed === 'object' && typeof parsed.model === 'string'
2770
+ ? String(parsed.model).trim()
2771
+ : '';
2807
2772
  const result = setDefaultCodingCli('codex');
2773
+ if (model) {
2774
+ writeCodexDefaultModel(model);
2775
+ }
2808
2776
  writeJson(res, 200, {
2809
2777
  ok: true,
2810
2778
  ...result,
2811
- status: readCachedCodingCliStatusPayload({ force: true }),
2779
+ status: model
2780
+ ? readRuntimeModelStatusPayload()
2781
+ : readCachedCodingCliStatusPayload({ force: true }),
2812
2782
  });
2813
2783
  }
2814
2784
  catch (err) {
@@ -2999,13 +2969,48 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2999
2969
  numericQueryParam(coerceSingleQueryParam(url.searchParams.get('after')));
3000
2970
  const replayAfter = afterSequence ?? readThreadEventLogHead(threadId);
3001
2971
  addThreadStreamClient(threadId, res);
2972
+ const sessionPath = walkSessionFiles(path.join(localAgentHome(), 'sessions')).get(threadId);
2973
+ let jsonlOffset = sessionPath && fs.existsSync(sessionPath) ? fs.statSync(sessionPath).size : 0;
2974
+ let jsonlEventIndex = 0;
2975
+ const workspacePath = readThreadWorkspacePath(sessionPath) || process.cwd();
2976
+ const stopCodexObserver = process.env.NODE_ENV === 'test'
2977
+ ? () => { }
2978
+ : startCodexThreadObserver({
2979
+ threadId,
2980
+ cwd: workspacePath,
2981
+ write: (chunk, id) => writeThreadStreamEvent(res, chunk, id ? { id } : {}),
2982
+ });
3002
2983
  const pingTimer = setInterval(() => {
3003
2984
  writeSse(res);
3004
2985
  res.flush?.();
3005
2986
  }, SPACE_LOG_STREAM_PING_MS);
3006
2987
  pingTimer.unref?.();
2988
+ const jsonlPollTimer = sessionPath
2989
+ ? setInterval(() => {
2990
+ const read = readAppendedJsonlLines(sessionPath, jsonlOffset);
2991
+ jsonlOffset = read.offset;
2992
+ for (const line of read.lines) {
2993
+ if (!isCodexHistoryTranscriptLine(line))
2994
+ continue;
2995
+ const parsed = parseJsonLine(line);
2996
+ if (!parsed)
2997
+ continue;
2998
+ if (isCodexJsonlMessageRecord(parsed))
2999
+ continue;
3000
+ jsonlEventIndex += 1;
3001
+ writeThreadStreamEvent(res, codexJsonlEventChunk(parsed), {
3002
+ id: `jsonl-${jsonlOffset}-${jsonlEventIndex}`,
3003
+ });
3004
+ }
3005
+ res.flush?.();
3006
+ }, MIRRORED_THREAD_STREAM_JSONL_POLL_MS)
3007
+ : null;
3008
+ jsonlPollTimer?.unref?.();
3007
3009
  const cleanup = () => {
3008
3010
  clearInterval(pingTimer);
3011
+ if (jsonlPollTimer)
3012
+ clearInterval(jsonlPollTimer);
3013
+ stopCodexObserver();
3009
3014
  removeThreadStreamClient(threadId, res);
3010
3015
  };
3011
3016
  req.on('close', cleanup);
@@ -3047,7 +3052,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
3047
3052
  const requestedAgentId = parsed && typeof parsed === 'object' && typeof parsed.agent_id === 'string'
3048
3053
  ? String(parsed.agent_id).trim()
3049
3054
  : '';
3050
- const fallbackAgentId = getAllAgents()[0]?.agent_id || 'local-agent-mirror';
3055
+ const fallbackAgentId = getAllAgents()[0]?.agent_id || 'codex';
3051
3056
  const agentId = requestedAgentId || fallbackAgentId;
3052
3057
  const session = threadId ? adoptMirroredThread(threadId, agentId) : null;
3053
3058
  if (!session) {