@supen-ai/cli 1.3.4 → 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 (131) hide show
  1. package/README.md +1 -1
  2. package/daemon/dist/agent-sdk/drivers/codex-app-server-driver.d.ts.map +1 -1
  3. package/daemon/dist/agent-sdk/drivers/codex-app-server-driver.js +0 -2
  4. package/daemon/dist/agent-sdk/drivers/codex-app-server-driver.js.map +1 -1
  5. package/daemon/dist/agent-sdk/drivers/registry.d.ts +2 -8
  6. package/daemon/dist/agent-sdk/drivers/registry.d.ts.map +1 -1
  7. package/daemon/dist/agent-sdk/drivers/registry.js +7 -138
  8. package/daemon/dist/agent-sdk/drivers/registry.js.map +1 -1
  9. package/daemon/dist/agent-sdk/index.d.ts +0 -11
  10. package/daemon/dist/agent-sdk/index.d.ts.map +1 -1
  11. package/daemon/dist/agent-sdk/index.js +0 -11
  12. package/daemon/dist/agent-sdk/index.js.map +1 -1
  13. package/daemon/dist/agent-sdk/session-manager.d.ts +1 -1
  14. package/daemon/dist/agent-sdk/session-manager.d.ts.map +1 -1
  15. package/daemon/dist/agent-sdk/session-manager.js +1 -2
  16. package/daemon/dist/agent-sdk/session-manager.js.map +1 -1
  17. package/daemon/dist/channels/acp.d.ts.map +1 -1
  18. package/daemon/dist/channels/acp.js +2 -3
  19. package/daemon/dist/channels/acp.js.map +1 -1
  20. package/daemon/dist/channels/http-routes.js +6 -6
  21. package/daemon/dist/channels/http-routes.js.map +1 -1
  22. package/daemon/dist/core/config.d.ts +1 -1
  23. package/daemon/dist/core/config.d.ts.map +1 -1
  24. package/daemon/dist/core/config.js +1 -1
  25. package/daemon/dist/core/config.js.map +1 -1
  26. package/daemon/dist/core/cortex.d.ts.map +1 -1
  27. package/daemon/dist/core/cortex.js +90 -205
  28. package/daemon/dist/core/cortex.js.map +1 -1
  29. package/daemon/dist/core/env.js +1 -1
  30. package/daemon/dist/core/gateway.d.ts.map +1 -1
  31. package/daemon/dist/core/gateway.js +1 -11
  32. package/daemon/dist/core/gateway.js.map +1 -1
  33. package/daemon/dist/core/loop-guard.d.ts +1 -1
  34. package/daemon/dist/core/loop-guard.js +1 -1
  35. package/daemon/dist/core/protocol-adapter.d.ts +1 -3
  36. package/daemon/dist/core/protocol-adapter.d.ts.map +1 -1
  37. package/daemon/dist/core/protocol-adapter.js +1 -3
  38. package/daemon/dist/core/protocol-adapter.js.map +1 -1
  39. package/daemon/dist/http/routes/rpc.d.ts.map +1 -1
  40. package/daemon/dist/http/routes/rpc.js +16 -1
  41. package/daemon/dist/http/routes/rpc.js.map +1 -1
  42. package/daemon/dist/http/routes/system.d.ts +2 -0
  43. package/daemon/dist/http/routes/system.d.ts.map +1 -1
  44. package/daemon/dist/http/routes/system.js +426 -138
  45. package/daemon/dist/http/routes/system.js.map +1 -1
  46. package/daemon/dist/mcp/index.d.ts +2 -6
  47. package/daemon/dist/mcp/index.d.ts.map +1 -1
  48. package/daemon/dist/mcp/index.js +2 -6
  49. package/daemon/dist/mcp/index.js.map +1 -1
  50. package/daemon/dist/mcp/tools.d.ts +5 -7
  51. package/daemon/dist/mcp/tools.d.ts.map +1 -1
  52. package/daemon/dist/mcp/tools.js +4 -8
  53. package/daemon/dist/mcp/tools.js.map +1 -1
  54. package/daemon/dist/tools/built-ins.d.ts +9 -84
  55. package/daemon/dist/tools/built-ins.d.ts.map +1 -1
  56. package/daemon/dist/tools/built-ins.js +3 -3
  57. package/daemon/dist/tools/built-ins.js.map +1 -1
  58. package/daemon/dist/tools/local-tool.d.ts +7 -0
  59. package/daemon/dist/tools/local-tool.d.ts.map +1 -0
  60. package/daemon/dist/tools/local-tool.js +4 -0
  61. package/daemon/dist/tools/local-tool.js.map +1 -0
  62. package/daemon/dist/tools/skill-tools.d.ts +5 -8
  63. package/daemon/dist/tools/skill-tools.d.ts.map +1 -1
  64. package/daemon/dist/tools/skill-tools.js +6 -10
  65. package/daemon/dist/tools/skill-tools.js.map +1 -1
  66. package/daemon/package.json +6 -9
  67. package/dist/backend.js +5 -5
  68. package/dist/backend.js.map +1 -1
  69. package/dist/bootstrap.js +2 -6
  70. package/dist/bootstrap.js.map +1 -1
  71. package/dist/computer.js +1 -1
  72. package/dist/doctor.js +1 -1
  73. package/dist/doctor.js.map +1 -1
  74. package/dist/index.js +1 -1
  75. package/dist/repl.js +6 -66
  76. package/dist/repl.js.map +1 -1
  77. package/dist/service.js +2 -3
  78. package/dist/service.js.map +1 -1
  79. package/package.json +3 -6
  80. package/daemon/dist/agent-sdk/drivers/acpx-driver.d.ts +0 -21
  81. package/daemon/dist/agent-sdk/drivers/acpx-driver.d.ts.map +0 -1
  82. package/daemon/dist/agent-sdk/drivers/acpx-driver.js +0 -488
  83. package/daemon/dist/agent-sdk/drivers/acpx-driver.js.map +0 -1
  84. package/daemon/dist/agent-sdk/drivers/claude-acpx-driver.d.ts +0 -5
  85. package/daemon/dist/agent-sdk/drivers/claude-acpx-driver.d.ts.map +0 -1
  86. package/daemon/dist/agent-sdk/drivers/claude-acpx-driver.js +0 -7
  87. package/daemon/dist/agent-sdk/drivers/claude-acpx-driver.js.map +0 -1
  88. package/daemon/dist/agent-sdk/drivers/claude-cli-direct-driver.d.ts +0 -20
  89. package/daemon/dist/agent-sdk/drivers/claude-cli-direct-driver.d.ts.map +0 -1
  90. package/daemon/dist/agent-sdk/drivers/claude-cli-direct-driver.js +0 -264
  91. package/daemon/dist/agent-sdk/drivers/claude-cli-direct-driver.js.map +0 -1
  92. package/daemon/dist/agent-sdk/drivers/claude-code-driver.d.ts +0 -29
  93. package/daemon/dist/agent-sdk/drivers/claude-code-driver.d.ts.map +0 -1
  94. package/daemon/dist/agent-sdk/drivers/claude-code-driver.js +0 -24
  95. package/daemon/dist/agent-sdk/drivers/claude-code-driver.js.map +0 -1
  96. package/daemon/dist/agent-sdk/drivers/claude-code-events.d.ts +0 -25
  97. package/daemon/dist/agent-sdk/drivers/claude-code-events.d.ts.map +0 -1
  98. package/daemon/dist/agent-sdk/drivers/claude-code-events.js +0 -58
  99. package/daemon/dist/agent-sdk/drivers/claude-code-events.js.map +0 -1
  100. package/daemon/dist/agent-sdk/drivers/claude-code-items.d.ts +0 -41
  101. package/daemon/dist/agent-sdk/drivers/claude-code-items.d.ts.map +0 -1
  102. package/daemon/dist/agent-sdk/drivers/claude-code-items.js +0 -77
  103. package/daemon/dist/agent-sdk/drivers/claude-code-items.js.map +0 -1
  104. package/daemon/dist/agent-sdk/drivers/codex-acpx-driver.d.ts +0 -5
  105. package/daemon/dist/agent-sdk/drivers/codex-acpx-driver.d.ts.map +0 -1
  106. package/daemon/dist/agent-sdk/drivers/codex-acpx-driver.js +0 -7
  107. package/daemon/dist/agent-sdk/drivers/codex-acpx-driver.js.map +0 -1
  108. package/daemon/dist/agent-sdk/drivers/codex-exec-driver.d.ts +0 -28
  109. package/daemon/dist/agent-sdk/drivers/codex-exec-driver.d.ts.map +0 -1
  110. package/daemon/dist/agent-sdk/drivers/codex-exec-driver.js +0 -219
  111. package/daemon/dist/agent-sdk/drivers/codex-exec-driver.js.map +0 -1
  112. package/daemon/dist/agent-sdk/drivers/codex-exec-events.d.ts +0 -9
  113. package/daemon/dist/agent-sdk/drivers/codex-exec-events.d.ts.map +0 -1
  114. package/daemon/dist/agent-sdk/drivers/codex-exec-events.js +0 -82
  115. package/daemon/dist/agent-sdk/drivers/codex-exec-events.js.map +0 -1
  116. package/daemon/dist/agent-sdk/drivers/codex-exec-items.d.ts +0 -9
  117. package/daemon/dist/agent-sdk/drivers/codex-exec-items.d.ts.map +0 -1
  118. package/daemon/dist/agent-sdk/drivers/codex-exec-items.js +0 -86
  119. package/daemon/dist/agent-sdk/drivers/codex-exec-items.js.map +0 -1
  120. package/daemon/dist/agent-sdk/drivers/gemini-cli-driver.d.ts +0 -17
  121. package/daemon/dist/agent-sdk/drivers/gemini-cli-driver.d.ts.map +0 -1
  122. package/daemon/dist/agent-sdk/drivers/gemini-cli-driver.js +0 -255
  123. package/daemon/dist/agent-sdk/drivers/gemini-cli-driver.js.map +0 -1
  124. package/daemon/dist/core/sdk-wrapper.d.ts +0 -36
  125. package/daemon/dist/core/sdk-wrapper.d.ts.map +0 -1
  126. package/daemon/dist/core/sdk-wrapper.js +0 -533
  127. package/daemon/dist/core/sdk-wrapper.js.map +0 -1
  128. package/daemon/dist/skills/claude_code.d.ts +0 -25
  129. package/daemon/dist/skills/claude_code.d.ts.map +0 -1
  130. package/daemon/dist/skills/claude_code.js +0 -49
  131. 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,13 +30,18 @@ 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
