@supen-ai/cli 1.3.4 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) 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/core/thread-context.d.ts +76 -0
  40. package/daemon/dist/core/thread-context.d.ts.map +1 -0
  41. package/daemon/dist/core/thread-context.js +308 -0
  42. package/daemon/dist/core/thread-context.js.map +1 -0
  43. package/daemon/dist/http/routes/rpc.d.ts.map +1 -1
  44. package/daemon/dist/http/routes/rpc.js +18 -1
  45. package/daemon/dist/http/routes/rpc.js.map +1 -1
  46. package/daemon/dist/http/routes/sessions.js +1 -1
  47. package/daemon/dist/http/routes/system.d.ts +3 -0
  48. package/daemon/dist/http/routes/system.d.ts.map +1 -1
  49. package/daemon/dist/http/routes/system.js +437 -141
  50. package/daemon/dist/http/routes/system.js.map +1 -1
  51. package/daemon/dist/index.d.ts.map +1 -1
  52. package/daemon/dist/index.js +2 -1
  53. package/daemon/dist/index.js.map +1 -1
  54. package/daemon/dist/mcp/index.d.ts +2 -6
  55. package/daemon/dist/mcp/index.d.ts.map +1 -1
  56. package/daemon/dist/mcp/index.js +2 -6
  57. package/daemon/dist/mcp/index.js.map +1 -1
  58. package/daemon/dist/mcp/tools.d.ts +5 -7
  59. package/daemon/dist/mcp/tools.d.ts.map +1 -1
  60. package/daemon/dist/mcp/tools.js +4 -8
  61. package/daemon/dist/mcp/tools.js.map +1 -1
  62. package/daemon/dist/tools/built-ins.d.ts +9 -84
  63. package/daemon/dist/tools/built-ins.d.ts.map +1 -1
  64. package/daemon/dist/tools/built-ins.js +3 -3
  65. package/daemon/dist/tools/built-ins.js.map +1 -1
  66. package/daemon/dist/tools/local-tool.d.ts +7 -0
  67. package/daemon/dist/tools/local-tool.d.ts.map +1 -0
  68. package/daemon/dist/tools/local-tool.js +4 -0
  69. package/daemon/dist/tools/local-tool.js.map +1 -0
  70. package/daemon/dist/tools/skill-tools.d.ts +5 -8
  71. package/daemon/dist/tools/skill-tools.d.ts.map +1 -1
  72. package/daemon/dist/tools/skill-tools.js +6 -10
  73. package/daemon/dist/tools/skill-tools.js.map +1 -1
  74. package/daemon/package.json +6 -9
  75. package/dist/backend.js +5 -5
  76. package/dist/backend.js.map +1 -1
  77. package/dist/bootstrap.js +2 -6
  78. package/dist/bootstrap.js.map +1 -1
  79. package/dist/computer.js +1 -1
  80. package/dist/doctor.js +1 -1
  81. package/dist/doctor.js.map +1 -1
  82. package/dist/index.js +1 -1
  83. package/dist/repl.js +6 -66
  84. package/dist/repl.js.map +1 -1
  85. package/dist/service.js +2 -3
  86. package/dist/service.js.map +1 -1
  87. package/package.json +3 -6
  88. package/daemon/dist/agent-sdk/drivers/acpx-driver.d.ts +0 -21
  89. package/daemon/dist/agent-sdk/drivers/acpx-driver.d.ts.map +0 -1
  90. package/daemon/dist/agent-sdk/drivers/acpx-driver.js +0 -488
  91. package/daemon/dist/agent-sdk/drivers/acpx-driver.js.map +0 -1
  92. package/daemon/dist/agent-sdk/drivers/claude-acpx-driver.d.ts +0 -5
  93. package/daemon/dist/agent-sdk/drivers/claude-acpx-driver.d.ts.map +0 -1
  94. package/daemon/dist/agent-sdk/drivers/claude-acpx-driver.js +0 -7
  95. package/daemon/dist/agent-sdk/drivers/claude-acpx-driver.js.map +0 -1
  96. package/daemon/dist/agent-sdk/drivers/claude-cli-direct-driver.d.ts +0 -20
  97. package/daemon/dist/agent-sdk/drivers/claude-cli-direct-driver.d.ts.map +0 -1
  98. package/daemon/dist/agent-sdk/drivers/claude-cli-direct-driver.js +0 -264
  99. package/daemon/dist/agent-sdk/drivers/claude-cli-direct-driver.js.map +0 -1
  100. package/daemon/dist/agent-sdk/drivers/claude-code-driver.d.ts +0 -29
  101. package/daemon/dist/agent-sdk/drivers/claude-code-driver.d.ts.map +0 -1
  102. package/daemon/dist/agent-sdk/drivers/claude-code-driver.js +0 -24
  103. package/daemon/dist/agent-sdk/drivers/claude-code-driver.js.map +0 -1
  104. package/daemon/dist/agent-sdk/drivers/claude-code-events.d.ts +0 -25
  105. package/daemon/dist/agent-sdk/drivers/claude-code-events.d.ts.map +0 -1
  106. package/daemon/dist/agent-sdk/drivers/claude-code-events.js +0 -58
  107. package/daemon/dist/agent-sdk/drivers/claude-code-events.js.map +0 -1
  108. package/daemon/dist/agent-sdk/drivers/claude-code-items.d.ts +0 -41
  109. package/daemon/dist/agent-sdk/drivers/claude-code-items.d.ts.map +0 -1
  110. package/daemon/dist/agent-sdk/drivers/claude-code-items.js +0 -77
  111. package/daemon/dist/agent-sdk/drivers/claude-code-items.js.map +0 -1
  112. package/daemon/dist/agent-sdk/drivers/codex-acpx-driver.d.ts +0 -5
  113. package/daemon/dist/agent-sdk/drivers/codex-acpx-driver.d.ts.map +0 -1
  114. package/daemon/dist/agent-sdk/drivers/codex-acpx-driver.js +0 -7
  115. package/daemon/dist/agent-sdk/drivers/codex-acpx-driver.js.map +0 -1
  116. package/daemon/dist/agent-sdk/drivers/codex-exec-driver.d.ts +0 -28
  117. package/daemon/dist/agent-sdk/drivers/codex-exec-driver.d.ts.map +0 -1
  118. package/daemon/dist/agent-sdk/drivers/codex-exec-driver.js +0 -219
  119. package/daemon/dist/agent-sdk/drivers/codex-exec-driver.js.map +0 -1
  120. package/daemon/dist/agent-sdk/drivers/codex-exec-events.d.ts +0 -9
  121. package/daemon/dist/agent-sdk/drivers/codex-exec-events.d.ts.map +0 -1
  122. package/daemon/dist/agent-sdk/drivers/codex-exec-events.js +0 -82
  123. package/daemon/dist/agent-sdk/drivers/codex-exec-events.js.map +0 -1
  124. package/daemon/dist/agent-sdk/drivers/codex-exec-items.d.ts +0 -9
  125. package/daemon/dist/agent-sdk/drivers/codex-exec-items.d.ts.map +0 -1
  126. package/daemon/dist/agent-sdk/drivers/codex-exec-items.js +0 -86
  127. package/daemon/dist/agent-sdk/drivers/codex-exec-items.js.map +0 -1
  128. package/daemon/dist/agent-sdk/drivers/gemini-cli-driver.d.ts +0 -17
  129. package/daemon/dist/agent-sdk/drivers/gemini-cli-driver.d.ts.map +0 -1
  130. package/daemon/dist/agent-sdk/drivers/gemini-cli-driver.js +0 -255
  131. package/daemon/dist/agent-sdk/drivers/gemini-cli-driver.js.map +0 -1
  132. package/daemon/dist/core/sdk-wrapper.d.ts +0 -36
  133. package/daemon/dist/core/sdk-wrapper.d.ts.map +0 -1
  134. package/daemon/dist/core/sdk-wrapper.js +0 -533
  135. package/daemon/dist/core/sdk-wrapper.js.map +0 -1
  136. package/daemon/dist/skills/claude_code.d.ts +0 -25
  137. package/daemon/dist/skills/claude_code.d.ts.map +0 -1
  138. package/daemon/dist/skills/claude_code.js +0 -49
  139. 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,20 +22,27 @@ 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';
