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.
Files changed (3) hide show
  1. package/dist/index.js +251 -32
  2. package/dist/server.js +251 -32
  3. 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 output = (0, import_node_child_process3.execSync)(`"${CODEX_PATH}" --version`, {
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
- const proc = this.spawnCodexProcess(projectPath, message, void 0);
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: void 0,
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
- const entry = this.activeSessions.get(sessionId);
1023
+ let entry = this.activeSessions.get(sessionId);
982
1024
  if (!entry) {
983
- throw new Error(`Session ${sessionId} not found or already ended`);
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
- if (entry.process.exitCode === null && entry.process.signalCode === null) {
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
- throw new Error(`Session ${sessionId} has no thread ID, cannot resume`);
1059
+ console.warn(`[CodexProvider] Session ${sessionId} has no thread ID, falling back to new thread`);
995
1060
  }
996
- const proc = this.spawnCodexProcess(entry.session.projectPath, message, threadId);
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
- return Array.from(this.activeSessions.values()).map((e) => e.session);
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
- const proc = (0, import_child_process2.spawn)(CODEX_PATH, args, {
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
- const agentType = this.sessionAgentType.get(sessionId) ?? "claude-code";
1391
- return this.providerFactory.getProvider(agentType);
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 import_path = require("path");
3506
- var import_os = require("os");
3507
- var CLAUDE_PROJECTS_DIR = (0, import_path.join)((0, import_os.homedir)(), ".claude", "projects");
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, import_path.join)(CLAUDE_PROJECTS_DIR, encodeDirName(projectPath), `${sessionId}.jsonl`);
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, import_path.join)(CLAUDE_PROJECTS_DIR, encodedPath);
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, import_path.join)(CLAUDE_PROJECTS_DIR, encodedPath);
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, import_path.join)(projectDir, entry.name);
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, import_path.join)(projectDir, entry.name));
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, import_path.join)(projectDir, "sessions-index.json");
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, import_path.join)(projectDir, `${s.sessionId}.jsonl`);
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, import_path.join)(projectDir, `${sessionId}.jsonl`);
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, import_path.join)(CLAUDE_PROJECTS_DIR, encodedPath, `${sessionId}.jsonl`);
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, import_path.join)(dirPath, entry.name));
3822
- const ts = contentTs ?? (await (0, import_promises3.stat)((0, import_path.join)(dirPath, entry.name))).mtimeMs;
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, import_path.join)(dirPath, entry.name));
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: histResult.value
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 output = (0, import_node_child_process3.execSync)(`"${CODEX_PATH}" --version`, {
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
- const proc = this.spawnCodexProcess(projectPath, message, void 0);
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: void 0,
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
- const entry = this.activeSessions.get(sessionId);
1028
+ let entry = this.activeSessions.get(sessionId);
987
1029
  if (!entry) {
988
- throw new Error(`Session ${sessionId} not found or already ended`);
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
- if (entry.process.exitCode === null && entry.process.signalCode === null) {
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
- throw new Error(`Session ${sessionId} has no thread ID, cannot resume`);
1064
+ console.warn(`[CodexProvider] Session ${sessionId} has no thread ID, falling back to new thread`);
1000
1065
  }
1001
- const proc = this.spawnCodexProcess(entry.session.projectPath, message, threadId);
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
- return Array.from(this.activeSessions.values()).map((e) => e.session);
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
- const proc = (0, import_child_process2.spawn)(CODEX_PATH, args, {
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
- const agentType = this.sessionAgentType.get(sessionId) ?? "claude-code";
1396
- return this.providerFactory.getProvider(agentType);
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 import_path = require("path");
3511
- var import_os = require("os");
3512
- var CLAUDE_PROJECTS_DIR = (0, import_path.join)((0, import_os.homedir)(), ".claude", "projects");
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, import_path.join)(CLAUDE_PROJECTS_DIR, encodeDirName(projectPath), `${sessionId}.jsonl`);
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, import_path.join)(CLAUDE_PROJECTS_DIR, encodedPath);
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, import_path.join)(CLAUDE_PROJECTS_DIR, encodedPath);
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, import_path.join)(projectDir, entry.name);
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, import_path.join)(projectDir, entry.name));
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, import_path.join)(projectDir, "sessions-index.json");
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, import_path.join)(projectDir, `${s.sessionId}.jsonl`);
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, import_path.join)(projectDir, `${sessionId}.jsonl`);
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, import_path.join)(CLAUDE_PROJECTS_DIR, encodedPath, `${sessionId}.jsonl`);
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, import_path.join)(dirPath, entry.name));
3827
- const ts = contentTs ?? (await (0, import_promises3.stat)((0, import_path.join)(dirPath, entry.name))).mtimeMs;
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, import_path.join)(dirPath, entry.name));
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: histResult.value
4614
+ sessions
4396
4615
  });
4397
4616
  } else {
4398
4617
  wsBridge.send(ws, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sessix-server",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "bin": {
5
5
  "sessix-server": "./dist/index.js"
6
6
  },