@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.
- package/README.md +1 -1
- package/daemon/dist/agent-sdk/drivers/codex-app-server-driver.d.ts.map +1 -1
- package/daemon/dist/agent-sdk/drivers/codex-app-server-driver.js +0 -2
- package/daemon/dist/agent-sdk/drivers/codex-app-server-driver.js.map +1 -1
- package/daemon/dist/agent-sdk/drivers/registry.d.ts +2 -8
- package/daemon/dist/agent-sdk/drivers/registry.d.ts.map +1 -1
- package/daemon/dist/agent-sdk/drivers/registry.js +7 -138
- package/daemon/dist/agent-sdk/drivers/registry.js.map +1 -1
- package/daemon/dist/agent-sdk/index.d.ts +0 -11
- package/daemon/dist/agent-sdk/index.d.ts.map +1 -1
- package/daemon/dist/agent-sdk/index.js +0 -11
- package/daemon/dist/agent-sdk/index.js.map +1 -1
- package/daemon/dist/agent-sdk/session-manager.d.ts +1 -1
- package/daemon/dist/agent-sdk/session-manager.d.ts.map +1 -1
- package/daemon/dist/agent-sdk/session-manager.js +1 -2
- package/daemon/dist/agent-sdk/session-manager.js.map +1 -1
- package/daemon/dist/channels/acp.d.ts.map +1 -1
- package/daemon/dist/channels/acp.js +2 -3
- package/daemon/dist/channels/acp.js.map +1 -1
- package/daemon/dist/channels/http-routes.js +6 -6
- package/daemon/dist/channels/http-routes.js.map +1 -1
- package/daemon/dist/core/config.d.ts +1 -1
- package/daemon/dist/core/config.d.ts.map +1 -1
- package/daemon/dist/core/config.js +1 -1
- package/daemon/dist/core/config.js.map +1 -1
- package/daemon/dist/core/cortex.d.ts.map +1 -1
- package/daemon/dist/core/cortex.js +90 -205
- package/daemon/dist/core/cortex.js.map +1 -1
- package/daemon/dist/core/env.js +1 -1
- package/daemon/dist/core/gateway.d.ts.map +1 -1
- package/daemon/dist/core/gateway.js +1 -11
- package/daemon/dist/core/gateway.js.map +1 -1
- package/daemon/dist/core/loop-guard.d.ts +1 -1
- package/daemon/dist/core/loop-guard.js +1 -1
- package/daemon/dist/core/protocol-adapter.d.ts +1 -3
- package/daemon/dist/core/protocol-adapter.d.ts.map +1 -1
- package/daemon/dist/core/protocol-adapter.js +1 -3
- package/daemon/dist/core/protocol-adapter.js.map +1 -1
- package/daemon/dist/http/routes/rpc.d.ts.map +1 -1
- package/daemon/dist/http/routes/rpc.js +16 -1
- package/daemon/dist/http/routes/rpc.js.map +1 -1
- package/daemon/dist/http/routes/system.d.ts +2 -0
- package/daemon/dist/http/routes/system.d.ts.map +1 -1
- package/daemon/dist/http/routes/system.js +426 -138
- package/daemon/dist/http/routes/system.js.map +1 -1
- package/daemon/dist/mcp/index.d.ts +2 -6
- package/daemon/dist/mcp/index.d.ts.map +1 -1
- package/daemon/dist/mcp/index.js +2 -6
- package/daemon/dist/mcp/index.js.map +1 -1
- package/daemon/dist/mcp/tools.d.ts +5 -7
- package/daemon/dist/mcp/tools.d.ts.map +1 -1
- package/daemon/dist/mcp/tools.js +4 -8
- package/daemon/dist/mcp/tools.js.map +1 -1
- package/daemon/dist/tools/built-ins.d.ts +9 -84
- package/daemon/dist/tools/built-ins.d.ts.map +1 -1
- package/daemon/dist/tools/built-ins.js +3 -3
- package/daemon/dist/tools/built-ins.js.map +1 -1
- package/daemon/dist/tools/local-tool.d.ts +7 -0
- package/daemon/dist/tools/local-tool.d.ts.map +1 -0
- package/daemon/dist/tools/local-tool.js +4 -0
- package/daemon/dist/tools/local-tool.js.map +1 -0
- package/daemon/dist/tools/skill-tools.d.ts +5 -8
- package/daemon/dist/tools/skill-tools.d.ts.map +1 -1
- package/daemon/dist/tools/skill-tools.js +6 -10
- package/daemon/dist/tools/skill-tools.js.map +1 -1
- package/daemon/package.json +6 -9
- package/dist/backend.js +5 -5
- package/dist/backend.js.map +1 -1
- package/dist/bootstrap.js +2 -6
- package/dist/bootstrap.js.map +1 -1
- package/dist/computer.js +1 -1
- package/dist/doctor.js +1 -1
- package/dist/doctor.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/repl.js +6 -66
- package/dist/repl.js.map +1 -1
- package/dist/service.js +2 -3
- package/dist/service.js.map +1 -1
- package/package.json +3 -6
- package/daemon/dist/agent-sdk/drivers/acpx-driver.d.ts +0 -21
- package/daemon/dist/agent-sdk/drivers/acpx-driver.d.ts.map +0 -1
- package/daemon/dist/agent-sdk/drivers/acpx-driver.js +0 -488
- package/daemon/dist/agent-sdk/drivers/acpx-driver.js.map +0 -1
- package/daemon/dist/agent-sdk/drivers/claude-acpx-driver.d.ts +0 -5
- package/daemon/dist/agent-sdk/drivers/claude-acpx-driver.d.ts.map +0 -1
- package/daemon/dist/agent-sdk/drivers/claude-acpx-driver.js +0 -7
- package/daemon/dist/agent-sdk/drivers/claude-acpx-driver.js.map +0 -1
- package/daemon/dist/agent-sdk/drivers/claude-cli-direct-driver.d.ts +0 -20
- package/daemon/dist/agent-sdk/drivers/claude-cli-direct-driver.d.ts.map +0 -1
- package/daemon/dist/agent-sdk/drivers/claude-cli-direct-driver.js +0 -264
- package/daemon/dist/agent-sdk/drivers/claude-cli-direct-driver.js.map +0 -1
- package/daemon/dist/agent-sdk/drivers/claude-code-driver.d.ts +0 -29
- package/daemon/dist/agent-sdk/drivers/claude-code-driver.d.ts.map +0 -1
- package/daemon/dist/agent-sdk/drivers/claude-code-driver.js +0 -24
- package/daemon/dist/agent-sdk/drivers/claude-code-driver.js.map +0 -1
- package/daemon/dist/agent-sdk/drivers/claude-code-events.d.ts +0 -25
- package/daemon/dist/agent-sdk/drivers/claude-code-events.d.ts.map +0 -1
- package/daemon/dist/agent-sdk/drivers/claude-code-events.js +0 -58
- package/daemon/dist/agent-sdk/drivers/claude-code-events.js.map +0 -1
- package/daemon/dist/agent-sdk/drivers/claude-code-items.d.ts +0 -41
- package/daemon/dist/agent-sdk/drivers/claude-code-items.d.ts.map +0 -1
- package/daemon/dist/agent-sdk/drivers/claude-code-items.js +0 -77
- package/daemon/dist/agent-sdk/drivers/claude-code-items.js.map +0 -1
- package/daemon/dist/agent-sdk/drivers/codex-acpx-driver.d.ts +0 -5
- package/daemon/dist/agent-sdk/drivers/codex-acpx-driver.d.ts.map +0 -1
- package/daemon/dist/agent-sdk/drivers/codex-acpx-driver.js +0 -7
- package/daemon/dist/agent-sdk/drivers/codex-acpx-driver.js.map +0 -1
- package/daemon/dist/agent-sdk/drivers/codex-exec-driver.d.ts +0 -28
- package/daemon/dist/agent-sdk/drivers/codex-exec-driver.d.ts.map +0 -1
- package/daemon/dist/agent-sdk/drivers/codex-exec-driver.js +0 -219
- package/daemon/dist/agent-sdk/drivers/codex-exec-driver.js.map +0 -1
- package/daemon/dist/agent-sdk/drivers/codex-exec-events.d.ts +0 -9
- package/daemon/dist/agent-sdk/drivers/codex-exec-events.d.ts.map +0 -1
- package/daemon/dist/agent-sdk/drivers/codex-exec-events.js +0 -82
- package/daemon/dist/agent-sdk/drivers/codex-exec-events.js.map +0 -1
- package/daemon/dist/agent-sdk/drivers/codex-exec-items.d.ts +0 -9
- package/daemon/dist/agent-sdk/drivers/codex-exec-items.d.ts.map +0 -1
- package/daemon/dist/agent-sdk/drivers/codex-exec-items.js +0 -86
- package/daemon/dist/agent-sdk/drivers/codex-exec-items.js.map +0 -1
- package/daemon/dist/agent-sdk/drivers/gemini-cli-driver.d.ts +0 -17
- package/daemon/dist/agent-sdk/drivers/gemini-cli-driver.d.ts.map +0 -1
- package/daemon/dist/agent-sdk/drivers/gemini-cli-driver.js +0 -255
- package/daemon/dist/agent-sdk/drivers/gemini-cli-driver.js.map +0 -1
- package/daemon/dist/core/sdk-wrapper.d.ts +0 -36
- package/daemon/dist/core/sdk-wrapper.d.ts.map +0 -1
- package/daemon/dist/core/sdk-wrapper.js +0 -533
- package/daemon/dist/core/sdk-wrapper.js.map +0 -1
- package/daemon/dist/skills/claude_code.d.ts +0 -25
- package/daemon/dist/skills/claude_code.d.ts.map +0 -1
- package/daemon/dist/skills/claude_code.js +0 -49
- 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 =
|
|
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),
|
|
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
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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' ||
|
|
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:
|
|
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:
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
1572
|
+
const collapsedMessages = collapseCompletedAssistantProgressMessages(messages);
|
|
1573
|
+
const slicedMessages = collapsedMessages.slice(-safeLimit);
|
|
1235
1574
|
const slicedEvents = events.slice(-safeLimit * 8);
|
|
1236
|
-
const status =
|
|
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: '
|
|
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:
|
|
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:
|
|
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\/
|
|
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/
|
|
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\/
|
|
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'
|
|
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'
|
|
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'
|
|
1823
|
+
if (value === 'app-server')
|
|
1487
1824
|
return value;
|
|
1488
1825
|
return null;
|
|
1489
1826
|
}
|
|
1490
1827
|
function isManagedCliName(name) {
|
|
1491
|
-
return name === 'codex'
|
|
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:
|
|
1932
|
-
model_options:
|
|
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:
|
|
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:
|
|
2185
|
-
model_options:
|
|
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:
|
|
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
|
|
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:
|
|
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 || '
|
|
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) {
|