26
+ import { attachThreadContextToChunk, readThreadContext, } from '../../core/thread-context.js';
25
27
  import { addThreadStreamClient, removeThreadStreamClient, writeThreadStreamEvent } from '../thread-stream.js';
26
28
  const DEFAULT_TOKEN_TTL_MINUTES = 20;
27
29
  const SPACE_LOG_EVENT_LIMIT = 200;
28
30
  const SPACE_LOG_STREAM_POLL_MS = 1000;
29
31
  const SPACE_LOG_STREAM_PING_MS = 15000;
30
32
  const MIRRORED_THREAD_LIMIT = 80;
31
- const MIRRORED_THREAD_HISTORY_LIMIT = 100;
32
- const MIRRORED_THREAD_HISTORY_MAX_BYTES = 64 * 1024 * 1024;
33
+ const MIRRORED_THREAD_HISTORY_LIMIT = 200;
34
+ const MIRRORED_THREAD_HISTORY_MAX_BYTES = 256 * 1024 * 1024;
35
+ const MIRRORED_THREAD_PROJECTION_VERSION = 2;
33
36
  const MIRRORED_THREAD_TEXT_LIMIT = 48_000;
34
37
  const MIRRORED_THREAD_INLINE_DATA_URL_LIMIT = 120_000;
35
38
  const MIRRORED_THREAD_STREAM_REPLAY_LIMIT = 500;
36
39
  const MIRRORED_THREAD_STREAM_REPLAY_MAX_BYTES = 8 * 1024 * 1024;
40
+ const MIRRORED_THREAD_STREAM_JSONL_POLL_MS = 500;
41
+ const MIRRORED_THREAD_STREAM_JSONL_READ_MAX_BYTES = 2 * 1024 * 1024;
42
+ const MIRRORED_THREAD_STATUS_TAIL_MAX_BYTES = 256 * 1024;
37
43
  const MIRRORED_THREAD_RUNNING_LEASE_MS = 30 * 60 * 1000;
38
44
  const MIRRORED_THREAD_HISTORY_CHUNK_BYTES = 1024 * 1024;
45
+ const MIRRORED_THREAD_SESSION_FILE_INDEX_TTL_MS = 2_000;
39
46
  const require = createRequire(import.meta.url);
40
47
  const HOST_DAEMON_INSTALL_DIR = path.join(SUPEN_HOME, 'daemon');
41
48
  const HOST_DAEMON_CLI_PACKAGE_ROOT = path.join(HOST_DAEMON_INSTALL_DIR, 'node_modules', '@supen-ai', 'cli');
@@ -433,6 +440,9 @@ export function readRecentJsonlLines(filePath, maxBytes = MIRRORED_THREAD_HISTOR
433
440
  }
434
441
  }