36
  const MIRRORED_THREAD_INLINE_DATA_URL_LIMIT = 120_000;
35
37
  const MIRRORED_THREAD_STREAM_REPLAY_LIMIT = 500;
36
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;
37
42
  const MIRRORED_THREAD_RUNNING_LEASE_MS = 30 * 60 * 1000;
38
43
  const MIRRORED_THREAD_HISTORY_CHUNK_BYTES = 1024 * 1024;
44
+ const MIRRORED_THREAD_SESSION_FILE_INDEX_TTL_MS = 2_000;
39
45
  const require = createRequire(import.meta.url);
40
46
  const HOST_DAEMON_INSTALL_DIR = path.join(SUPEN_HOME, 'daemon');
41
47
  const HOST_DAEMON_CLI_PACKAGE_ROOT = path.join(HOST_DAEMON_INSTALL_DIR, 'node_modules', '@supen-ai', 'cli');
@@ -433,6 +439,9 @@ export function readRecentJsonlLines(filePath, maxBytes = MIRRORED_THREAD_HISTOR
433
439
  }
434
440
  }
435
441
  function isCodexHistoryMessageLine(line) {
442
+ if (!line.includes('"type":"message"') && !line.includes('"type":"user_message"')) {
443
+ return false;
444
+ }
436
445
  const parsed = parseJsonLine(line);
437
446
  const payload = parsed?.payload && typeof parsed.payload === 'object'
438
447
  ? parsed.payload
@@ -440,8 +449,78 @@ function isCodexHistoryMessageLine(line) {
440
449
  return ((parsed?.type === 'event_msg' && payload.type === 'user_message') ||
441
450
  (parsed?.type === 'response_item' && payload.type === 'message'));
442
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
+ }
443
522
  function readRecentCodexHistoryLines(filePath, targetMessages, maxBytes = MIRRORED_THREAD_HISTORY_MAX_BYTES) {
444
- const safeTargetMessages = Math.max(1, Math.min(Math.floor(targetMessages), 501));
523
+ const safeTargetMessages = Math.max(1, Math.min(Math.floor(targetMessages), 5_000));
445
524
  const safeMaxBytes = Math.max(1, Math.min(Math.floor(maxBytes), MIRRORED_THREAD_HISTORY_MAX_BYTES));
446
525
  const size = fs.statSync(filePath).size;
447
526
  const fd = fs.openSync(filePath, 'r');
@@ -464,12 +543,9 @@ function readRecentCodexHistoryLines(filePath, targetMessages, maxBytes = MIRROR
464
543
  lines.unshift(leadingPartial);
465
544
  leadingPartial = '';
466
545
  }
467
- const completeLines = lines.filter(Boolean);
468
- for (const line of completeLines) {
469
- if (isCodexHistoryMessageLine(line))
470
- messageCount += 1;
471
- }
472
- collected = [...completeLines, ...collected];
546
+ const historyLines = lines.filter((line) => line && isCodexHistoryTranscriptLine(line));
547
+ messageCount += historyLines.filter(isCodexVisibleUserHistoryLine).length;
548
+ collected = [...historyLines, ...collected];
473
549
  }
474
550
  return collected.filter(Boolean);
475
551
  }
