sessix-server 0.3.0 → 0.3.2
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/dist/index.js +251 -32
- package/dist/server.js +251 -32
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -861,6 +861,9 @@ ${context}`;
|
|
|
861
861
|
var import_child_process2 = require("child_process");
|
|
862
862
|
var import_readline2 = require("readline");
|
|
863
863
|
var import_events2 = require("events");
|
|
864
|
+
var import_fs = require("fs");
|
|
865
|
+
var import_path = require("path");
|
|
866
|
+
var import_os = require("os");
|
|
864
867
|
var import_uuid2 = require("uuid");
|
|
865
868
|
|
|
866
869
|
// src/utils/codexPath.ts
|
|
@@ -891,10 +894,23 @@ function findCodexPath() {
|
|
|
891
894
|
}
|
|
892
895
|
return "codex";
|
|
893
896
|
}
|
|
897
|
+
function resolveCodexJsEntry(codexPath) {
|
|
898
|
+
try {
|
|
899
|
+
const realPath = (0, import_node_fs2.realpathSync)(codexPath);
|
|
900
|
+
const head = (0, import_node_fs2.readFileSync)(realPath, { encoding: "utf-8", flag: "r" }).slice(0, 100);
|
|
901
|
+
if (head.startsWith("#!/usr/bin/env node") || head.startsWith("#!/usr/bin/node")) {
|
|
902
|
+
return realPath;
|
|
903
|
+
}
|
|
904
|
+
} catch {
|
|
905
|
+
}
|
|
906
|
+
return void 0;
|
|
907
|
+
}
|
|
894
908
|
var CODEX_PATH = findCodexPath();
|
|
909
|
+
var CODEX_JS_ENTRY = resolveCodexJsEntry(CODEX_PATH);
|
|
895
910
|
async function getCodexVersion() {
|
|
896
911
|
try {
|
|
897
|
-
const
|
|
912
|
+
const cmd = CODEX_JS_ENTRY ? `"${process.execPath}" "${CODEX_JS_ENTRY}" --version` : `"${CODEX_PATH}" --version`;
|
|
913
|
+
const output = (0, import_node_child_process3.execSync)(cmd, {
|
|
898
914
|
encoding: "utf-8",
|
|
899
915
|
timeout: 5e3
|
|
900
916
|
}).trim();
|
|
@@ -904,6 +920,14 @@ async function getCodexVersion() {
|
|
|
904
920
|
}
|
|
905
921
|
}
|
|
906
922
|
function isCodexAvailable() {
|
|
923
|
+
if (CODEX_JS_ENTRY) {
|
|
924
|
+
try {
|
|
925
|
+
(0, import_node_fs2.accessSync)(CODEX_JS_ENTRY, import_node_fs2.constants.R_OK);
|
|
926
|
+
return true;
|
|
927
|
+
} catch {
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
907
931
|
try {
|
|
908
932
|
(0, import_node_fs2.accessSync)(CODEX_PATH, import_node_fs2.constants.X_OK);
|
|
909
933
|
return true;
|
|
@@ -918,11 +942,18 @@ function isCodexAvailable() {
|
|
|
918
942
|
}
|
|
919
943
|
|
|
920
944
|
// src/providers/CodexProvider.ts
|
|
945
|
+
var SESSIX_DIR = (0, import_path.join)((0, import_os.homedir)(), ".sessix");
|
|
946
|
+
var CODEX_SESSIONS_FILE = (0, import_path.join)(SESSIX_DIR, "codex-sessions.json");
|
|
921
947
|
var CodexProvider = class {
|
|
922
948
|
activeSessions = /* @__PURE__ */ new Map();
|
|
923
949
|
emitter = new import_events2.EventEmitter();
|
|
950
|
+
/** 持久化的会话元数据(sessionId → metadata) */
|
|
951
|
+
persistedSessions = /* @__PURE__ */ new Map();
|
|
924
952
|
/** 自增 ID 计数器,用于生成 ClaudeStreamEvent 中的 message/block ID */
|
|
925
953
|
idCounter = 0;
|
|
954
|
+
constructor() {
|
|
955
|
+
this.loadPersistedSessions();
|
|
956
|
+
}
|
|
926
957
|
async startSession(opts) {
|
|
927
958
|
const { projectPath, message, sessionId: existingSessionId } = opts;
|
|
928
959
|
const sessionId = existingSessionId ?? (0, import_uuid2.v4)();
|
|
@@ -941,12 +972,22 @@ var CodexProvider = class {
|
|
|
941
972
|
agentType: "codex"
|
|
942
973
|
};
|
|
943
974
|
const resume = opts.resume ?? !!existingSessionId;
|
|
944
|
-
|
|
975
|
+
let resumeThreadId;
|
|
976
|
+
if (resume && existingSessionId) {
|
|
977
|
+
const persisted = this.persistedSessions.get(existingSessionId);
|
|
978
|
+
if (persisted?.threadId) {
|
|
979
|
+
resumeThreadId = persisted.threadId;
|
|
980
|
+
console.log(`[CodexProvider] Resuming session ${sessionId} with threadId: ${resumeThreadId}`);
|
|
981
|
+
} else {
|
|
982
|
+
console.warn(`[CodexProvider] Session ${sessionId} resume requested but no persisted threadId found, creating new session`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
const proc = this.spawnCodexProcess(projectPath, message, resumeThreadId, opts.model);
|
|
945
986
|
session.pid = proc.pid;
|
|
946
987
|
this.activeSessions.set(sessionId, {
|
|
947
988
|
session,
|
|
948
989
|
process: proc,
|
|
949
|
-
threadId:
|
|
990
|
+
threadId: resumeThreadId,
|
|
950
991
|
turnStartTime: Date.now()
|
|
951
992
|
});
|
|
952
993
|
const initEvent = {
|
|
@@ -955,6 +996,7 @@ var CodexProvider = class {
|
|
|
955
996
|
session_id: sessionId
|
|
956
997
|
};
|
|
957
998
|
this.emitter.emit(this.getEventName(sessionId), initEvent);
|
|
999
|
+
this.emitUserMessage(sessionId, message);
|
|
958
1000
|
proc.on("error", (err) => {
|
|
959
1001
|
console.error(`[CodexProvider] Session ${sessionId} process error:`, err.message);
|
|
960
1002
|
this.activeSessions.delete(sessionId);
|
|
@@ -978,22 +1020,46 @@ var CodexProvider = class {
|
|
|
978
1020
|
this.activeSessions.delete(sessionId);
|
|
979
1021
|
}
|
|
980
1022
|
async sendMessage(sessionId, message) {
|
|
981
|
-
|
|
1023
|
+
let entry = this.activeSessions.get(sessionId);
|
|
982
1024
|
if (!entry) {
|
|
983
|
-
|
|
1025
|
+
const persisted = this.persistedSessions.get(sessionId);
|
|
1026
|
+
if (!persisted?.threadId) {
|
|
1027
|
+
throw new Error(`Session ${sessionId} not found or already ended`);
|
|
1028
|
+
}
|
|
1029
|
+
const projectId = persisted.projectPath.split("/").filter(Boolean).pop() ?? "unknown";
|
|
1030
|
+
const session = {
|
|
1031
|
+
id: sessionId,
|
|
1032
|
+
projectId,
|
|
1033
|
+
projectPath: persisted.projectPath,
|
|
1034
|
+
status: "running",
|
|
1035
|
+
createdAt: persisted.createdAt,
|
|
1036
|
+
lastActiveAt: Date.now(),
|
|
1037
|
+
summary: persisted.summary,
|
|
1038
|
+
agentType: "codex"
|
|
1039
|
+
};
|
|
1040
|
+
const placeholderProc = (0, import_child_process2.spawn)(process.execPath, ["-e", ""], { stdio: "ignore" });
|
|
1041
|
+
entry = {
|
|
1042
|
+
session,
|
|
1043
|
+
process: placeholderProc,
|
|
1044
|
+
threadId: persisted.threadId,
|
|
1045
|
+
turnStartTime: Date.now()
|
|
1046
|
+
};
|
|
1047
|
+
this.activeSessions.set(sessionId, entry);
|
|
984
1048
|
}
|
|
985
|
-
|
|
1049
|
+
const procAlive = entry.process.exitCode === null && entry.process.signalCode === null;
|
|
1050
|
+
if (procAlive) {
|
|
986
1051
|
try {
|
|
987
1052
|
entry.process.stdin?.end();
|
|
988
1053
|
} catch {
|
|
989
1054
|
}
|
|
990
1055
|
await killProcessCrossPlatform(entry.process);
|
|
991
1056
|
}
|
|
992
|
-
const threadId = entry.threadId;
|
|
1057
|
+
const threadId = entry.threadId ?? this.persistedSessions.get(sessionId)?.threadId;
|
|
993
1058
|
if (!threadId) {
|
|
994
|
-
|
|
1059
|
+
console.warn(`[CodexProvider] Session ${sessionId} has no thread ID, falling back to new thread`);
|
|
995
1060
|
}
|
|
996
|
-
|
|
1061
|
+
this.emitUserMessage(sessionId, message);
|
|
1062
|
+
const proc = this.spawnCodexProcess(entry.session.projectPath, message, threadId ?? void 0);
|
|
997
1063
|
entry.session.status = "running";
|
|
998
1064
|
entry.session.lastActiveAt = Date.now();
|
|
999
1065
|
entry.session.pid = proc.pid;
|
|
@@ -1016,7 +1082,26 @@ var CodexProvider = class {
|
|
|
1016
1082
|
};
|
|
1017
1083
|
}
|
|
1018
1084
|
getActiveSessions() {
|
|
1019
|
-
|
|
1085
|
+
const active = /* @__PURE__ */ new Map();
|
|
1086
|
+
for (const [id, entry] of this.activeSessions) {
|
|
1087
|
+
active.set(id, entry.session);
|
|
1088
|
+
}
|
|
1089
|
+
for (const [id, persisted] of this.persistedSessions) {
|
|
1090
|
+
if (!active.has(id) && persisted.threadId) {
|
|
1091
|
+
const projectId = persisted.projectPath.split("/").filter(Boolean).pop() ?? "unknown";
|
|
1092
|
+
active.set(id, {
|
|
1093
|
+
id,
|
|
1094
|
+
projectId,
|
|
1095
|
+
projectPath: persisted.projectPath,
|
|
1096
|
+
status: "idle",
|
|
1097
|
+
createdAt: persisted.createdAt,
|
|
1098
|
+
lastActiveAt: persisted.lastActiveAt,
|
|
1099
|
+
summary: persisted.summary,
|
|
1100
|
+
agentType: "codex"
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return Array.from(active.values());
|
|
1020
1105
|
}
|
|
1021
1106
|
async generateSuggestion(_context) {
|
|
1022
1107
|
return "";
|
|
@@ -1040,16 +1125,29 @@ var CodexProvider = class {
|
|
|
1040
1125
|
* @param message 用户消息
|
|
1041
1126
|
* @param resumeThreadId 如果提供,则使用 `codex exec resume` 恢复会话
|
|
1042
1127
|
*/
|
|
1043
|
-
spawnCodexProcess(projectPath, message, resumeThreadId) {
|
|
1128
|
+
spawnCodexProcess(projectPath, message, resumeThreadId, model) {
|
|
1044
1129
|
const args = ["exec", "--json", "--full-auto"];
|
|
1130
|
+
if (model) {
|
|
1131
|
+
args.push("-m", model);
|
|
1132
|
+
}
|
|
1133
|
+
args.push("-C", projectPath);
|
|
1045
1134
|
if (resumeThreadId) {
|
|
1046
1135
|
args.push("resume", resumeThreadId);
|
|
1047
|
-
} else {
|
|
1048
|
-
args.push("-C", projectPath);
|
|
1049
1136
|
}
|
|
1050
1137
|
args.push(message);
|
|
1051
1138
|
const env = { ...process.env };
|
|
1052
|
-
|
|
1139
|
+
let cmd;
|
|
1140
|
+
let spawnArgs;
|
|
1141
|
+
if (CODEX_JS_ENTRY) {
|
|
1142
|
+
cmd = process.execPath;
|
|
1143
|
+
spawnArgs = [CODEX_JS_ENTRY, ...args];
|
|
1144
|
+
console.log(`[CodexProvider] Spawning via node: ${cmd} ${CODEX_JS_ENTRY} ${args.join(" ")}`);
|
|
1145
|
+
} else {
|
|
1146
|
+
cmd = CODEX_PATH;
|
|
1147
|
+
spawnArgs = args;
|
|
1148
|
+
console.log(`[CodexProvider] Spawning: ${CODEX_PATH} ${args.join(" ")}`);
|
|
1149
|
+
}
|
|
1150
|
+
const proc = (0, import_child_process2.spawn)(cmd, spawnArgs, {
|
|
1053
1151
|
cwd: projectPath,
|
|
1054
1152
|
env,
|
|
1055
1153
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1071,6 +1169,7 @@ var CodexProvider = class {
|
|
|
1071
1169
|
rl.on("line", (line) => {
|
|
1072
1170
|
const trimmed = line.trim();
|
|
1073
1171
|
if (!trimmed) return;
|
|
1172
|
+
console.log(`[CodexProvider] Session ${sessionId} stdout: ${trimmed.substring(0, 200)}`);
|
|
1074
1173
|
let event;
|
|
1075
1174
|
try {
|
|
1076
1175
|
event = JSON.parse(trimmed);
|
|
@@ -1092,6 +1191,14 @@ var CodexProvider = class {
|
|
|
1092
1191
|
case "thread.started": {
|
|
1093
1192
|
if (event.thread_id) {
|
|
1094
1193
|
entry.threadId = event.thread_id;
|
|
1194
|
+
this.persistSession(sessionId, {
|
|
1195
|
+
threadId: event.thread_id,
|
|
1196
|
+
projectPath: entry.session.projectPath,
|
|
1197
|
+
summary: entry.session.summary,
|
|
1198
|
+
createdAt: entry.session.createdAt,
|
|
1199
|
+
lastActiveAt: Date.now()
|
|
1200
|
+
});
|
|
1201
|
+
console.log(`[CodexProvider] Session ${sessionId} threadId persisted: ${event.thread_id}`);
|
|
1095
1202
|
}
|
|
1096
1203
|
break;
|
|
1097
1204
|
}
|
|
@@ -1119,6 +1226,15 @@ var CodexProvider = class {
|
|
|
1119
1226
|
usage: event.usage
|
|
1120
1227
|
};
|
|
1121
1228
|
this.emitter.emit(this.getEventName(sessionId), resultEvent);
|
|
1229
|
+
if (entry.threadId) {
|
|
1230
|
+
this.persistSession(sessionId, {
|
|
1231
|
+
threadId: entry.threadId,
|
|
1232
|
+
projectPath: entry.session.projectPath,
|
|
1233
|
+
summary: entry.session.summary,
|
|
1234
|
+
createdAt: entry.session.createdAt,
|
|
1235
|
+
lastActiveAt: Date.now()
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1122
1238
|
break;
|
|
1123
1239
|
}
|
|
1124
1240
|
case "turn.failed": {
|
|
@@ -1255,6 +1371,7 @@ var CodexProvider = class {
|
|
|
1255
1371
|
const entry = this.activeSessions.get(sessionId);
|
|
1256
1372
|
if (!entry) return;
|
|
1257
1373
|
if (entry.process !== proc) return;
|
|
1374
|
+
console.log(`[CodexProvider] Session ${sessionId} process exited: code=${code} signal=${signal} threadId=${entry.threadId ?? "NONE"}`);
|
|
1258
1375
|
if (entry.rl) {
|
|
1259
1376
|
entry.rl.close();
|
|
1260
1377
|
entry.rl = void 0;
|
|
@@ -1280,6 +1397,20 @@ var CodexProvider = class {
|
|
|
1280
1397
|
this.emitter.emit(this.getEventName(sessionId), syntheticResult);
|
|
1281
1398
|
});
|
|
1282
1399
|
}
|
|
1400
|
+
/**
|
|
1401
|
+
* 合成用户消息事件(Codex CLI 不会在 stdout 回显用户输入,需手动补充到事件流)
|
|
1402
|
+
*/
|
|
1403
|
+
emitUserMessage(sessionId, message) {
|
|
1404
|
+
const event = {
|
|
1405
|
+
type: "user",
|
|
1406
|
+
session_id: sessionId,
|
|
1407
|
+
message: {
|
|
1408
|
+
role: "user",
|
|
1409
|
+
content: [{ type: "text", text: message }]
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
this.emitter.emit(this.getEventName(sessionId), event);
|
|
1413
|
+
}
|
|
1283
1414
|
emitError(sessionId, message) {
|
|
1284
1415
|
const event = {
|
|
1285
1416
|
type: "result",
|
|
@@ -1295,6 +1426,70 @@ var CodexProvider = class {
|
|
|
1295
1426
|
getEventName(sessionId) {
|
|
1296
1427
|
return `claude:${sessionId}`;
|
|
1297
1428
|
}
|
|
1429
|
+
// ============================================
|
|
1430
|
+
// 持久化方法
|
|
1431
|
+
// ============================================
|
|
1432
|
+
/**
|
|
1433
|
+
* 从磁盘加载持久化的 Codex 会话元数据
|
|
1434
|
+
*/
|
|
1435
|
+
loadPersistedSessions() {
|
|
1436
|
+
try {
|
|
1437
|
+
if (!(0, import_fs.existsSync)(CODEX_SESSIONS_FILE)) return;
|
|
1438
|
+
const data = JSON.parse((0, import_fs.readFileSync)(CODEX_SESSIONS_FILE, "utf-8"));
|
|
1439
|
+
for (const [sessionId, meta] of Object.entries(data)) {
|
|
1440
|
+
this.persistedSessions.set(sessionId, meta);
|
|
1441
|
+
}
|
|
1442
|
+
console.log(`[CodexProvider] Loaded ${this.persistedSessions.size} persisted sessions`);
|
|
1443
|
+
} catch (err) {
|
|
1444
|
+
console.warn("[CodexProvider] Failed to load persisted sessions:", err);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* 持久化单个会话的元数据到磁盘
|
|
1449
|
+
*/
|
|
1450
|
+
persistSession(sessionId, meta) {
|
|
1451
|
+
this.persistedSessions.set(sessionId, meta);
|
|
1452
|
+
this.flushPersistedSessions();
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* 将所有持久化数据写入磁盘
|
|
1456
|
+
*/
|
|
1457
|
+
flushPersistedSessions() {
|
|
1458
|
+
try {
|
|
1459
|
+
if (!(0, import_fs.existsSync)(SESSIX_DIR)) {
|
|
1460
|
+
(0, import_fs.mkdirSync)(SESSIX_DIR, { recursive: true });
|
|
1461
|
+
}
|
|
1462
|
+
const data = {};
|
|
1463
|
+
for (const [sessionId, meta] of this.persistedSessions) {
|
|
1464
|
+
data[sessionId] = meta;
|
|
1465
|
+
}
|
|
1466
|
+
(0, import_fs.writeFileSync)(CODEX_SESSIONS_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
1467
|
+
} catch (err) {
|
|
1468
|
+
console.error("[CodexProvider] Failed to persist sessions:", err);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* 检查某个 sessionId 是否为已知的 Codex 会话(供 SessionManager 查询 agentType)
|
|
1473
|
+
*/
|
|
1474
|
+
isKnownSession(sessionId) {
|
|
1475
|
+
return this.activeSessions.has(sessionId) || this.persistedSessions.has(sessionId);
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* 获取指定项目路径下的 Codex 持久化会话列表(供 project_sessions 合并)
|
|
1479
|
+
*/
|
|
1480
|
+
getPersistedSessionsForProject(projectPath) {
|
|
1481
|
+
const result = [];
|
|
1482
|
+
for (const [sessionId, meta] of this.persistedSessions) {
|
|
1483
|
+
if (meta.projectPath === projectPath && meta.threadId) {
|
|
1484
|
+
result.push({
|
|
1485
|
+
sessionId,
|
|
1486
|
+
lastModified: meta.lastActiveAt,
|
|
1487
|
+
summary: meta.summary
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return result;
|
|
1492
|
+
}
|
|
1298
1493
|
};
|
|
1299
1494
|
|
|
1300
1495
|
// src/providers/ProviderFactory.ts
|
|
@@ -1387,8 +1582,15 @@ var SessionManager = class {
|
|
|
1387
1582
|
}
|
|
1388
1583
|
getProviderForSession(sessionId) {
|
|
1389
1584
|
if (!this.providerFactory) return this.provider;
|
|
1390
|
-
|
|
1391
|
-
|
|
1585
|
+
let agentType = this.sessionAgentType.get(sessionId);
|
|
1586
|
+
if (!agentType) {
|
|
1587
|
+
const codexProvider = this.providerFactory.getProvider("codex");
|
|
1588
|
+
if (codexProvider instanceof CodexProvider && codexProvider.isKnownSession(sessionId)) {
|
|
1589
|
+
agentType = "codex";
|
|
1590
|
+
this.sessionAgentType.set(sessionId, agentType);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
return this.providerFactory.getProvider(agentType ?? "claude-code");
|
|
1392
1594
|
}
|
|
1393
1595
|
// ============================================
|
|
1394
1596
|
// 公开 API
|
|
@@ -3502,11 +3704,11 @@ var ActivityPushChannel = class {
|
|
|
3502
3704
|
// src/session/ProjectReader.ts
|
|
3503
3705
|
var import_promises3 = require("fs/promises");
|
|
3504
3706
|
var import_readline3 = require("readline");
|
|
3505
|
-
var
|
|
3506
|
-
var
|
|
3507
|
-
var CLAUDE_PROJECTS_DIR = (0,
|
|
3707
|
+
var import_path2 = require("path");
|
|
3708
|
+
var import_os2 = require("os");
|
|
3709
|
+
var CLAUDE_PROJECTS_DIR = (0, import_path2.join)((0, import_os2.homedir)(), ".claude", "projects");
|
|
3508
3710
|
function getSessionFilePath(projectPath, sessionId) {
|
|
3509
|
-
return (0,
|
|
3711
|
+
return (0, import_path2.join)(CLAUDE_PROJECTS_DIR, encodeDirName(projectPath), `${sessionId}.jsonl`);
|
|
3510
3712
|
}
|
|
3511
3713
|
async function getProjects() {
|
|
3512
3714
|
try {
|
|
@@ -3523,7 +3725,7 @@ async function getProjects() {
|
|
|
3523
3725
|
const encodedPath = entry.name;
|
|
3524
3726
|
const decodedPath = decodeDirName(encodedPath);
|
|
3525
3727
|
const name = decodedPath.split("/").filter(Boolean).pop() ?? encodedPath;
|
|
3526
|
-
const projectDir = (0,
|
|
3728
|
+
const projectDir = (0, import_path2.join)(CLAUDE_PROJECTS_DIR, encodedPath);
|
|
3527
3729
|
const { count: sessionCount, latestMtime } = await countJsonlFilesWithMtime(projectDir);
|
|
3528
3730
|
projects.push({
|
|
3529
3731
|
id: encodedPath,
|
|
@@ -3545,7 +3747,7 @@ async function getProjects() {
|
|
|
3545
3747
|
async function getHistoricalSessions(projectPath) {
|
|
3546
3748
|
try {
|
|
3547
3749
|
const encodedPath = encodeDirName(projectPath);
|
|
3548
|
-
const projectDir = (0,
|
|
3750
|
+
const projectDir = (0, import_path2.join)(CLAUDE_PROJECTS_DIR, encodedPath);
|
|
3549
3751
|
const dirExists = await directoryExists(projectDir);
|
|
3550
3752
|
if (!dirExists) {
|
|
3551
3753
|
return { ok: true, value: [] };
|
|
@@ -3556,7 +3758,7 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3556
3758
|
await Promise.all(
|
|
3557
3759
|
jsonlFiles.map(async (entry) => {
|
|
3558
3760
|
const sessionId = entry.name.slice(0, -6);
|
|
3559
|
-
const filePath = (0,
|
|
3761
|
+
const filePath = (0, import_path2.join)(projectDir, entry.name);
|
|
3560
3762
|
try {
|
|
3561
3763
|
const contentTs = await extractLastTimestamp(filePath);
|
|
3562
3764
|
if (contentTs) {
|
|
@@ -3575,13 +3777,13 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3575
3777
|
);
|
|
3576
3778
|
for (const entry of uuidDirs) {
|
|
3577
3779
|
try {
|
|
3578
|
-
const fileStat = await (0, import_promises3.stat)((0,
|
|
3780
|
+
const fileStat = await (0, import_promises3.stat)((0, import_path2.join)(projectDir, entry.name));
|
|
3579
3781
|
mtimeMap.set(entry.name, fileStat.mtimeMs);
|
|
3580
3782
|
} catch {
|
|
3581
3783
|
mtimeMap.set(entry.name, 0);
|
|
3582
3784
|
}
|
|
3583
3785
|
}
|
|
3584
|
-
const indexPath = (0,
|
|
3786
|
+
const indexPath = (0, import_path2.join)(projectDir, "sessions-index.json");
|
|
3585
3787
|
const sessionMap = /* @__PURE__ */ new Map();
|
|
3586
3788
|
try {
|
|
3587
3789
|
const indexContent = await (0, import_promises3.readFile)(indexPath, "utf-8");
|
|
@@ -3599,7 +3801,7 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3599
3801
|
}
|
|
3600
3802
|
await Promise.all(
|
|
3601
3803
|
Array.from(sessionMap.values()).filter((s) => (s.messageCount ?? 0) > 0 && !s.summary && !s.firstPrompt).map(async (s) => {
|
|
3602
|
-
const filePath = (0,
|
|
3804
|
+
const filePath = (0, import_path2.join)(projectDir, `${s.sessionId}.jsonl`);
|
|
3603
3805
|
const firstPrompt = await extractFirstPrompt(filePath).catch(() => void 0);
|
|
3604
3806
|
if (firstPrompt) s.firstPrompt = firstPrompt;
|
|
3605
3807
|
})
|
|
@@ -3613,7 +3815,7 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3613
3815
|
if (uuidDirSet.has(sessionId)) {
|
|
3614
3816
|
sessionMap.set(sessionId, { sessionId, lastModified: mtime, messageCount: -1 });
|
|
3615
3817
|
} else {
|
|
3616
|
-
const filePath = (0,
|
|
3818
|
+
const filePath = (0, import_path2.join)(projectDir, `${sessionId}.jsonl`);
|
|
3617
3819
|
const firstPrompt = await extractFirstPrompt(filePath).catch(() => void 0);
|
|
3618
3820
|
sessionMap.set(sessionId, { sessionId, lastModified: mtime, firstPrompt });
|
|
3619
3821
|
}
|
|
@@ -3637,7 +3839,7 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3637
3839
|
async function getSessionHistory(projectPath, sessionId) {
|
|
3638
3840
|
try {
|
|
3639
3841
|
const encodedPath = encodeDirName(projectPath);
|
|
3640
|
-
const filePath = (0,
|
|
3842
|
+
const filePath = (0, import_path2.join)(CLAUDE_PROJECTS_DIR, encodedPath, `${sessionId}.jsonl`);
|
|
3641
3843
|
const raw = await (0, import_promises3.readFile)(filePath, "utf-8").catch((err) => {
|
|
3642
3844
|
if (err.code === "ENOENT") return null;
|
|
3643
3845
|
throw err;
|
|
@@ -3818,15 +4020,15 @@ async function countJsonlFilesWithMtime(dirPath) {
|
|
|
3818
4020
|
await Promise.all([
|
|
3819
4021
|
...jsonlEntries.map(async (entry) => {
|
|
3820
4022
|
try {
|
|
3821
|
-
const contentTs = await extractLastTimestamp((0,
|
|
3822
|
-
const ts = contentTs ?? (await (0, import_promises3.stat)((0,
|
|
4023
|
+
const contentTs = await extractLastTimestamp((0, import_path2.join)(dirPath, entry.name));
|
|
4024
|
+
const ts = contentTs ?? (await (0, import_promises3.stat)((0, import_path2.join)(dirPath, entry.name))).mtimeMs;
|
|
3823
4025
|
if (ts > latestMtime) latestMtime = ts;
|
|
3824
4026
|
} catch {
|
|
3825
4027
|
}
|
|
3826
4028
|
}),
|
|
3827
4029
|
...uuidDirs.map(async (entry) => {
|
|
3828
4030
|
try {
|
|
3829
|
-
const fileStat = await (0, import_promises3.stat)((0,
|
|
4031
|
+
const fileStat = await (0, import_promises3.stat)((0, import_path2.join)(dirPath, entry.name));
|
|
3830
4032
|
if (fileStat.mtimeMs > latestMtime) latestMtime = fileStat.mtimeMs;
|
|
3831
4033
|
} catch {
|
|
3832
4034
|
}
|
|
@@ -4384,10 +4586,27 @@ async function start(opts = {}) {
|
|
|
4384
4586
|
case "list_project_sessions": {
|
|
4385
4587
|
const histResult = await getHistoricalSessions(event.projectPath);
|
|
4386
4588
|
if (histResult.ok) {
|
|
4589
|
+
const sessions = histResult.value;
|
|
4590
|
+
const codexProvider = providerFactory.getProvider("codex");
|
|
4591
|
+
if (codexProvider instanceof CodexProvider) {
|
|
4592
|
+
const codexSessions = codexProvider.getPersistedSessionsForProject(event.projectPath);
|
|
4593
|
+
const existingIds = new Set(sessions.map((s) => s.sessionId));
|
|
4594
|
+
for (const cs of codexSessions) {
|
|
4595
|
+
if (!existingIds.has(cs.sessionId)) {
|
|
4596
|
+
sessions.push({
|
|
4597
|
+
sessionId: cs.sessionId,
|
|
4598
|
+
lastModified: cs.lastModified,
|
|
4599
|
+
summary: cs.summary,
|
|
4600
|
+
agentType: "codex"
|
|
4601
|
+
});
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4604
|
+
sessions.sort((a, b) => b.lastModified - a.lastModified);
|
|
4605
|
+
}
|
|
4387
4606
|
wsBridge.send(ws, {
|
|
4388
4607
|
type: "project_sessions",
|
|
4389
4608
|
projectPath: event.projectPath,
|
|
4390
|
-
sessions
|
|
4609
|
+
sessions
|
|
4391
4610
|
});
|
|
4392
4611
|
} else {
|
|
4393
4612
|
wsBridge.send(ws, {
|
package/dist/server.js
CHANGED
|
@@ -866,6 +866,9 @@ ${context}`;
|
|
|
866
866
|
var import_child_process2 = require("child_process");
|
|
867
867
|
var import_readline2 = require("readline");
|
|
868
868
|
var import_events2 = require("events");
|
|
869
|
+
var import_fs = require("fs");
|
|
870
|
+
var import_path = require("path");
|
|
871
|
+
var import_os = require("os");
|
|
869
872
|
var import_uuid2 = require("uuid");
|
|
870
873
|
|
|
871
874
|
// src/utils/codexPath.ts
|
|
@@ -896,10 +899,23 @@ function findCodexPath() {
|
|
|
896
899
|
}
|
|
897
900
|
return "codex";
|
|
898
901
|
}
|
|
902
|
+
function resolveCodexJsEntry(codexPath) {
|
|
903
|
+
try {
|
|
904
|
+
const realPath = (0, import_node_fs2.realpathSync)(codexPath);
|
|
905
|
+
const head = (0, import_node_fs2.readFileSync)(realPath, { encoding: "utf-8", flag: "r" }).slice(0, 100);
|
|
906
|
+
if (head.startsWith("#!/usr/bin/env node") || head.startsWith("#!/usr/bin/node")) {
|
|
907
|
+
return realPath;
|
|
908
|
+
}
|
|
909
|
+
} catch {
|
|
910
|
+
}
|
|
911
|
+
return void 0;
|
|
912
|
+
}
|
|
899
913
|
var CODEX_PATH = findCodexPath();
|
|
914
|
+
var CODEX_JS_ENTRY = resolveCodexJsEntry(CODEX_PATH);
|
|
900
915
|
async function getCodexVersion() {
|
|
901
916
|
try {
|
|
902
|
-
const
|
|
917
|
+
const cmd = CODEX_JS_ENTRY ? `"${process.execPath}" "${CODEX_JS_ENTRY}" --version` : `"${CODEX_PATH}" --version`;
|
|
918
|
+
const output = (0, import_node_child_process3.execSync)(cmd, {
|
|
903
919
|
encoding: "utf-8",
|
|
904
920
|
timeout: 5e3
|
|
905
921
|
}).trim();
|
|
@@ -909,6 +925,14 @@ async function getCodexVersion() {
|
|
|
909
925
|
}
|
|
910
926
|
}
|
|
911
927
|
function isCodexAvailable() {
|
|
928
|
+
if (CODEX_JS_ENTRY) {
|
|
929
|
+
try {
|
|
930
|
+
(0, import_node_fs2.accessSync)(CODEX_JS_ENTRY, import_node_fs2.constants.R_OK);
|
|
931
|
+
return true;
|
|
932
|
+
} catch {
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
912
936
|
try {
|
|
913
937
|
(0, import_node_fs2.accessSync)(CODEX_PATH, import_node_fs2.constants.X_OK);
|
|
914
938
|
return true;
|
|
@@ -923,11 +947,18 @@ function isCodexAvailable() {
|
|
|
923
947
|
}
|
|
924
948
|
|
|
925
949
|
// src/providers/CodexProvider.ts
|
|
950
|
+
var SESSIX_DIR = (0, import_path.join)((0, import_os.homedir)(), ".sessix");
|
|
951
|
+
var CODEX_SESSIONS_FILE = (0, import_path.join)(SESSIX_DIR, "codex-sessions.json");
|
|
926
952
|
var CodexProvider = class {
|
|
927
953
|
activeSessions = /* @__PURE__ */ new Map();
|
|
928
954
|
emitter = new import_events2.EventEmitter();
|
|
955
|
+
/** 持久化的会话元数据(sessionId → metadata) */
|
|
956
|
+
persistedSessions = /* @__PURE__ */ new Map();
|
|
929
957
|
/** 自增 ID 计数器,用于生成 ClaudeStreamEvent 中的 message/block ID */
|
|
930
958
|
idCounter = 0;
|
|
959
|
+
constructor() {
|
|
960
|
+
this.loadPersistedSessions();
|
|
961
|
+
}
|
|
931
962
|
async startSession(opts) {
|
|
932
963
|
const { projectPath, message, sessionId: existingSessionId } = opts;
|
|
933
964
|
const sessionId = existingSessionId ?? (0, import_uuid2.v4)();
|
|
@@ -946,12 +977,22 @@ var CodexProvider = class {
|
|
|
946
977
|
agentType: "codex"
|
|
947
978
|
};
|
|
948
979
|
const resume = opts.resume ?? !!existingSessionId;
|
|
949
|
-
|
|
980
|
+
let resumeThreadId;
|
|
981
|
+
if (resume && existingSessionId) {
|
|
982
|
+
const persisted = this.persistedSessions.get(existingSessionId);
|
|
983
|
+
if (persisted?.threadId) {
|
|
984
|
+
resumeThreadId = persisted.threadId;
|
|
985
|
+
console.log(`[CodexProvider] Resuming session ${sessionId} with threadId: ${resumeThreadId}`);
|
|
986
|
+
} else {
|
|
987
|
+
console.warn(`[CodexProvider] Session ${sessionId} resume requested but no persisted threadId found, creating new session`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
const proc = this.spawnCodexProcess(projectPath, message, resumeThreadId, opts.model);
|
|
950
991
|
session.pid = proc.pid;
|
|
951
992
|
this.activeSessions.set(sessionId, {
|
|
952
993
|
session,
|
|
953
994
|
process: proc,
|
|
954
|
-
threadId:
|
|
995
|
+
threadId: resumeThreadId,
|
|
955
996
|
turnStartTime: Date.now()
|
|
956
997
|
});
|
|
957
998
|
const initEvent = {
|
|
@@ -960,6 +1001,7 @@ var CodexProvider = class {
|
|
|
960
1001
|
session_id: sessionId
|
|
961
1002
|
};
|
|
962
1003
|
this.emitter.emit(this.getEventName(sessionId), initEvent);
|
|
1004
|
+
this.emitUserMessage(sessionId, message);
|
|
963
1005
|
proc.on("error", (err) => {
|
|
964
1006
|
console.error(`[CodexProvider] Session ${sessionId} process error:`, err.message);
|
|
965
1007
|
this.activeSessions.delete(sessionId);
|
|
@@ -983,22 +1025,46 @@ var CodexProvider = class {
|
|
|
983
1025
|
this.activeSessions.delete(sessionId);
|
|
984
1026
|
}
|
|
985
1027
|
async sendMessage(sessionId, message) {
|
|
986
|
-
|
|
1028
|
+
let entry = this.activeSessions.get(sessionId);
|
|
987
1029
|
if (!entry) {
|
|
988
|
-
|
|
1030
|
+
const persisted = this.persistedSessions.get(sessionId);
|
|
1031
|
+
if (!persisted?.threadId) {
|
|
1032
|
+
throw new Error(`Session ${sessionId} not found or already ended`);
|
|
1033
|
+
}
|
|
1034
|
+
const projectId = persisted.projectPath.split("/").filter(Boolean).pop() ?? "unknown";
|
|
1035
|
+
const session = {
|
|
1036
|
+
id: sessionId,
|
|
1037
|
+
projectId,
|
|
1038
|
+
projectPath: persisted.projectPath,
|
|
1039
|
+
status: "running",
|
|
1040
|
+
createdAt: persisted.createdAt,
|
|
1041
|
+
lastActiveAt: Date.now(),
|
|
1042
|
+
summary: persisted.summary,
|
|
1043
|
+
agentType: "codex"
|
|
1044
|
+
};
|
|
1045
|
+
const placeholderProc = (0, import_child_process2.spawn)(process.execPath, ["-e", ""], { stdio: "ignore" });
|
|
1046
|
+
entry = {
|
|
1047
|
+
session,
|
|
1048
|
+
process: placeholderProc,
|
|
1049
|
+
threadId: persisted.threadId,
|
|
1050
|
+
turnStartTime: Date.now()
|
|
1051
|
+
};
|
|
1052
|
+
this.activeSessions.set(sessionId, entry);
|
|
989
1053
|
}
|
|
990
|
-
|
|
1054
|
+
const procAlive = entry.process.exitCode === null && entry.process.signalCode === null;
|
|
1055
|
+
if (procAlive) {
|
|
991
1056
|
try {
|
|
992
1057
|
entry.process.stdin?.end();
|
|
993
1058
|
} catch {
|
|
994
1059
|
}
|
|
995
1060
|
await killProcessCrossPlatform(entry.process);
|
|
996
1061
|
}
|
|
997
|
-
const threadId = entry.threadId;
|
|
1062
|
+
const threadId = entry.threadId ?? this.persistedSessions.get(sessionId)?.threadId;
|
|
998
1063
|
if (!threadId) {
|
|
999
|
-
|
|
1064
|
+
console.warn(`[CodexProvider] Session ${sessionId} has no thread ID, falling back to new thread`);
|
|
1000
1065
|
}
|
|
1001
|
-
|
|
1066
|
+
this.emitUserMessage(sessionId, message);
|
|
1067
|
+
const proc = this.spawnCodexProcess(entry.session.projectPath, message, threadId ?? void 0);
|
|
1002
1068
|
entry.session.status = "running";
|
|
1003
1069
|
entry.session.lastActiveAt = Date.now();
|
|
1004
1070
|
entry.session.pid = proc.pid;
|
|
@@ -1021,7 +1087,26 @@ var CodexProvider = class {
|
|
|
1021
1087
|
};
|
|
1022
1088
|
}
|
|
1023
1089
|
getActiveSessions() {
|
|
1024
|
-
|
|
1090
|
+
const active = /* @__PURE__ */ new Map();
|
|
1091
|
+
for (const [id, entry] of this.activeSessions) {
|
|
1092
|
+
active.set(id, entry.session);
|
|
1093
|
+
}
|
|
1094
|
+
for (const [id, persisted] of this.persistedSessions) {
|
|
1095
|
+
if (!active.has(id) && persisted.threadId) {
|
|
1096
|
+
const projectId = persisted.projectPath.split("/").filter(Boolean).pop() ?? "unknown";
|
|
1097
|
+
active.set(id, {
|
|
1098
|
+
id,
|
|
1099
|
+
projectId,
|
|
1100
|
+
projectPath: persisted.projectPath,
|
|
1101
|
+
status: "idle",
|
|
1102
|
+
createdAt: persisted.createdAt,
|
|
1103
|
+
lastActiveAt: persisted.lastActiveAt,
|
|
1104
|
+
summary: persisted.summary,
|
|
1105
|
+
agentType: "codex"
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return Array.from(active.values());
|
|
1025
1110
|
}
|
|
1026
1111
|
async generateSuggestion(_context) {
|
|
1027
1112
|
return "";
|
|
@@ -1045,16 +1130,29 @@ var CodexProvider = class {
|
|
|
1045
1130
|
* @param message 用户消息
|
|
1046
1131
|
* @param resumeThreadId 如果提供,则使用 `codex exec resume` 恢复会话
|
|
1047
1132
|
*/
|
|
1048
|
-
spawnCodexProcess(projectPath, message, resumeThreadId) {
|
|
1133
|
+
spawnCodexProcess(projectPath, message, resumeThreadId, model) {
|
|
1049
1134
|
const args = ["exec", "--json", "--full-auto"];
|
|
1135
|
+
if (model) {
|
|
1136
|
+
args.push("-m", model);
|
|
1137
|
+
}
|
|
1138
|
+
args.push("-C", projectPath);
|
|
1050
1139
|
if (resumeThreadId) {
|
|
1051
1140
|
args.push("resume", resumeThreadId);
|
|
1052
|
-
} else {
|
|
1053
|
-
args.push("-C", projectPath);
|
|
1054
1141
|
}
|
|
1055
1142
|
args.push(message);
|
|
1056
1143
|
const env = { ...process.env };
|
|
1057
|
-
|
|
1144
|
+
let cmd;
|
|
1145
|
+
let spawnArgs;
|
|
1146
|
+
if (CODEX_JS_ENTRY) {
|
|
1147
|
+
cmd = process.execPath;
|
|
1148
|
+
spawnArgs = [CODEX_JS_ENTRY, ...args];
|
|
1149
|
+
console.log(`[CodexProvider] Spawning via node: ${cmd} ${CODEX_JS_ENTRY} ${args.join(" ")}`);
|
|
1150
|
+
} else {
|
|
1151
|
+
cmd = CODEX_PATH;
|
|
1152
|
+
spawnArgs = args;
|
|
1153
|
+
console.log(`[CodexProvider] Spawning: ${CODEX_PATH} ${args.join(" ")}`);
|
|
1154
|
+
}
|
|
1155
|
+
const proc = (0, import_child_process2.spawn)(cmd, spawnArgs, {
|
|
1058
1156
|
cwd: projectPath,
|
|
1059
1157
|
env,
|
|
1060
1158
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1076,6 +1174,7 @@ var CodexProvider = class {
|
|
|
1076
1174
|
rl.on("line", (line) => {
|
|
1077
1175
|
const trimmed = line.trim();
|
|
1078
1176
|
if (!trimmed) return;
|
|
1177
|
+
console.log(`[CodexProvider] Session ${sessionId} stdout: ${trimmed.substring(0, 200)}`);
|
|
1079
1178
|
let event;
|
|
1080
1179
|
try {
|
|
1081
1180
|
event = JSON.parse(trimmed);
|
|
@@ -1097,6 +1196,14 @@ var CodexProvider = class {
|
|
|
1097
1196
|
case "thread.started": {
|
|
1098
1197
|
if (event.thread_id) {
|
|
1099
1198
|
entry.threadId = event.thread_id;
|
|
1199
|
+
this.persistSession(sessionId, {
|
|
1200
|
+
threadId: event.thread_id,
|
|
1201
|
+
projectPath: entry.session.projectPath,
|
|
1202
|
+
summary: entry.session.summary,
|
|
1203
|
+
createdAt: entry.session.createdAt,
|
|
1204
|
+
lastActiveAt: Date.now()
|
|
1205
|
+
});
|
|
1206
|
+
console.log(`[CodexProvider] Session ${sessionId} threadId persisted: ${event.thread_id}`);
|
|
1100
1207
|
}
|
|
1101
1208
|
break;
|
|
1102
1209
|
}
|
|
@@ -1124,6 +1231,15 @@ var CodexProvider = class {
|
|
|
1124
1231
|
usage: event.usage
|
|
1125
1232
|
};
|
|
1126
1233
|
this.emitter.emit(this.getEventName(sessionId), resultEvent);
|
|
1234
|
+
if (entry.threadId) {
|
|
1235
|
+
this.persistSession(sessionId, {
|
|
1236
|
+
threadId: entry.threadId,
|
|
1237
|
+
projectPath: entry.session.projectPath,
|
|
1238
|
+
summary: entry.session.summary,
|
|
1239
|
+
createdAt: entry.session.createdAt,
|
|
1240
|
+
lastActiveAt: Date.now()
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1127
1243
|
break;
|
|
1128
1244
|
}
|
|
1129
1245
|
case "turn.failed": {
|
|
@@ -1260,6 +1376,7 @@ var CodexProvider = class {
|
|
|
1260
1376
|
const entry = this.activeSessions.get(sessionId);
|
|
1261
1377
|
if (!entry) return;
|
|
1262
1378
|
if (entry.process !== proc) return;
|
|
1379
|
+
console.log(`[CodexProvider] Session ${sessionId} process exited: code=${code} signal=${signal} threadId=${entry.threadId ?? "NONE"}`);
|
|
1263
1380
|
if (entry.rl) {
|
|
1264
1381
|
entry.rl.close();
|
|
1265
1382
|
entry.rl = void 0;
|
|
@@ -1285,6 +1402,20 @@ var CodexProvider = class {
|
|
|
1285
1402
|
this.emitter.emit(this.getEventName(sessionId), syntheticResult);
|
|
1286
1403
|
});
|
|
1287
1404
|
}
|
|
1405
|
+
/**
|
|
1406
|
+
* 合成用户消息事件(Codex CLI 不会在 stdout 回显用户输入,需手动补充到事件流)
|
|
1407
|
+
*/
|
|
1408
|
+
emitUserMessage(sessionId, message) {
|
|
1409
|
+
const event = {
|
|
1410
|
+
type: "user",
|
|
1411
|
+
session_id: sessionId,
|
|
1412
|
+
message: {
|
|
1413
|
+
role: "user",
|
|
1414
|
+
content: [{ type: "text", text: message }]
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
this.emitter.emit(this.getEventName(sessionId), event);
|
|
1418
|
+
}
|
|
1288
1419
|
emitError(sessionId, message) {
|
|
1289
1420
|
const event = {
|
|
1290
1421
|
type: "result",
|
|
@@ -1300,6 +1431,70 @@ var CodexProvider = class {
|
|
|
1300
1431
|
getEventName(sessionId) {
|
|
1301
1432
|
return `claude:${sessionId}`;
|
|
1302
1433
|
}
|
|
1434
|
+
// ============================================
|
|
1435
|
+
// 持久化方法
|
|
1436
|
+
// ============================================
|
|
1437
|
+
/**
|
|
1438
|
+
* 从磁盘加载持久化的 Codex 会话元数据
|
|
1439
|
+
*/
|
|
1440
|
+
loadPersistedSessions() {
|
|
1441
|
+
try {
|
|
1442
|
+
if (!(0, import_fs.existsSync)(CODEX_SESSIONS_FILE)) return;
|
|
1443
|
+
const data = JSON.parse((0, import_fs.readFileSync)(CODEX_SESSIONS_FILE, "utf-8"));
|
|
1444
|
+
for (const [sessionId, meta] of Object.entries(data)) {
|
|
1445
|
+
this.persistedSessions.set(sessionId, meta);
|
|
1446
|
+
}
|
|
1447
|
+
console.log(`[CodexProvider] Loaded ${this.persistedSessions.size} persisted sessions`);
|
|
1448
|
+
} catch (err) {
|
|
1449
|
+
console.warn("[CodexProvider] Failed to load persisted sessions:", err);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* 持久化单个会话的元数据到磁盘
|
|
1454
|
+
*/
|
|
1455
|
+
persistSession(sessionId, meta) {
|
|
1456
|
+
this.persistedSessions.set(sessionId, meta);
|
|
1457
|
+
this.flushPersistedSessions();
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* 将所有持久化数据写入磁盘
|
|
1461
|
+
*/
|
|
1462
|
+
flushPersistedSessions() {
|
|
1463
|
+
try {
|
|
1464
|
+
if (!(0, import_fs.existsSync)(SESSIX_DIR)) {
|
|
1465
|
+
(0, import_fs.mkdirSync)(SESSIX_DIR, { recursive: true });
|
|
1466
|
+
}
|
|
1467
|
+
const data = {};
|
|
1468
|
+
for (const [sessionId, meta] of this.persistedSessions) {
|
|
1469
|
+
data[sessionId] = meta;
|
|
1470
|
+
}
|
|
1471
|
+
(0, import_fs.writeFileSync)(CODEX_SESSIONS_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
1472
|
+
} catch (err) {
|
|
1473
|
+
console.error("[CodexProvider] Failed to persist sessions:", err);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* 检查某个 sessionId 是否为已知的 Codex 会话(供 SessionManager 查询 agentType)
|
|
1478
|
+
*/
|
|
1479
|
+
isKnownSession(sessionId) {
|
|
1480
|
+
return this.activeSessions.has(sessionId) || this.persistedSessions.has(sessionId);
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* 获取指定项目路径下的 Codex 持久化会话列表(供 project_sessions 合并)
|
|
1484
|
+
*/
|
|
1485
|
+
getPersistedSessionsForProject(projectPath) {
|
|
1486
|
+
const result = [];
|
|
1487
|
+
for (const [sessionId, meta] of this.persistedSessions) {
|
|
1488
|
+
if (meta.projectPath === projectPath && meta.threadId) {
|
|
1489
|
+
result.push({
|
|
1490
|
+
sessionId,
|
|
1491
|
+
lastModified: meta.lastActiveAt,
|
|
1492
|
+
summary: meta.summary
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
return result;
|
|
1497
|
+
}
|
|
1303
1498
|
};
|
|
1304
1499
|
|
|
1305
1500
|
// src/providers/ProviderFactory.ts
|
|
@@ -1392,8 +1587,15 @@ var SessionManager = class {
|
|
|
1392
1587
|
}
|
|
1393
1588
|
getProviderForSession(sessionId) {
|
|
1394
1589
|
if (!this.providerFactory) return this.provider;
|
|
1395
|
-
|
|
1396
|
-
|
|
1590
|
+
let agentType = this.sessionAgentType.get(sessionId);
|
|
1591
|
+
if (!agentType) {
|
|
1592
|
+
const codexProvider = this.providerFactory.getProvider("codex");
|
|
1593
|
+
if (codexProvider instanceof CodexProvider && codexProvider.isKnownSession(sessionId)) {
|
|
1594
|
+
agentType = "codex";
|
|
1595
|
+
this.sessionAgentType.set(sessionId, agentType);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
return this.providerFactory.getProvider(agentType ?? "claude-code");
|
|
1397
1599
|
}
|
|
1398
1600
|
// ============================================
|
|
1399
1601
|
// 公开 API
|
|
@@ -3507,11 +3709,11 @@ var ActivityPushChannel = class {
|
|
|
3507
3709
|
// src/session/ProjectReader.ts
|
|
3508
3710
|
var import_promises3 = require("fs/promises");
|
|
3509
3711
|
var import_readline3 = require("readline");
|
|
3510
|
-
var
|
|
3511
|
-
var
|
|
3512
|
-
var CLAUDE_PROJECTS_DIR = (0,
|
|
3712
|
+
var import_path2 = require("path");
|
|
3713
|
+
var import_os2 = require("os");
|
|
3714
|
+
var CLAUDE_PROJECTS_DIR = (0, import_path2.join)((0, import_os2.homedir)(), ".claude", "projects");
|
|
3513
3715
|
function getSessionFilePath(projectPath, sessionId) {
|
|
3514
|
-
return (0,
|
|
3716
|
+
return (0, import_path2.join)(CLAUDE_PROJECTS_DIR, encodeDirName(projectPath), `${sessionId}.jsonl`);
|
|
3515
3717
|
}
|
|
3516
3718
|
async function getProjects() {
|
|
3517
3719
|
try {
|
|
@@ -3528,7 +3730,7 @@ async function getProjects() {
|
|
|
3528
3730
|
const encodedPath = entry.name;
|
|
3529
3731
|
const decodedPath = decodeDirName(encodedPath);
|
|
3530
3732
|
const name = decodedPath.split("/").filter(Boolean).pop() ?? encodedPath;
|
|
3531
|
-
const projectDir = (0,
|
|
3733
|
+
const projectDir = (0, import_path2.join)(CLAUDE_PROJECTS_DIR, encodedPath);
|
|
3532
3734
|
const { count: sessionCount, latestMtime } = await countJsonlFilesWithMtime(projectDir);
|
|
3533
3735
|
projects.push({
|
|
3534
3736
|
id: encodedPath,
|
|
@@ -3550,7 +3752,7 @@ async function getProjects() {
|
|
|
3550
3752
|
async function getHistoricalSessions(projectPath) {
|
|
3551
3753
|
try {
|
|
3552
3754
|
const encodedPath = encodeDirName(projectPath);
|
|
3553
|
-
const projectDir = (0,
|
|
3755
|
+
const projectDir = (0, import_path2.join)(CLAUDE_PROJECTS_DIR, encodedPath);
|
|
3554
3756
|
const dirExists = await directoryExists(projectDir);
|
|
3555
3757
|
if (!dirExists) {
|
|
3556
3758
|
return { ok: true, value: [] };
|
|
@@ -3561,7 +3763,7 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3561
3763
|
await Promise.all(
|
|
3562
3764
|
jsonlFiles.map(async (entry) => {
|
|
3563
3765
|
const sessionId = entry.name.slice(0, -6);
|
|
3564
|
-
const filePath = (0,
|
|
3766
|
+
const filePath = (0, import_path2.join)(projectDir, entry.name);
|
|
3565
3767
|
try {
|
|
3566
3768
|
const contentTs = await extractLastTimestamp(filePath);
|
|
3567
3769
|
if (contentTs) {
|
|
@@ -3580,13 +3782,13 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3580
3782
|
);
|
|
3581
3783
|
for (const entry of uuidDirs) {
|
|
3582
3784
|
try {
|
|
3583
|
-
const fileStat = await (0, import_promises3.stat)((0,
|
|
3785
|
+
const fileStat = await (0, import_promises3.stat)((0, import_path2.join)(projectDir, entry.name));
|
|
3584
3786
|
mtimeMap.set(entry.name, fileStat.mtimeMs);
|
|
3585
3787
|
} catch {
|
|
3586
3788
|
mtimeMap.set(entry.name, 0);
|
|
3587
3789
|
}
|
|
3588
3790
|
}
|
|
3589
|
-
const indexPath = (0,
|
|
3791
|
+
const indexPath = (0, import_path2.join)(projectDir, "sessions-index.json");
|
|
3590
3792
|
const sessionMap = /* @__PURE__ */ new Map();
|
|
3591
3793
|
try {
|
|
3592
3794
|
const indexContent = await (0, import_promises3.readFile)(indexPath, "utf-8");
|
|
@@ -3604,7 +3806,7 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3604
3806
|
}
|
|
3605
3807
|
await Promise.all(
|
|
3606
3808
|
Array.from(sessionMap.values()).filter((s) => (s.messageCount ?? 0) > 0 && !s.summary && !s.firstPrompt).map(async (s) => {
|
|
3607
|
-
const filePath = (0,
|
|
3809
|
+
const filePath = (0, import_path2.join)(projectDir, `${s.sessionId}.jsonl`);
|
|
3608
3810
|
const firstPrompt = await extractFirstPrompt(filePath).catch(() => void 0);
|
|
3609
3811
|
if (firstPrompt) s.firstPrompt = firstPrompt;
|
|
3610
3812
|
})
|
|
@@ -3618,7 +3820,7 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3618
3820
|
if (uuidDirSet.has(sessionId)) {
|
|
3619
3821
|
sessionMap.set(sessionId, { sessionId, lastModified: mtime, messageCount: -1 });
|
|
3620
3822
|
} else {
|
|
3621
|
-
const filePath = (0,
|
|
3823
|
+
const filePath = (0, import_path2.join)(projectDir, `${sessionId}.jsonl`);
|
|
3622
3824
|
const firstPrompt = await extractFirstPrompt(filePath).catch(() => void 0);
|
|
3623
3825
|
sessionMap.set(sessionId, { sessionId, lastModified: mtime, firstPrompt });
|
|
3624
3826
|
}
|
|
@@ -3642,7 +3844,7 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3642
3844
|
async function getSessionHistory(projectPath, sessionId) {
|
|
3643
3845
|
try {
|
|
3644
3846
|
const encodedPath = encodeDirName(projectPath);
|
|
3645
|
-
const filePath = (0,
|
|
3847
|
+
const filePath = (0, import_path2.join)(CLAUDE_PROJECTS_DIR, encodedPath, `${sessionId}.jsonl`);
|
|
3646
3848
|
const raw = await (0, import_promises3.readFile)(filePath, "utf-8").catch((err) => {
|
|
3647
3849
|
if (err.code === "ENOENT") return null;
|
|
3648
3850
|
throw err;
|
|
@@ -3823,15 +4025,15 @@ async function countJsonlFilesWithMtime(dirPath) {
|
|
|
3823
4025
|
await Promise.all([
|
|
3824
4026
|
...jsonlEntries.map(async (entry) => {
|
|
3825
4027
|
try {
|
|
3826
|
-
const contentTs = await extractLastTimestamp((0,
|
|
3827
|
-
const ts = contentTs ?? (await (0, import_promises3.stat)((0,
|
|
4028
|
+
const contentTs = await extractLastTimestamp((0, import_path2.join)(dirPath, entry.name));
|
|
4029
|
+
const ts = contentTs ?? (await (0, import_promises3.stat)((0, import_path2.join)(dirPath, entry.name))).mtimeMs;
|
|
3828
4030
|
if (ts > latestMtime) latestMtime = ts;
|
|
3829
4031
|
} catch {
|
|
3830
4032
|
}
|
|
3831
4033
|
}),
|
|
3832
4034
|
...uuidDirs.map(async (entry) => {
|
|
3833
4035
|
try {
|
|
3834
|
-
const fileStat = await (0, import_promises3.stat)((0,
|
|
4036
|
+
const fileStat = await (0, import_promises3.stat)((0, import_path2.join)(dirPath, entry.name));
|
|
3835
4037
|
if (fileStat.mtimeMs > latestMtime) latestMtime = fileStat.mtimeMs;
|
|
3836
4038
|
} catch {
|
|
3837
4039
|
}
|
|
@@ -4389,10 +4591,27 @@ async function start(opts = {}) {
|
|
|
4389
4591
|
case "list_project_sessions": {
|
|
4390
4592
|
const histResult = await getHistoricalSessions(event.projectPath);
|
|
4391
4593
|
if (histResult.ok) {
|
|
4594
|
+
const sessions = histResult.value;
|
|
4595
|
+
const codexProvider = providerFactory.getProvider("codex");
|
|
4596
|
+
if (codexProvider instanceof CodexProvider) {
|
|
4597
|
+
const codexSessions = codexProvider.getPersistedSessionsForProject(event.projectPath);
|
|
4598
|
+
const existingIds = new Set(sessions.map((s) => s.sessionId));
|
|
4599
|
+
for (const cs of codexSessions) {
|
|
4600
|
+
if (!existingIds.has(cs.sessionId)) {
|
|
4601
|
+
sessions.push({
|
|
4602
|
+
sessionId: cs.sessionId,
|
|
4603
|
+
lastModified: cs.lastModified,
|
|
4604
|
+
summary: cs.summary,
|
|
4605
|
+
agentType: "codex"
|
|
4606
|
+
});
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4609
|
+
sessions.sort((a, b) => b.lastModified - a.lastModified);
|
|
4610
|
+
}
|
|
4392
4611
|
wsBridge.send(ws, {
|
|
4393
4612
|
type: "project_sessions",
|
|
4394
4613
|
projectPath: event.projectPath,
|
|
4395
|
-
sessions
|
|
4614
|
+
sessions
|
|
4396
4615
|
});
|
|
4397
4616
|
} else {
|
|
4398
4617
|
wsBridge.send(ws, {
|