435
442
  function isCodexHistoryMessageLine(line) {
443
+ if (!line.includes('"type":"message"') && !line.includes('"type":"user_message"')) {
444
+ return false;
445
+ }
436
446
  const parsed = parseJsonLine(line);
437
447
  const payload = parsed?.payload && typeof parsed.payload === 'object'
438
448
  ? parsed.payload
@@ -440,8 +450,78 @@ function isCodexHistoryMessageLine(line) {
440
450
  return ((parsed?.type === 'event_msg' && payload.type === 'user_message') ||
441
451
  (parsed?.type === 'response_item' && payload.type === 'message'));
442
452
  }
453
+ function isCodexVisibleUserHistoryLine(line) {
454
+ const parsed = parseJsonLine(line);
455
+ const payload = parsed?.payload && typeof parsed.payload === 'object'
456
+ ? parsed.payload
457
+ : {};
458
+ if (parsed?.type === 'event_msg' && payload.type === 'user_message') {
459
+ const text = typeof payload.message === 'string' ? payload.message.trim() : '';
460
+ return Boolean(text && !isInternalMirroredUserMessage(text));
461
+ }
462
+ if (parsed?.type !== 'response_item' || payload.type !== 'message' || payload.role !== 'user') {
463
+ return false;
464
+ }
465
+ const text = mirroredMessageContentParts(payload.content).text;
466
+ return Boolean(text && !isInternalMirroredUserMessage(sanitizeMirroredUserText(text)));
467
+ }
468
+ function isCodexHistoryTranscriptEventLine(line) {
469
+ if (!line.includes('"type":"function_call"') &&
470
+ !line.includes('"type":"custom_tool_call"') &&
471
+ !line.includes('"type":"patch_apply_end"') &&
472
+ !line.includes('"type":"tool_search_call"') &&
473
+ !line.includes('"type":"web_search_call"') &&
474
+ !line.includes('"type":"mcp_tool_call"')) {
475
+ return false;
476
+ }
477
+ const parsed = parseJsonLine(line);
478
+ const payload = parsed?.payload && typeof parsed.payload === 'object'
479
+ ? parsed.payload
480
+ : {};
481
+ if (parsed?.type === 'event_msg') {
482
+ return payload.type === 'patch_apply_end';
483
+ }
484
+ if (parsed?.type !== 'response_item')
485
+ return false;
486
+ return (payload.type === 'function_call' ||
487
+ payload.type === 'custom_tool_call' ||
488
+ payload.type === 'tool_search_call' ||
489
+ payload.type === 'web_search_call' ||
490
+ payload.type === 'mcp_tool_call');
491
+ }
492
+ function isCodexHistoryTranscriptLine(line) {
493
+ return isCodexHistoryMessageLine(line) || isCodexHistoryTranscriptEventLine(line);
494
+ }
495
+ function isCodexJsonlMessageRecord(parsed) {
496
+ const payload = parsed?.payload && typeof parsed.payload === 'object'
497
+ ? parsed.payload
498
+ : {};
499
+ return parsed?.type === 'response_item' && payload.type === 'message';
500
+ }
501
+ function codexJsonlEventChunk(parsed) {
502
+ const payload = parsed.payload && typeof parsed.payload === 'object'
503
+ ? parsed.payload
504
+ : {};
505
+ const payloadType = typeof payload.type === 'string' ? payload.type : '';
506
+ return {
507
+ type: 'data-codex-event',
508
+ data: {
509
+ eventType: `codex-jsonl/${parsed.type || 'record'}${payloadType ? `/${payloadType}` : ''}`,
510
+ raw: parsed,
511
+ },
512
+ };
513
+ }
514
+ function codexAppServerEventChunk(method, params = {}) {
515
+ return {
516
+ type: 'data-codex-event',
517
+ data: {
518
+ eventType: method,
519
+ raw: { method, params },
520
+ },
521
+ };
522
+ }
443
523
  function readRecentCodexHistoryLines(filePath, targetMessages, maxBytes = MIRRORED_THREAD_HISTORY_MAX_BYTES) {
444
- const safeTargetMessages = Math.max(1, Math.min(Math.floor(targetMessages), 501));
524
+ const safeTargetMessages = Math.max(1, Math.min(Math.floor(targetMessages), 5_000));
445
525
  const safeMaxBytes = Math.max(1, Math.min(Math.floor(maxBytes), MIRRORED_THREAD_HISTORY_MAX_BYTES));
446
526
  const size = fs.statSync(filePath).size;
447
527
  const fd = fs.openSync(filePath, 'r');
@@ -464,12 +544,9 @@ function readRecentCodexHistoryLines(filePath, targetMessages, maxBytes = MIRROR
464
544
  lines.unshift(leadingPartial);
465
545
  leadingPartial = '';
466
546
  }
467
- const completeLines = lines.filter(Boolean);
468
- for (const line of completeLines) {
469
- if (isCodexHistoryMessageLine(line))
470
- messageCount += 1;
471
- }
472
- collected = [...completeLines, ...collected];
547
+ const historyLines = lines.filter((line) => line && isCodexHistoryTranscriptLine(line));
548
+ messageCount += historyLines.filter(isCodexVisibleUserHistoryLine).length;
549
+ collected = [...historyLines, ...collected];
473
550
  }
474
551
  return collected.filter(Boolean);
475
552
  }
@@ -574,7 +651,8 @@ function readThreadStateEntries(limit) {
574
651
  return [];
575
652
  }
576
653
  }
