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