@@ -574,7 +650,8 @@ function readThreadStateEntries(limit) {
574
650
  return [];
575
651
  }
576
652
  }
577
- function walkSessionFiles(dir, out = new Map()) {
653
+ let mirroredSessionFileIndexCache = null;
654
+ function walkSessionFilesUncached(dir, out = new Map()) {
578
655
  if (!fs.existsSync(dir))
579
656
  return out;
580
657
  let entries;
@@ -587,7 +664,7 @@ function walkSessionFiles(dir, out = new Map()) {
587
664
  for (const entry of entries) {
588
665
  const fullPath = path.join(dir, entry.name);
589
666
  if (entry.isDirectory()) {
590
- walkSessionFiles(fullPath, out);
667
+ walkSessionFilesUncached(fullPath, out);
591
668
  continue;
592
669
  }
593
670
  if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
@@ -598,6 +675,21 @@ function walkSessionFiles(dir, out = new Map()) {
598
675
  }
599
676
  return out;
600
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
+ }
601
693
  function readFileHead(filePath, maxBytes = 64 * 1024) {
602
694
  const fd = fs.openSync(filePath, 'r');
603
695
  try {
@@ -622,6 +714,34 @@ function readFileTail(filePath, maxBytes = 256 * 1024) {
622
714
  fs.closeSync(fd);
623
715
  }
624
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
+ }
625
745
  function readThreadWorkspacePath(filePath) {
626
746
  if (!filePath)
627
747
  return null;
@@ -645,6 +765,91 @@ function readThreadWorkspacePath(filePath) {
645
765
  }
646
766
  return null;
647
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
+ }
648
853
  function normalizeThreadStatusToken(value) {
649
854
  return typeof value === 'string'
650
855
  ? value.trim().toLowerCase().replace(/[-_\s]/g, '')
@@ -697,7 +902,7 @@ function readThreadStatus(filePath) {
697
902
  let sawUserTurn = false;
698
903
  let completedAfterLastUser = false;
699
904
  let lastRunningActivityMs = null;
700
- const tail = readFileTail(filePath, MIRRORED_THREAD_STREAM_REPLAY_MAX_BYTES);
905
+ const tail = readFileTail(filePath, MIRRORED_THREAD_STATUS_TAIL_MAX_BYTES);
701
906
  for (const line of tail.split(/\r?\n/)) {
702
907
  if (!line.trim())
703
908
  continue;
@@ -707,7 +912,9 @@ function readThreadStatus(filePath) {
707
912
  ? parsed.payload
708
913
  : {};
709
914
  if (parsed?.type === 'event_msg') {
710
- 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') {
711
918
  sawUserTurn = true;
712
919
  completedAfterLastUser = false;
713
920
  lastRunningActivityMs = timestampMs;
@@ -730,6 +937,26 @@ function readThreadStatus(filePath) {
730
937
  lastRunningActivityMs = timestampMs;
731
938
  continue;
732
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
+ }
733
960
  const raw = payload.raw && typeof payload.raw === 'object'
734
961
  ? payload.raw
735
962
  : null;
@@ -774,6 +1001,17 @@ function readThreadStatus(filePath) {
774
1001
  return 'idle';
775
1002
  }
776
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
+ }
777
1015
  function projectNameFromPath(projectPath) {
778
1016
  if (!projectPath)
779
1017
  return 'No workspace';
@@ -892,7 +1130,7 @@ function readMirroredTaskProjects(limit = MIRRORED_THREAD_LIMIT) {
892
1130
  id: entry.id,
893
1131
  title,
894
1132
  updated_at: entry.updated_at,
895
- status: readThreadStatus(sessionPath),
1133
+ status: readMirroredThreadStatus(entry.id, sessionPath),
896
1134
  session_path: sessionPath,
897
1135
  });
898
1136
  projects.set(projectId, project);
@@ -923,7 +1161,7 @@ function readMirroredTaskProjects(limit = MIRRORED_THREAD_LIMIT) {
923
1161
  id: entry.id,
924
1162
  title: entry.thread_name,
925
1163
  updated_at: entry.updated_at,
926
- status: readThreadStatus(sessionPath),
1164
+ status: readMirroredThreadStatus(entry.id, sessionPath),
927
1165
  session_path: sessionPath,
928
1166
  });
929
1167
  projects.set(projectId, project);
@@ -1050,6 +1288,9 @@ function isInternalMirroredUserMessage(text) {
1050
1288
  const trimmed = text.trimStart();
1051
1289
  return (trimmed.startsWith('<environment_context>') ||
1052
1290
  trimmed.startsWith('<developer_context>') ||
1291
+ trimmed.startsWith('<codex_internal_context') ||
1292
+ trimmed.startsWith('<goal_context>') ||
1293
+ trimmed.startsWith('<turn_aborted>') ||
1053
1294
  trimmed.startsWith('[plugin:vite:'));
1054
1295
  }
1055
1296
  function isMirroredGoalContextMessage(text) {
@@ -1065,6 +1306,53 @@ function mirroredThreadWorkspacePath(sessionPath, stateEntry) {
1065
1306
  }
1066
1307
  return null;
1067
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
+ }
1068
1356
  export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY_LIMIT, options = {}) {
1069
1357
  const safeLimit = Math.max(1, Math.min(limit, 500));
1070
1358
  const sessionPath = walkSessionFiles(path.join(localAgentHome(), 'sessions')).get(threadId);
@@ -1075,10 +1363,12 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1075
1363
  const workspacePath = mirroredThreadWorkspacePath(sessionPath, stateEntry);
1076
1364
  const indexEntry = readThreadIndexEntry(threadId);
1077
1365
  const eventLogHead = readThreadEventLogHead(eventLogThreadId);
1078
- 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);
1079
1368
  const messages = [];
1080
1369
  const events = [];
1081
1370
  let messageIndex = 0;
1371
+ let eventIndex = 0;
1082
1372
  let latestTimestamp = indexEntry?.updated_at || '';
1083
1373
  let recentGoalContext = null;
1084
1374
  const mirroredUserMessageKey = (text) => sanitizeMirroredUserText(text)
@@ -1086,11 +1376,37 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1086
1376
  .trim();
1087
1377
  const pushUserMessage = (text, timestamp, messageId, attachments = []) => {
1088
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
+ };
1089
1399
  if (isMirroredGoalContextMessage(text)) {
1090
1400
  recentGoalContext = { text, timestamp };
1401
+ pushInternalBoundary();
1402
+ return;
1091
1403
  }
1092
1404
  text = truncateMirroredHistoryString(text, MIRRORED_THREAD_TEXT_LIMIT, 'Message');
1093
- if ((!text && attachments.length === 0) || isInternalMirroredUserMessage(text))
1405
+ if (isInternalMirroredUserMessage(text)) {
1406
+ pushInternalBoundary();
1407
+ return;
1408
+ }
1409
+ if (!text && attachments.length === 0)
1094
1410
  return;
1095
1411
  const key = mirroredUserMessageKey(text);
1096
1412
  const timestampMs = Date.parse(timestamp);
@@ -1170,6 +1486,15 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1170
1486
  const messageId = typeof payload.id === 'string' ? payload.id : `${threadId}-web-user-${event.sequence}`;
1171
1487
  pushUserMessage(text, timestamp, messageId, attachments);
1172
1488
  };
1489
+ const pushCodexJsonlEvent = (parsed, timestamp) => {
1490
+ eventIndex += 1;
1491
+ events.push({
1492
+ id: `${threadId}-jsonl-event-${eventIndex}`,
1493
+ timestamp,
1494
+ task_id: threadId,
1495
+ chunk: codexJsonlEventChunk(parsed),
1496
+ });
1497
+ };
1173
1498
  for (const line of lines) {
1174
1499
  const parsed = parseJsonLine(line);
1175
1500
  if (typeof parsed?.timestamp === 'string')
@@ -1185,12 +1510,19 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1185
1510
  const text = typeof payload.message === 'string' ? payload.message.trim() : '';
1186
1511
  pushUserMessage(text, timestamp, undefined, imageAttachmentsFromUnknown(payload.images));
1187
1512
  }
1513
+ else if (payload.type === 'patch_apply_end') {
1514
+ pushCodexJsonlEvent(parsed, timestamp);
1515
+ }
1188
1516
  continue;
1189
1517
  }
1190
1518
  if (parsed?.type !== 'response_item')
1191
1519
  continue;
1192
- if (payload.type !== 'message')
1520
+ if (payload.type !== 'message') {
1521
+ if (isCodexHistoryTranscriptEventLine(line)) {
1522
+ pushCodexJsonlEvent(parsed, timestamp);
1523
+ }
1193
1524
  continue;
1525
+ }
1194
1526
  messageIndex += 1;
1195
1527
  const messageId = `${threadId}-message-${messageIndex}`;
1196
1528
  const role = payload.role;
@@ -1210,16 +1542,22 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1210
1542
  content: truncateMirroredHistoryString(text, MIRRORED_THREAD_TEXT_LIMIT, 'Message'),
1211
1543
  timestamp,
1212
1544
  task_id: threadId,
1545
+ ...(typeof payload.phase === 'string' && payload.phase.trim()
1546
+ ? { phase: payload.phase.trim() }
1547
+ : {}),
1213
1548
  });
1214
1549
  }
1215
1550
  const eventLogEvents = listRecentThreadEventsAfter(eventLogThreadId, 0, {
1216
1551
  limit: MIRRORED_THREAD_STREAM_REPLAY_LIMIT,
1217
1552
  maxBytes: MIRRORED_THREAD_STREAM_REPLAY_MAX_BYTES,
1218
1553
  });
1219
- for (const event of eventLogEvents) {
1554
+ const usableEventLogEvents = rawThreadEventsAreStaleForHistory(eventLogEvents, latestTimestamp)
1555
+ ? []
1556
+ : eventLogEvents;
1557
+ for (const event of usableEventLogEvents) {
1220
1558
  pushWebUserMessage(event);
1221
1559
  }
1222
- for (const event of eventLogEvents) {
1560
+ for (const event of usableEventLogEvents) {
1223
1561
  const chunk = threadStreamChunkForEvent(event);
1224
1562
  if (!chunk)
1225
1563
  continue;
@@ -1231,14 +1569,15 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1231
1569
  chunk,
1232
1570
  });
1233
1571
  }
1234
- const slicedMessages = messages.slice(-safeLimit);
1572
+ const collapsedMessages = collapseCompletedAssistantProgressMessages(messages);
1573
+ const slicedMessages = collapsedMessages.slice(-safeLimit);
1235
1574
  const slicedEvents = events.slice(-safeLimit * 8);
1236
- const status = readThreadStatus(sessionPath);
1575
+ const status = readMirroredThreadStatus(eventLogThreadId, sessionPath);
1237
1576
  const latestGoalContext = recentGoalContext;
1238
1577
  return {
1239
1578
  session: {
1240
1579
  task_id: threadId,
1241
- agent_id: 'local-agent-mirror',
1580
+ agent_id: 'codex',
1242
1581
  channel: 'local-agent',
1243
1582
  source_ref: `local-agent:${threadId}`,
1244
1583
  title: indexEntry?.thread_name || stateEntry?.title || slicedMessages.find((message) => message.role === 'user')?.content || 'New Task',
@@ -1254,14 +1593,14 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1254
1593
  items: [],
1255
1594
  runtimeState: {
1256
1595
  event_log_head: eventLogHead,
1257
- projection_version: 1,
1596
+ projection_version: MIRRORED_THREAD_PROJECTION_VERSION,
1258
1597
  event_log: { status: 'current' },
1259
1598
  runtime: {
1260
1599
  status: status === 'running' ? 'running' : 'idle',
1261
1600
  updated_at: latestTimestamp || undefined,
1262
1601
  },
1263
1602
  },
1264
- has_more: messages.length > safeLimit,
1603
+ has_more: collapsedMessages.length > safeLimit || lines.length >= historyLineTarget,
1265
1604
  ...(latestGoalContext ? { goal_context: latestGoalContext } : {}),
1266
1605
  };
1267
1606
  }
@@ -1309,14 +1648,14 @@ function coerceSingleQueryParam(value) {
1309
1648
  return value || null;
1310
1649
  }
1311
1650
  function codexThreadsListRoute(pathname) {
1312
- return /^\/api\/computers\/\{computer_id\}\/agents\/[^/]+\/codex\/threads$/.test(pathname);
1651
+ return /^\/api\/computers\/\{computer_id\}\/agents\/codex\/threads$/.test(pathname);
1313
1652
  }
1314
1653
  function codexThreadActionRoute(pathname, action) {
1315
1654
  const suffix = action === 'messages' ? 'messages' : action;
1316
- 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}$`));
1317
1656
  }
1318
1657
  function codexProjectsOpenRoute(pathname) {
1319
- return /^\/api\/computers\/\{computer_id\}\/agents\/[^/]+\/codex\/projects\/open$/.test(pathname);
1658
+ return /^\/api\/computers\/\{computer_id\}\/agents\/codex\/projects\/open$/.test(pathname);
1320
1659
  }
1321
1660
  function collectSpaceLogEntries(filters = {}) {
1322
1661
  const spaceId = currentSpaceId();
@@ -1439,11 +1778,9 @@ function buildMcpSettingsResponse() {
1439
1778
  }
1440
1779
  const CODING_CLI_INSTALL_COMMANDS = {
1441
1780
  codex: { command: 'npm', args: ['install', '-g', '@openai/codex'] },
1442
- gemini: { command: 'npm', args: ['install', '-g', '@google/gemini-cli'] },
1443
1781
  };
1444
1782
  const MANAGED_CODING_CLI_INSTALL_COMMANDS = {
1445
1783
  codex: CODING_CLI_INSTALL_COMMANDS.codex,
1446
- gemini: CODING_CLI_INSTALL_COMMANDS.gemini,
1447
1784
  };
1448
1785
  function resolveManagedCodingCliInstallCommand(cli, current) {
1449
1786
  if (cli === 'codex' && current.install_source === 'homebrew') {
@@ -1454,7 +1791,7 @@ function resolveManagedCodingCliInstallCommand(cli, current) {
1454
1791
  }
1455
1792
  return MANAGED_CODING_CLI_INSTALL_COMMANDS[cli];
1456
1793
  }
1457
- const CODING_CLI_NAMES = ['codex', 'gemini', 'claude'];
1794
+ const CODING_CLI_NAMES = ['codex'];
1458
1795
  const DETECTABLE_SPACE_APPS = [
1459
1796
  { id: 'libreoffice', command: 'libreoffice', managed: false },
1460
1797
  { id: 'dotnet', command: 'dotnet', managed: false },
@@ -1477,18 +1814,18 @@ function nowIso() {
1477
1814
  }
1478
1815
  function normalizeCliName(raw) {
1479
1816
  const value = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
1480
- if (value === 'codex' || value === 'gemini' || value === 'claude')
1817
+ if (value === 'codex')
1481
1818
  return value;
1482
1819
  return null;
1483
1820
  }
1484
1821
  function normalizeCodexTransport(raw) {
1485
1822
  const value = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
1486
- if (value === 'app-server' || value === 'exec' || value === 'acpx')
1823
+ if (value === 'app-server')
1487
1824
  return value;
1488
1825
  return null;
1489
1826
  }
1490
1827
  function isManagedCliName(name) {
1491
- return name === 'codex' || name === 'gemini';
1828
+ return name === 'codex';
1492
1829
  }
1493
1830
  function trimLogNoise(value) {
1494
1831
  const line = value.trim();
@@ -1672,16 +2009,6 @@ function inferCliInstallSource(name, executablePath, resolvedPath) {
1672
2009
  return { install_source: 'node-package', update_supported: false };
1673
2010
  }
1674
2011
  }
1675
- if (name === 'gemini') {
1676
- const npmRoot = detectGlobalNpmRoot();
1677
- const geminiPackagePath = resolvedPath || executablePath || '';
1678
- if (npmRoot && geminiPackagePath.startsWith(`${npmRoot}${path.sep}@google${path.sep}gemini-cli${path.sep}`)) {
1679
- return { install_source: 'npm-global', update_supported: true };
1680
- }
1681
- if (normalized.includes('/node_modules/@google/gemini-cli/')) {
1682
- return { install_source: 'node-package', update_supported: false };
1683
- }
1684
- }
1685
2012
  return { install_source: null, update_supported: false };
1686
2013
  }
1687
2014
  function detectCliInstalled(name) {
@@ -1904,13 +2231,6 @@ function readCodingCliStatusPayload() {
1904
2231
  });
1905
2232
  const codexInstalled = clis.find((cli) => cli.name === 'codex')?.installed ?? false;
1906
2233
  const codexVersion = clis.find((cli) => cli.name === 'codex')?.version ?? null;
1907
- const gemini = clis.find((cli) => cli.name === 'gemini');
1908
- const claude = clis.find((cli) => cli.name === 'claude');
1909
- const acpxDetected = detectCliInstalled('acpx');
1910
- const selected_cli = process.env.SUPEN_CODING_CLI || readConfigSummary().coding_cli || 'codex';
1911
- const codexTransport = normalizeCodexTransport(process.env.SUPEN_CODEX_TRANSPORT) ||
1912
- normalizeCodexTransport(readConfigSummary().codex_transport) ||
1913
- 'app-server';
1914
2234
  const codexDefaultModel = readCodexDefaultModel();
1915
2235
  const codexModelOptions = readCodexModelOptions(codexDefaultModel);
1916
2236
  const installed_apps = DETECTABLE_SPACE_APPS.map((app) => {
@@ -1927,9 +2247,9 @@ function readCodingCliStatusPayload() {
1927
2247
  };
1928
2248
  });
1929
2249
  return {
1930
- selected_cli,
1931
- default_model: selected_cli === 'codex' ? codexDefaultModel : null,
1932
- model_options: selected_cli === 'codex' ? codexModelOptions : [],
2250
+ selected_cli: 'codex',
2251
+ default_model: codexDefaultModel,
2252
+ model_options: codexModelOptions,
1933
2253
  clis,
1934
2254
  agent_runtimes: [
1935
2255
  {
@@ -1941,53 +2261,9 @@ function readCodingCliStatusPayload() {
1941
2261
  model_options: codexModelOptions,
1942
2262
  transport: 'app-server',
1943
2263
  recommended: true,
1944
- default: selected_cli === 'codex' && codexTransport === 'app-server',
2264
+ default: true,
1945
2265
  description: 'Daemon-managed codex app-server process with protocol-native streaming.',
1946
2266
  },
1947
- {
1948
- id: 'codex-exec',
1949
- label: 'Codex CLI Exec',
1950
- installed: codexInstalled,
1951
- version: codexVersion,
1952
- default_model: codexDefaultModel,
1953
- model_options: codexModelOptions,
1954
- transport: 'exec',
1955
- recommended: false,
1956
- default: selected_cli === 'codex' && codexTransport === 'exec',
1957
- description: 'Direct codex exec fallback, typically one process per turn.',
1958
- },
1959
- {
1960
- id: 'codex-acpx',
1961
- label: 'Codex via ACPX',
1962
- installed: codexInstalled && acpxDetected.installed,
1963
- version: acpxDetected.version || codexVersion,
1964
- default_model: codexDefaultModel,
1965
- model_options: codexModelOptions,
1966
- transport: 'acpx',
1967
- recommended: false,
1968
- default: selected_cli === 'codex' && codexTransport === 'acpx',
1969
- description: 'Compatibility bridge through ACPX for hosts that still need it.',
1970
- },
1971
- {
1972
- id: 'claude-code',
1973
- label: 'Claude Code',
1974
- installed: Boolean(claude?.installed),
1975
- version: claude?.version ?? null,
1976
- transport: 'cli',
1977
- recommended: false,
1978
- default: selected_cli === 'claude',
1979
- description: 'Claude Code CLI runtime.',
1980
- },
1981
- {
1982
- id: 'gemini-cli',
1983
- label: 'Gemini CLI',
1984
- installed: Boolean(gemini?.installed),
1985
- version: gemini?.version ?? null,
1986
- transport: 'cli',
1987
- recommended: false,
1988
- default: selected_cli === 'gemini',
1989
- description: 'Gemini CLI runtime.',
1990
- },
1991
2267
  ],
1992
2268
  installed_apps,
1993
2269
  codex_auth: detectCodexAuthStatus(codexInstalled),
@@ -2173,52 +2449,20 @@ async function readCodexAgentStatusPayload() {
2173
2449
  };
2174
2450
  }
2175
2451
  function readRuntimeModelStatusPayload() {
2176
- const selected_cli = process.env.SUPEN_CODING_CLI || readConfigSummary().coding_cli || 'codex';
2177
- const codexTransport = normalizeCodexTransport(process.env.SUPEN_CODEX_TRANSPORT) ||
2178
- normalizeCodexTransport(readConfigSummary().codex_transport) ||
2179
- 'app-server';
2180
2452
  const codexDefaultModel = readCodexDefaultModel();
2181
2453
  const codexModelOptions = readCodexModelOptions(codexDefaultModel);
2182
2454
  return {
2183
- selected_cli,
2184
- default_model: selected_cli === 'codex' ? codexDefaultModel : null,
2185
- model_options: selected_cli === 'codex' ? codexModelOptions : [],
2455
+ selected_cli: 'codex',
2456
+ default_model: codexDefaultModel,
2457
+ model_options: codexModelOptions,
2186
2458
  agent_runtimes: [
2187
2459
  {
2188
2460
  id: 'codex-app-server',
2189
- default: selected_cli === 'codex' && codexTransport === 'app-server',
2461
+ default: true,
2190
2462
  default_model: codexDefaultModel,
2191
2463
  model_options: codexModelOptions,
2192
2464
  transport: 'app-server',
2193
2465
  },
2194
- {
2195
- id: 'codex-exec',
2196
- default: selected_cli === 'codex' && codexTransport === 'exec',
2197
- default_model: codexDefaultModel,
2198
- model_options: codexModelOptions,
2199
- transport: 'exec',
2200
- },
2201
- {
2202
- id: 'codex-acpx',
2203
- default: selected_cli === 'codex' && codexTransport === 'acpx',
2204
- default_model: codexDefaultModel,
2205
- model_options: codexModelOptions,
2206
- transport: 'acpx',
2207
- },
2208
- {
2209
- id: 'claude-code',
2210
- default: selected_cli === 'claude',
2211
- default_model: null,
2212
- model_options: [],
2213
- transport: 'cli',
2214
- },
2215
- {
2216
- id: 'gemini-cli',
2217
- default: selected_cli === 'gemini',
2218
- default_model: null,
2219
- model_options: [],
2220
- transport: 'cli',
2221
- },
2222
2466
  ],
2223
2467
  };
2224
2468
  }
@@ -2504,7 +2748,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2504
2748
  try {
2505
2749
  const transport = normalizeCodexTransport(decodeURIComponent(codexTransportDefaultMatch[1] || '').trim());
2506
2750
  if (!transport) {
2507
- 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');
2508
2752
  return true;
2509
2753
  }
2510
2754
  const result = setDefaultCodexTransport(transport);
@@ -2521,11 +2765,20 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2521
2765
  }
2522
2766
  if (pathname === '/api/computers/{computer_id}/agents/codex/default' && method === 'PUT') {
2523
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
+ : '';
2524
2772
  const result = setDefaultCodingCli('codex');
2773
+ if (model) {
2774
+ writeCodexDefaultModel(model);
2775
+ }
2525
2776
  writeJson(res, 200, {
2526
2777
  ok: true,
2527
2778
  ...result,
2528
- status: readCachedCodingCliStatusPayload({ force: true }),
2779
+ status: model
2780
+ ? readRuntimeModelStatusPayload()
2781
+ : readCachedCodingCliStatusPayload({ force: true }),
2529
2782
  });
2530
2783
  }
2531
2784
  catch (err) {
@@ -2716,13 +2969,48 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2716
2969
  numericQueryParam(coerceSingleQueryParam(url.searchParams.get('after')));
2717
2970
  const replayAfter = afterSequence ?? readThreadEventLogHead(threadId);
2718
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
+ });
2719
2983
  const pingTimer = setInterval(() => {
2720
2984
  writeSse(res);
2721
2985
  res.flush?.();
2722
2986
  }, SPACE_LOG_STREAM_PING_MS);
2723
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?.();
2724
3009
  const cleanup = () => {
2725
3010
  clearInterval(pingTimer);
3011
+ if (jsonlPollTimer)
3012
+ clearInterval(jsonlPollTimer);
3013
+ stopCodexObserver();
2726
3014
  removeThreadStreamClient(threadId, res);
2727
3015
  };
2728
3016
  req.on('close', cleanup);
@@ -2764,7 +3052,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2764
3052
  const requestedAgentId = parsed && typeof parsed === 'object' && typeof parsed.agent_id === 'string'
2765
3053
  ? String(parsed.agent_id).trim()
2766
3054
  : '';
2767
- const fallbackAgentId = getAllAgents()[0]?.agent_id || 'local-agent-mirror';
3055
+ const fallbackAgentId = getAllAgents()[0]?.agent_id || 'codex';
2768
3056
  const agentId = requestedAgentId || fallbackAgentId;
2769
3057
  const session = threadId ? adoptMirroredThread(threadId, agentId) : null;
2770
3058
  if (!session) {