577
- function walkSessionFiles(dir, out = new Map()) {
654
+ let mirroredSessionFileIndexCache = null;
655
+ function walkSessionFilesUncached(dir, out = new Map()) {
578
656
  if (!fs.existsSync(dir))
579
657
  return out;
580
658
  let entries;
@@ -587,7 +665,7 @@ function walkSessionFiles(dir, out = new Map()) {
587
665
  for (const entry of entries) {
588
666
  const fullPath = path.join(dir, entry.name);
589
667
  if (entry.isDirectory()) {
590
- walkSessionFiles(fullPath, out);
668
+ walkSessionFilesUncached(fullPath, out);
591
669
  continue;
592
670
  }
593
671
  if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
@@ -598,6 +676,21 @@ function walkSessionFiles(dir, out = new Map()) {
598
676
  }
599
677
  return out;
600
678
  }
679
+ function walkSessionFiles(dir) {
680
+ const now = Date.now();
681
+ if (mirroredSessionFileIndexCache &&
682
+ mirroredSessionFileIndexCache.root === dir &&
683
+ now - mirroredSessionFileIndexCache.indexedAt <= MIRRORED_THREAD_SESSION_FILE_INDEX_TTL_MS) {
684
+ return new Map(mirroredSessionFileIndexCache.files);
685
+ }
686
+ const files = walkSessionFilesUncached(dir);
687
+ mirroredSessionFileIndexCache = {
688
+ root: dir,
689
+ indexedAt: now,
690
+ files: new Map(files),
691
+ };
692
+ return files;
693
+ }
601
694
  function readFileHead(filePath, maxBytes = 64 * 1024) {
602
695
  const fd = fs.openSync(filePath, 'r');
603
696
  try {
@@ -622,6 +715,34 @@ function readFileTail(filePath, maxBytes = 256 * 1024) {
622
715
  fs.closeSync(fd);
623
716
  }
624
717
  }
718
+ function readAppendedJsonlLines(filePath, offset) {
719
+ if (!fs.existsSync(filePath))
720
+ return { offset, lines: [], partial: '' };
721
+ const stat = fs.statSync(filePath);
722
+ if (stat.size <= offset)
723
+ return { offset: stat.size, lines: [], partial: '' };
724
+ const bytesToRead = Math.min(stat.size - offset, MIRRORED_THREAD_STREAM_JSONL_READ_MAX_BYTES);
725
+ const readStart = stat.size - offset > MIRRORED_THREAD_STREAM_JSONL_READ_MAX_BYTES
726
+ ? stat.size - MIRRORED_THREAD_STREAM_JSONL_READ_MAX_BYTES
727
+ : offset;
728
+ const fd = fs.openSync(filePath, 'r');
729
+ try {
730
+ const buffer = Buffer.alloc(bytesToRead);
731
+ const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, readStart);
732
+ const text = buffer.subarray(0, bytesRead).toString('utf-8');
733
+ const parts = text.split(/\r?\n/);
734
+ const partial = text.endsWith('\n') || text.endsWith('\r') ? '' : (parts.pop() || '');
735
+ const completeLines = parts.filter(Boolean);
736
+ return {
737
+ offset: readStart + bytesRead - Buffer.byteLength(partial),
738
+ lines: readStart === offset ? completeLines : completeLines.slice(1),
739
+ partial,
740
+ };
741
+ }
742
+ finally {
743
+ fs.closeSync(fd);
744
+ }
745
+ }
625
746
  function readThreadWorkspacePath(filePath) {
626
747
  if (!filePath)
627
748
  return null;
@@ -645,6 +766,91 @@ function readThreadWorkspacePath(filePath) {
645
766
  }
646
767
  return null;
647
768
  }
769
+ function startCodexThreadObserver(input) {
770
+ const child = spawn('codex', ['app-server', '--listen', 'stdio://'], {
771
+ cwd: input.cwd,
772
+ stdio: ['pipe', 'pipe', 'pipe'],
773
+ env: withRuntimeSpaceEnv(process.env),
774
+ });
775
+ let buffer = '';
776
+ let nextId = 1;
777
+ let observerEventIndex = 0;
778
+ let initialized = false;
779
+ const send = (message) => {
780
+ if (!child.stdin.writable)
781
+ return;
782
+ child.stdin.write(`${JSON.stringify(message)}\n`);
783
+ };
784
+ const request = (method, params) => {
785
+ send({ id: nextId++, method, ...(params ? { params } : {}) });
786
+ };
787
+ child.stdout.on('data', (chunk) => {
788
+ buffer += String(chunk);
789
+ while (true) {
790
+ const newlineIndex = buffer.indexOf('\n');
791
+ if (newlineIndex < 0)
792
+ break;
793
+ const line = buffer.slice(0, newlineIndex).trim();
794
+ buffer = buffer.slice(newlineIndex + 1);
795
+ if (!line.startsWith('{'))
796
+ continue;
797
+ let parsed;
798
+ try {
799
+ parsed = JSON.parse(line);
800
+ }
801
+ catch {
802
+ continue;
803
+ }
804
+ if (parsed.id !== undefined && typeof parsed.method === 'string') {
805
+ send({ id: parsed.id, result: {} });
806
+ continue;
807
+ }
808
+ if (parsed.id !== undefined) {
809
+ if (!initialized && parsed.id === 1) {
810
+ initialized = true;
811
+ send({ method: 'initialized' });
812
+ request('thread/resume', {
813
+ threadId: input.threadId,
814
+ cwd: input.cwd,
815
+ experimentalRawEvents: true,
816
+ });
817
+ }
818
+ continue;
819
+ }
820
+ const method = typeof parsed.method === 'string' ? parsed.method : '';
821
+ if (!method)
822
+ continue;
823
+ const params = parsed.params && typeof parsed.params === 'object'
824
+ ? parsed.params
825
+ : {};
826
+ const eventThreadId = typeof params.threadId === 'string' ? params.threadId : '';
827
+ if (eventThreadId !== input.threadId)
828
+ continue;
829
+ observerEventIndex += 1;
830
+ input.write(codexAppServerEventChunk(method, params), `observer-${Date.now()}-${observerEventIndex}`);
831
+ }
832
+ });
833
+ child.stderr.on('data', () => { });
834
+ child.once('error', (error) => {
835
+ logger.debug({ err: error, threadId: input.threadId }, 'Codex thread observer failed');
836
+ });
837
+ request('initialize', {
838
+ clientInfo: {
839
+ name: 'supen-daemon-thread-observer',
840
+ title: 'Supen',
841
+ version: '0.1.0',
842
+ },
843
+ capabilities: { experimentalApi: true },
844
+ });
845
+ return () => {
846
+ try {
847
+ child.kill('SIGTERM');
848
+ }
849
+ catch {
850
+ // Observer cleanup is best-effort; the HTTP stream is already closing.
851
+ }
852
+ };
853
+ }
648
854
  function normalizeThreadStatusToken(value) {
649
855
  return typeof value === 'string'
650
856
  ? value.trim().toLowerCase().replace(/[-_\s]/g, '')
@@ -697,7 +903,7 @@ function readThreadStatus(filePath) {
697
903
  let sawUserTurn = false;
698
904
  let completedAfterLastUser = false;
699
905
  let lastRunningActivityMs = null;
700
- const tail = readFileTail(filePath, MIRRORED_THREAD_STREAM_REPLAY_MAX_BYTES);
906
+ const tail = readFileTail(filePath, MIRRORED_THREAD_STATUS_TAIL_MAX_BYTES);
701
907
  for (const line of tail.split(/\r?\n/)) {
702
908
  if (!line.trim())
703
909
  continue;
@@ -707,7 +913,9 @@ function readThreadStatus(filePath) {
707
913
  ? parsed.payload
708
914
  : {};
709
915
  if (parsed?.type === 'event_msg') {
710
- if (payload.type === 'task_started' || payload.type === 'user_message') {
916
+ if (payload.type === 'task_started' ||
917
+ payload.type === 'user_message' ||
918
+ payload.type === 'thread_goal_updated') {
711
919
  sawUserTurn = true;
712
920
  completedAfterLastUser = false;
713
921
  lastRunningActivityMs = timestampMs;
@@ -730,6 +938,26 @@ function readThreadStatus(filePath) {
730
938
  lastRunningActivityMs = timestampMs;
731
939
  continue;
732
940
  }
941
+ if (parsed?.type === 'response_item' &&
942
+ payload.type === 'message' &&
943
+ payload.role === 'assistant') {
944
+ if (payload.phase === 'final_answer') {
945
+ completedAfterLastUser = true;
946
+ continue;
947
+ }
948
+ if (sawUserTurn) {
949
+ completedAfterLastUser = false;
950
+ lastRunningActivityMs = timestampMs;
951
+ }
952
+ continue;
953
+ }
954
+ if (parsed?.type === 'response_item' &&
955
+ sawUserTurn &&
956
+ !completedAfterLastUser &&
957
+ payload.type !== 'reasoning') {
958
+ lastRunningActivityMs = timestampMs;
959
+ continue;
960
+ }
733
961
  const raw = payload.raw && typeof payload.raw === 'object'
734
962
  ? payload.raw
735
963
  : null;
@@ -774,6 +1002,17 @@ function readThreadStatus(filePath) {
774
1002
  return 'idle';
775
1003
  }
776
1004
  }
1005
+ function readMirroredThreadStatus(threadId, filePath) {
1006
+ try {
1007
+ const runtimeState = projectThreadRuntimeState(threadId);
1008
+ if (isRunningThreadStatusToken(runtimeState.runtime.status))
1009
+ return 'running';
1010
+ }
1011
+ catch {
1012
+ // Fall back to the Codex session file status below.
1013
+ }
1014
+ return readThreadStatus(filePath);
1015
+ }
777
1016
  function projectNameFromPath(projectPath) {
778
1017
  if (!projectPath)
779
1018
  return 'No workspace';
@@ -892,7 +1131,7 @@ function readMirroredTaskProjects(limit = MIRRORED_THREAD_LIMIT) {
892
1131
  id: entry.id,
893
1132
  title,
894
1133
  updated_at: entry.updated_at,
895
- status: readThreadStatus(sessionPath),
1134
+ status: readMirroredThreadStatus(entry.id, sessionPath),
896
1135
  session_path: sessionPath,
897
1136
  });
898
1137
  projects.set(projectId, project);
@@ -923,7 +1162,7 @@ function readMirroredTaskProjects(limit = MIRRORED_THREAD_LIMIT) {
923
1162
  id: entry.id,
924
1163
  title: entry.thread_name,
925
1164
  updated_at: entry.updated_at,
926
- status: readThreadStatus(sessionPath),
1165
+ status: readMirroredThreadStatus(entry.id, sessionPath),
927
1166
  session_path: sessionPath,
928
1167
  });
929
1168
  projects.set(projectId, project);
@@ -1050,6 +1289,9 @@ function isInternalMirroredUserMessage(text) {
1050
1289
  const trimmed = text.trimStart();
1051
1290
  return (trimmed.startsWith('<environment_context>') ||
1052
1291
  trimmed.startsWith('<developer_context>') ||
1292
+ trimmed.startsWith('<codex_internal_context') ||
1293
+ trimmed.startsWith('<goal_context>') ||
1294
+ trimmed.startsWith('<turn_aborted>') ||
1053
1295
  trimmed.startsWith('[plugin:vite:'));
1054
1296
  }
1055
1297
  function isMirroredGoalContextMessage(text) {
@@ -1065,8 +1307,55 @@ function mirroredThreadWorkspacePath(sessionPath, stateEntry) {
1065
1307
  }
1066
1308
  return null;
1067
1309
  }
1310
+ function latestRawThreadEventTimestampMs(events) {
1311
+ let latest = null;
1312
+ for (const event of events) {
1313
+ const timestampMs = Date.parse(event.received_at);
1314
+ if (!Number.isFinite(timestampMs))
1315
+ continue;
1316
+ latest = latest === null ? timestampMs : Math.max(latest, timestampMs);
1317
+ }
1318
+ return latest;
1319
+ }
1320
+ function rawThreadEventsAreStaleForHistory(events, latestHistoryTimestamp) {
1321
+ const latestEventTimestampMs = latestRawThreadEventTimestampMs(events);
1322
+ if (latestEventTimestampMs === null)
1323
+ return false;
1324
+ const latestHistoryTimestampMs = Date.parse(latestHistoryTimestamp);
1325
+ if (!Number.isFinite(latestHistoryTimestampMs))
1326
+ return false;
1327
+ return latestEventTimestampMs + 30_000 < latestHistoryTimestampMs;
1328
+ }
1329
+ function collapseCompletedAssistantProgressMessages(input) {
1330
+ const output = [];
1331
+ let assistantRun = [];
1332
+ const flushAssistantRun = () => {
1333
+ if (assistantRun.length === 0)
1334
+ return;
1335
+ const finalAnswers = assistantRun.filter((message) => message.phase === 'final_answer');
1336
+ if (finalAnswers.length > 0) {
1337
+ output.push(...finalAnswers);
1338
+ }
1339
+ else {
1340
+ output.push(...assistantRun);
1341
+ }
1342
+ assistantRun = [];
1343
+ };
1344
+ for (const message of input) {
1345
+ if (message.role === 'assistant') {
1346
+ assistantRun.push(message);
1347
+ continue;
1348
+ }
1349
+ flushAssistantRun();
1350
+ output.push(message);
1351
+ }
1352
+ flushAssistantRun();
1353
+ return output
1354
+ .filter((message) => message.internal !== true)
1355
+ .map(({ phase: _phase, internal: _internal, ...message }) => message);
1356
+ }
1068
1357
  export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY_LIMIT, options = {}) {
1069
- const safeLimit = Math.max(1, Math.min(limit, 500));
1358
+ const safeLimit = Math.max(1, Math.min(limit, 1000));
1070
1359
  const sessionPath = walkSessionFiles(path.join(localAgentHome(), 'sessions')).get(threadId);
1071
1360
  if (!sessionPath)
1072
1361
  return null;
@@ -1075,10 +1364,12 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1075
1364
  const workspacePath = mirroredThreadWorkspacePath(sessionPath, stateEntry);
1076
1365
  const indexEntry = readThreadIndexEntry(threadId);
1077
1366
  const eventLogHead = readThreadEventLogHead(eventLogThreadId);
1078
- const lines = readRecentCodexHistoryLines(sessionPath, safeLimit + 1);
1367
+ const historyLineTarget = Math.min(250, Math.max(3, Math.ceil(safeLimit / 4)));
1368
+ const lines = readRecentCodexHistoryLines(sessionPath, historyLineTarget);
1079
1369
  const messages = [];
1080
1370
  const events = [];
1081
1371
  let messageIndex = 0;
1372
+ let eventIndex = 0;
1082
1373
  let latestTimestamp = indexEntry?.updated_at || '';
1083
1374
  let recentGoalContext = null;
1084
1375
  const mirroredUserMessageKey = (text) => sanitizeMirroredUserText(text)
@@ -1086,11 +1377,37 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1086
1377
  .trim();
1087
1378
  const pushUserMessage = (text, timestamp, messageId, attachments = []) => {
1088
1379
  text = sanitizeMirroredUserText(text);
1380
+ const pushInternalBoundary = () => {
1381
+ let lastVisibleMessage;
1382
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
1383
+ if (messages[index].internal === true)
1384
+ continue;
1385
+ lastVisibleMessage = messages[index];
1386
+ break;
1387
+ }
1388
+ if (lastVisibleMessage?.role !== 'assistant' || lastVisibleMessage.phase !== 'final_answer') {
1389
+ return;
1390
+ }
1391
+ messages.push({
1392
+ id: messageId || `${threadId}-internal-${messageIndex + 1}`,
1393
+ role: 'system',
1394
+ content: '',
1395
+ timestamp,
1396
+ task_id: threadId,
1397
+ internal: true,
1398
+ });
1399
+ };
1089
1400
  if (isMirroredGoalContextMessage(text)) {
1090
1401
  recentGoalContext = { text, timestamp };
1402
+ pushInternalBoundary();
1403
+ return;
1091
1404
  }
1092
1405
  text = truncateMirroredHistoryString(text, MIRRORED_THREAD_TEXT_LIMIT, 'Message');
1093
- if ((!text && attachments.length === 0) || isInternalMirroredUserMessage(text))
1406
+ if (isInternalMirroredUserMessage(text)) {
1407
+ pushInternalBoundary();
1408
+ return;
1409
+ }
1410
+ if (!text && attachments.length === 0)
1094
1411
  return;
1095
1412
  const key = mirroredUserMessageKey(text);
1096
1413
  const timestampMs = Date.parse(timestamp);
@@ -1170,6 +1487,15 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1170
1487
  const messageId = typeof payload.id === 'string' ? payload.id : `${threadId}-web-user-${event.sequence}`;
1171
1488
  pushUserMessage(text, timestamp, messageId, attachments);
1172
1489
  };
1490
+ const pushCodexJsonlEvent = (parsed, timestamp) => {
1491
+ eventIndex += 1;
1492
+ events.push({
1493
+ id: `${threadId}-jsonl-event-${eventIndex}`,
1494
+ timestamp,
1495
+ task_id: threadId,
1496
+ chunk: codexJsonlEventChunk(parsed),
1497
+ });
1498
+ };
1173
1499
  for (const line of lines) {
1174
1500
  const parsed = parseJsonLine(line);
1175
1501
  if (typeof parsed?.timestamp === 'string')
@@ -1185,12 +1511,19 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1185
1511
  const text = typeof payload.message === 'string' ? payload.message.trim() : '';
1186
1512
  pushUserMessage(text, timestamp, undefined, imageAttachmentsFromUnknown(payload.images));
1187
1513
  }
1514
+ else if (payload.type === 'patch_apply_end') {
1515
+ pushCodexJsonlEvent(parsed, timestamp);
1516
+ }
1188
1517
  continue;
1189
1518
  }
1190
1519
  if (parsed?.type !== 'response_item')
1191
1520
  continue;
1192
- if (payload.type !== 'message')
1521
+ if (payload.type !== 'message') {
1522
+ if (isCodexHistoryTranscriptEventLine(line)) {
1523
+ pushCodexJsonlEvent(parsed, timestamp);
1524
+ }
1193
1525
  continue;
1526
+ }
1194
1527
  messageIndex += 1;
1195
1528
  const messageId = `${threadId}-message-${messageIndex}`;
1196
1529
  const role = payload.role;
@@ -1210,16 +1543,22 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1210
1543
  content: truncateMirroredHistoryString(text, MIRRORED_THREAD_TEXT_LIMIT, 'Message'),
1211
1544
  timestamp,
1212
1545
  task_id: threadId,
1546
+ ...(typeof payload.phase === 'string' && payload.phase.trim()
1547
+ ? { phase: payload.phase.trim() }
1548
+ : {}),
1213
1549
  });
1214
1550
  }
1215
1551
  const eventLogEvents = listRecentThreadEventsAfter(eventLogThreadId, 0, {
1216
1552
  limit: MIRRORED_THREAD_STREAM_REPLAY_LIMIT,
1217
1553
  maxBytes: MIRRORED_THREAD_STREAM_REPLAY_MAX_BYTES,
1218
1554
  });
1219
- for (const event of eventLogEvents) {
1555
+ const usableEventLogEvents = rawThreadEventsAreStaleForHistory(eventLogEvents, latestTimestamp)
1556
+ ? []
1557
+ : eventLogEvents;
1558
+ for (const event of usableEventLogEvents) {
1220
1559
  pushWebUserMessage(event);
1221
1560
  }
1222
- for (const event of eventLogEvents) {
1561
+ for (const event of usableEventLogEvents) {
1223
1562
  const chunk = threadStreamChunkForEvent(event);
1224
1563
  if (!chunk)
1225
1564
  continue;
@@ -1231,14 +1570,20 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1231
1570
  chunk,
1232
1571
  });
1233
1572
  }
1234
- const slicedMessages = messages.slice(-safeLimit);
1573
+ const collapsedMessages = collapseCompletedAssistantProgressMessages(messages);
1574
+ const slicedMessages = collapsedMessages.slice(-safeLimit);
1235
1575
  const slicedEvents = events.slice(-safeLimit * 8);
1236
- const status = readThreadStatus(sessionPath);
1576
+ const status = readMirroredThreadStatus(eventLogThreadId, sessionPath);
1237
1577
  const latestGoalContext = recentGoalContext;
1578
+ const context = readThreadContext({
1579
+ threadId,
1580
+ eventLogThreadId,
1581
+ workspace: workspacePath,
1582
+ });
1238
1583
  return {
1239
1584
  session: {
1240
1585
  task_id: threadId,
1241
- agent_id: 'local-agent-mirror',
1586
+ agent_id: 'codex',
1242
1587
  channel: 'local-agent',
1243
1588
  source_ref: `local-agent:${threadId}`,
1244
1589
  title: indexEntry?.thread_name || stateEntry?.title || slicedMessages.find((message) => message.role === 'user')?.content || 'New Task',
@@ -1254,14 +1599,15 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1254
1599
  items: [],
1255
1600
  runtimeState: {
1256
1601
  event_log_head: eventLogHead,
1257
- projection_version: 1,
1602
+ projection_version: MIRRORED_THREAD_PROJECTION_VERSION,
1258
1603
  event_log: { status: 'current' },
1259
1604
  runtime: {
1260
1605
  status: status === 'running' ? 'running' : 'idle',
1261
1606
  updated_at: latestTimestamp || undefined,
1262
1607
  },
1263
1608
  },
1264
- has_more: messages.length > safeLimit,
1609
+ context,
1610
+ has_more: collapsedMessages.length > safeLimit || lines.length >= historyLineTarget,
1265
1611
  ...(latestGoalContext ? { goal_context: latestGoalContext } : {}),
1266
1612
  };
1267
1613
  }
@@ -1309,14 +1655,14 @@ function coerceSingleQueryParam(value) {
1309
1655
  return value || null;
1310
1656
  }
1311
1657
  function codexThreadsListRoute(pathname) {
1312
- return /^\/api\/computers\/\{computer_id\}\/agents\/[^/]+\/codex\/threads$/.test(pathname);
1658
+ return /^\/api\/computers\/\{computer_id\}\/agents\/codex\/threads$/.test(pathname);
1313
1659
  }
1314
1660
  function codexThreadActionRoute(pathname, action) {
1315
1661
  const suffix = action === 'messages' ? 'messages' : action;
1316
- return pathname.match(new RegExp(`^/api/computers/\\{computer_id\\}/agents/[^/]+/codex/threads/([^/]+)/${suffix}$`));
1662
+ return pathname.match(new RegExp(`^/api/computers/\\{computer_id\\}/agents/codex/threads/([^/]+)/${suffix}$`));
1317
1663
  }
1318
1664
  function codexProjectsOpenRoute(pathname) {
1319
- return /^\/api\/computers\/\{computer_id\}\/agents\/[^/]+\/codex\/projects\/open$/.test(pathname);
1665
+ return /^\/api\/computers\/\{computer_id\}\/agents\/codex\/projects\/open$/.test(pathname);
1320
1666
  }
1321
1667
  function collectSpaceLogEntries(filters = {}) {
1322
1668
  const spaceId = currentSpaceId();
@@ -1439,11 +1785,9 @@ function buildMcpSettingsResponse() {
1439
1785
  }
1440
1786
  const CODING_CLI_INSTALL_COMMANDS = {
1441
1787
  codex: { command: 'npm', args: ['install', '-g', '@openai/codex'] },
1442
- gemini: { command: 'npm', args: ['install', '-g', '@google/gemini-cli'] },
1443
1788
  };
1444
1789
  const MANAGED_CODING_CLI_INSTALL_COMMANDS = {
1445
1790
  codex: CODING_CLI_INSTALL_COMMANDS.codex,
1446
- gemini: CODING_CLI_INSTALL_COMMANDS.gemini,
1447
1791
  };
1448
1792
  function resolveManagedCodingCliInstallCommand(cli, current) {
1449
1793
  if (cli === 'codex' && current.install_source === 'homebrew') {
@@ -1454,7 +1798,7 @@ function resolveManagedCodingCliInstallCommand(cli, current) {
1454
1798
  }
1455
1799
  return MANAGED_CODING_CLI_INSTALL_COMMANDS[cli];
1456
1800
  }
1457
- const CODING_CLI_NAMES = ['codex', 'gemini', 'claude'];
1801
+ const CODING_CLI_NAMES = ['codex'];
1458
1802
  const DETECTABLE_SPACE_APPS = [
1459
1803
  { id: 'libreoffice', command: 'libreoffice', managed: false },
1460
1804
  { id: 'dotnet', command: 'dotnet', managed: false },
@@ -1477,18 +1821,18 @@ function nowIso() {
1477
1821
  }
1478
1822
  function normalizeCliName(raw) {
1479
1823
  const value = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
1480
- if (value === 'codex' || value === 'gemini' || value === 'claude')
1824
+ if (value === 'codex')
1481
1825
  return value;
1482
1826
  return null;
1483
1827
  }
1484
1828
  function normalizeCodexTransport(raw) {
1485
1829
  const value = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
1486
- if (value === 'app-server' || value === 'exec' || value === 'acpx')
1830
+ if (value === 'app-server')
1487
1831
  return value;
1488
1832
  return null;
1489
1833
  }
1490
1834
  function isManagedCliName(name) {
1491
- return name === 'codex' || name === 'gemini';
1835
+ return name === 'codex';
1492
1836
  }
1493
1837
  function trimLogNoise(value) {
1494
1838
  const line = value.trim();
@@ -1672,16 +2016,6 @@ function inferCliInstallSource(name, executablePath, resolvedPath) {
1672
2016
  return { install_source: 'node-package', update_supported: false };
1673
2017
  }
1674
2018
  }
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
2019
  return { install_source: null, update_supported: false };
1686
2020
  }
1687
2021
  function detectCliInstalled(name) {
@@ -1904,13 +2238,6 @@ function readCodingCliStatusPayload() {
1904
2238
  });
1905
2239
  const codexInstalled = clis.find((cli) => cli.name === 'codex')?.installed ?? false;
1906
2240
  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
2241
  const codexDefaultModel = readCodexDefaultModel();
1915
2242
  const codexModelOptions = readCodexModelOptions(codexDefaultModel);
1916
2243
  const installed_apps = DETECTABLE_SPACE_APPS.map((app) => {
@@ -1927,9 +2254,9 @@ function readCodingCliStatusPayload() {
1927
2254
  };
1928
2255
  });
1929
2256
  return {
1930
- selected_cli,
1931
- default_model: selected_cli === 'codex' ? codexDefaultModel : null,
1932
- model_options: selected_cli === 'codex' ? codexModelOptions : [],
2257
+ selected_cli: 'codex',
2258
+ default_model: codexDefaultModel,
2259
+ model_options: codexModelOptions,
1933
2260
  clis,
1934
2261
  agent_runtimes: [
1935
2262
  {
@@ -1941,53 +2268,9 @@ function readCodingCliStatusPayload() {
1941
2268
  model_options: codexModelOptions,
1942
2269
  transport: 'app-server',
1943
2270
  recommended: true,
1944
- default: selected_cli === 'codex' && codexTransport === 'app-server',
2271
+ default: true,
1945
2272
  description: 'Daemon-managed codex app-server process with protocol-native streaming.',
1946
2273
  },
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
2274
  ],
1992
2275
  installed_apps,
1993
2276
  codex_auth: detectCodexAuthStatus(codexInstalled),
@@ -2173,52 +2456,20 @@ async function readCodexAgentStatusPayload() {
2173
2456
  };
2174
2457
  }
2175
2458
  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
2459
  const codexDefaultModel = readCodexDefaultModel();
2181
2460
  const codexModelOptions = readCodexModelOptions(codexDefaultModel);
2182
2461
  return {
2183
- selected_cli,
2184
- default_model: selected_cli === 'codex' ? codexDefaultModel : null,
2185
- model_options: selected_cli === 'codex' ? codexModelOptions : [],
2462
+ selected_cli: 'codex',
2463
+ default_model: codexDefaultModel,
2464
+ model_options: codexModelOptions,
2186
2465
  agent_runtimes: [
2187
2466
  {
2188
2467
  id: 'codex-app-server',
2189
- default: selected_cli === 'codex' && codexTransport === 'app-server',
2468
+ default: true,
2190
2469
  default_model: codexDefaultModel,
2191
2470
  model_options: codexModelOptions,
2192
2471
  transport: 'app-server',
2193
2472
  },
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
2473
  ],
2223
2474
  };
2224
2475
  }
@@ -2504,7 +2755,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2504
2755
  try {
2505
2756
  const transport = normalizeCodexTransport(decodeURIComponent(codexTransportDefaultMatch[1] || '').trim());
2506
2757
  if (!transport) {
2507
- writeProtocolError(res, 400, 'validation_error', 'invalid_codex_transport', 'transport must be one of: app-server, exec, acpx');
2758
+ writeProtocolError(res, 400, 'validation_error', 'invalid_codex_transport', 'transport must be app-server');
2508
2759
  return true;
2509
2760
  }
2510
2761
  const result = setDefaultCodexTransport(transport);
@@ -2521,11 +2772,20 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2521
2772
  }
2522
2773
  if (pathname === '/api/computers/{computer_id}/agents/codex/default' && method === 'PUT') {
2523
2774
  try {
2775
+ const parsed = await readJsonBody(req);
2776
+ const model = parsed && typeof parsed === 'object' && typeof parsed.model === 'string'
2777
+ ? String(parsed.model).trim()
2778
+ : '';
2524
2779
  const result = setDefaultCodingCli('codex');
2780
+ if (model) {
2781
+ writeCodexDefaultModel(model);
2782
+ }
2525
2783
  writeJson(res, 200, {
2526
2784
  ok: true,
2527
2785
  ...result,
2528
- status: readCachedCodingCliStatusPayload({ force: true }),
2786
+ status: model
2787
+ ? readRuntimeModelStatusPayload()
2788
+ : readCachedCodingCliStatusPayload({ force: true }),
2529
2789
  });
2530
2790
  }
2531
2791
  catch (err) {
@@ -2716,13 +2976,49 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2716
2976
  numericQueryParam(coerceSingleQueryParam(url.searchParams.get('after')));
2717
2977
  const replayAfter = afterSequence ?? readThreadEventLogHead(threadId);
2718
2978
  addThreadStreamClient(threadId, res);
2979
+ const sessionPath = walkSessionFiles(path.join(localAgentHome(), 'sessions')).get(threadId);
2980
+ let jsonlOffset = sessionPath && fs.existsSync(sessionPath) ? fs.statSync(sessionPath).size : 0;
2981
+ let jsonlEventIndex = 0;
2982
+ const workspacePath = readThreadWorkspacePath(sessionPath) || process.cwd();
2983
+ const withThreadContext = (chunk) => attachThreadContextToChunk(chunk, readThreadContext({ threadId, workspace: workspacePath }));
2984
+ const stopCodexObserver = process.env.NODE_ENV === 'test'
2985
+ ? () => { }
2986
+ : startCodexThreadObserver({
2987
+ threadId,
2988
+ cwd: workspacePath,
2989
+ write: (chunk, id) => writeThreadStreamEvent(res, withThreadContext(chunk), id ? { id } : {}),
2990
+ });
2719
2991
  const pingTimer = setInterval(() => {
2720
2992
  writeSse(res);
2721
2993
  res.flush?.();
2722
2994
  }, SPACE_LOG_STREAM_PING_MS);
2723
2995
  pingTimer.unref?.();
2996
+ const jsonlPollTimer = sessionPath
2997
+ ? setInterval(() => {
2998
+ const read = readAppendedJsonlLines(sessionPath, jsonlOffset);
2999
+ jsonlOffset = read.offset;
3000
+ for (const line of read.lines) {
3001
+ if (!isCodexHistoryTranscriptLine(line))
3002
+ continue;
3003
+ const parsed = parseJsonLine(line);
3004
+ if (!parsed)
3005
+ continue;
3006
+ if (isCodexJsonlMessageRecord(parsed))
3007
+ continue;
3008
+ jsonlEventIndex += 1;
3009
+ writeThreadStreamEvent(res, withThreadContext(codexJsonlEventChunk(parsed)), {
3010
+ id: `jsonl-${jsonlOffset}-${jsonlEventIndex}`,
3011
+ });
3012
+ }
3013
+ res.flush?.();
3014
+ }, MIRRORED_THREAD_STREAM_JSONL_POLL_MS)
3015
+ : null;
3016
+ jsonlPollTimer?.unref?.();
2724
3017
  const cleanup = () => {
2725
3018
  clearInterval(pingTimer);
3019
+ if (jsonlPollTimer)
3020
+ clearInterval(jsonlPollTimer);
3021
+ stopCodexObserver();
2726
3022
  removeThreadStreamClient(threadId, res);
2727
3023
  };
2728
3024
  req.on('close', cleanup);
@@ -2737,7 +3033,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2737
3033
  const chunk = threadStreamChunkForEvent(event);
2738
3034
  if (!chunk)
2739
3035
  continue;
2740
- writeThreadStreamEvent(res, chunk, { id: event.sequence });
3036
+ writeThreadStreamEvent(res, withThreadContext(chunk), { id: event.sequence });
2741
3037
  }
2742
3038
  res.flush?.();
2743
3039
  return true;
@@ -2764,7 +3060,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2764
3060
  const requestedAgentId = parsed && typeof parsed === 'object' && typeof parsed.agent_id === 'string'
2765
3061
  ? String(parsed.agent_id).trim()
2766
3062
  : '';
2767
- const fallbackAgentId = getAllAgents()[0]?.agent_id || 'local-agent-mirror';
3063
+ const fallbackAgentId = getAllAgents()[0]?.agent_id || 'codex';
2768
3064
  const agentId = requestedAgentId || fallbackAgentId;
2769
3065
  const session = threadId ? adoptMirroredThread(threadId, agentId) : null;
2770
3066
  if (!session) {