sessix-server 0.3.3 → 0.3.5

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 +150 -47
  2. package/dist/server.js +150 -47
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -655,9 +655,33 @@ var ProcessProvider = class {
655
655
  const isQuestion = block.name === "AskUserQuestion" || block.name === "AskFollowupQuestion";
656
656
  if (!isQuestion) continue;
657
657
  const input = block.input;
658
- const question = input.question ?? "";
658
+ let question = "";
659
+ let options;
660
+ let questions;
661
+ if (typeof input.question === "string") {
662
+ question = input.question;
663
+ options = Array.isArray(input.options) ? input.options : void 0;
664
+ } else if (Array.isArray(input.questions) && input.questions.length > 0) {
665
+ questions = input.questions.map((q) => {
666
+ const item = {
667
+ question: typeof q.question === "string" ? q.question : "",
668
+ header: typeof q.header === "string" ? q.header : void 0,
669
+ multiSelect: typeof q.multiSelect === "boolean" ? q.multiSelect : void 0
670
+ };
671
+ if (Array.isArray(q.options)) {
672
+ item.options = q.options.map((o) => ({
673
+ label: typeof o.label === "string" ? o.label : String(o),
674
+ description: typeof o.description === "string" ? o.description : void 0
675
+ }));
676
+ }
677
+ return item;
678
+ });
679
+ const first = questions[0];
680
+ question = first.question;
681
+ options = first.options?.map((o) => o.label);
682
+ }
659
683
  if (!question) continue;
660
- const prevKey = `${block.id}:${question}:${JSON.stringify(input.options ?? [])}`;
684
+ const prevKey = `${block.id}:${question}:${JSON.stringify(options ?? [])}`;
661
685
  let sessionSet = this.emittedQuestionToolUseIds.get(sessionId);
662
686
  if (!sessionSet) {
663
687
  sessionSet = /* @__PURE__ */ new Set();
@@ -669,7 +693,8 @@ var ProcessProvider = class {
669
693
  this.emitter.emit(this.getQuestionEventName(sessionId), {
670
694
  toolUseId: block.id,
671
695
  question,
672
- options: input.options
696
+ options,
697
+ questions
673
698
  });
674
699
  }
675
700
  }
@@ -944,11 +969,15 @@ function isCodexAvailable() {
944
969
  // src/providers/CodexProvider.ts
945
970
  var SESSIX_DIR = (0, import_path.join)((0, import_os.homedir)(), ".sessix");
946
971
  var CODEX_SESSIONS_FILE = (0, import_path.join)(SESSIX_DIR, "codex-sessions.json");
972
+ var CODEX_EVENTS_DIR = (0, import_path.join)(SESSIX_DIR, "codex-events");
973
+ var SESSION_EXPIRY_MS = 30 * 24 * 60 * 60 * 1e3;
947
974
  var CodexProvider = class {
948
975
  activeSessions = /* @__PURE__ */ new Map();
949
976
  emitter = new import_events2.EventEmitter();
950
977
  /** 持久化的会话元数据(sessionId → metadata) */
951
978
  persistedSessions = /* @__PURE__ */ new Map();
979
+ /** 会话事件缓存(sessionId → events),用于历史回放 */
980
+ sessionEvents = /* @__PURE__ */ new Map();
952
981
  /** 自增 ID 计数器,用于生成 ClaudeStreamEvent 中的 message/block ID */
953
982
  idCounter = 0;
954
983
  constructor() {
@@ -995,7 +1024,7 @@ var CodexProvider = class {
995
1024
  subtype: "init",
996
1025
  session_id: sessionId
997
1026
  };
998
- this.emitter.emit(this.getEventName(sessionId), initEvent);
1027
+ this.emitAndRecord(sessionId, initEvent);
999
1028
  this.emitUserMessage(sessionId, message);
1000
1029
  proc.on("error", (err) => {
1001
1030
  console.error(`[CodexProvider] Session ${sessionId} process error:`, err.message);
@@ -1082,26 +1111,7 @@ var CodexProvider = class {
1082
1111
  };
1083
1112
  }
1084
1113
  getActiveSessions() {
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());
1114
+ return Array.from(this.activeSessions.values()).map((entry) => entry.session);
1105
1115
  }
1106
1116
  async generateSuggestion(_context) {
1107
1117
  return "";
@@ -1225,7 +1235,7 @@ var CodexProvider = class {
1225
1235
  num_turns: 1,
1226
1236
  usage: event.usage
1227
1237
  };
1228
- this.emitter.emit(this.getEventName(sessionId), resultEvent);
1238
+ this.emitAndRecord(sessionId, resultEvent);
1229
1239
  if (entry.threadId) {
1230
1240
  this.persistSession(sessionId, {
1231
1241
  threadId: entry.threadId,
@@ -1235,6 +1245,7 @@ var CodexProvider = class {
1235
1245
  lastActiveAt: Date.now()
1236
1246
  });
1237
1247
  }
1248
+ this.persistSessionEvents(sessionId);
1238
1249
  break;
1239
1250
  }
1240
1251
  case "turn.failed": {
@@ -1248,7 +1259,8 @@ var CodexProvider = class {
1248
1259
  duration_ms: entry.turnStartTime ? Date.now() - entry.turnStartTime : 0,
1249
1260
  num_turns: 1
1250
1261
  };
1251
- this.emitter.emit(this.getEventName(sessionId), failEvent);
1262
+ this.emitAndRecord(sessionId, failEvent);
1263
+ this.persistSessionEvents(sessionId);
1252
1264
  break;
1253
1265
  }
1254
1266
  }
@@ -1269,7 +1281,7 @@ var CodexProvider = class {
1269
1281
  content: [{ type: "text", text: item.text ?? "" }]
1270
1282
  }
1271
1283
  };
1272
- this.emitter.emit(this.getEventName(sessionId), msgEvent);
1284
+ this.emitAndRecord(sessionId, msgEvent);
1273
1285
  break;
1274
1286
  }
1275
1287
  case "command_execution": {
@@ -1289,7 +1301,7 @@ var CodexProvider = class {
1289
1301
  }]
1290
1302
  }
1291
1303
  };
1292
- this.emitter.emit(this.getEventName(sessionId), toolEvent);
1304
+ this.emitAndRecord(sessionId, toolEvent);
1293
1305
  const resultContent = item.aggregated_output ?? "";
1294
1306
  const isError = item.exit_code != null && item.exit_code !== 0;
1295
1307
  const toolResultEvent = {
@@ -1305,7 +1317,7 @@ var CodexProvider = class {
1305
1317
  }]
1306
1318
  }
1307
1319
  };
1308
- this.emitter.emit(this.getEventName(sessionId), toolResultEvent);
1320
+ this.emitAndRecord(sessionId, toolResultEvent);
1309
1321
  break;
1310
1322
  }
1311
1323
  case "file_change": {
@@ -1325,7 +1337,7 @@ var CodexProvider = class {
1325
1337
  }]
1326
1338
  }
1327
1339
  };
1328
- this.emitter.emit(this.getEventName(sessionId), toolEvent);
1340
+ this.emitAndRecord(sessionId, toolEvent);
1329
1341
  const toolResultEvent = {
1330
1342
  type: "user",
1331
1343
  session_id: sessionId,
@@ -1338,7 +1350,7 @@ var CodexProvider = class {
1338
1350
  }]
1339
1351
  }
1340
1352
  };
1341
- this.emitter.emit(this.getEventName(sessionId), toolResultEvent);
1353
+ this.emitAndRecord(sessionId, toolResultEvent);
1342
1354
  break;
1343
1355
  }
1344
1356
  case "reasoning": {
@@ -1352,7 +1364,7 @@ var CodexProvider = class {
1352
1364
  content: [{ type: "thinking", thinking: item.text ?? "" }]
1353
1365
  }
1354
1366
  };
1355
- this.emitter.emit(this.getEventName(sessionId), thinkEvent);
1367
+ this.emitAndRecord(sessionId, thinkEvent);
1356
1368
  break;
1357
1369
  }
1358
1370
  }
@@ -1394,7 +1406,7 @@ var CodexProvider = class {
1394
1406
  duration_ms: 0,
1395
1407
  num_turns: 0
1396
1408
  };
1397
- this.emitter.emit(this.getEventName(sessionId), syntheticResult);
1409
+ this.emitAndRecord(sessionId, syntheticResult);
1398
1410
  });
1399
1411
  }
1400
1412
  /**
@@ -1409,7 +1421,7 @@ var CodexProvider = class {
1409
1421
  content: [{ type: "text", text: message }]
1410
1422
  }
1411
1423
  };
1412
- this.emitter.emit(this.getEventName(sessionId), event);
1424
+ this.emitAndRecord(sessionId, event);
1413
1425
  }
1414
1426
  emitError(sessionId, message) {
1415
1427
  const event = {
@@ -1421,11 +1433,59 @@ var CodexProvider = class {
1421
1433
  is_error: true,
1422
1434
  num_turns: 0
1423
1435
  };
1424
- this.emitter.emit(this.getEventName(sessionId), event);
1436
+ this.emitAndRecord(sessionId, event);
1425
1437
  }
1426
1438
  getEventName(sessionId) {
1427
1439
  return `claude:${sessionId}`;
1428
1440
  }
1441
+ /**
1442
+ * 发射事件并同时记录到 sessionEvents 缓存
1443
+ */
1444
+ emitAndRecord(sessionId, event) {
1445
+ this.emitter.emit(this.getEventName(sessionId), event);
1446
+ let events = this.sessionEvents.get(sessionId);
1447
+ if (!events) {
1448
+ events = [];
1449
+ this.sessionEvents.set(sessionId, events);
1450
+ }
1451
+ events.push(event);
1452
+ }
1453
+ /**
1454
+ * 获取 Codex 会话的历史事件(供 load_session_history 调用)
1455
+ * 优先从内存读,miss 时从磁盘加载
1456
+ */
1457
+ getSessionHistory(sessionId) {
1458
+ const cached = this.sessionEvents.get(sessionId);
1459
+ if (cached && cached.length > 0) return cached;
1460
+ const filePath = (0, import_path.join)(CODEX_EVENTS_DIR, `${sessionId}.json`);
1461
+ try {
1462
+ if (!(0, import_fs.existsSync)(filePath)) return [];
1463
+ const data = JSON.parse((0, import_fs.readFileSync)(filePath, "utf-8"));
1464
+ this.sessionEvents.set(sessionId, data);
1465
+ return data;
1466
+ } catch {
1467
+ return [];
1468
+ }
1469
+ }
1470
+ /**
1471
+ * 将会话事件持久化到磁盘
1472
+ */
1473
+ persistSessionEvents(sessionId) {
1474
+ const events = this.sessionEvents.get(sessionId);
1475
+ if (!events || events.length === 0) return;
1476
+ try {
1477
+ if (!(0, import_fs.existsSync)(CODEX_EVENTS_DIR)) {
1478
+ (0, import_fs.mkdirSync)(CODEX_EVENTS_DIR, { recursive: true });
1479
+ }
1480
+ (0, import_fs.writeFileSync)(
1481
+ (0, import_path.join)(CODEX_EVENTS_DIR, `${sessionId}.json`),
1482
+ JSON.stringify(events),
1483
+ "utf-8"
1484
+ );
1485
+ } catch (err) {
1486
+ console.error(`[CodexProvider] Failed to persist events for ${sessionId}:`, err);
1487
+ }
1488
+ }
1429
1489
  // ============================================
1430
1490
  // 持久化方法
1431
1491
  // ============================================
@@ -1436,8 +1496,24 @@ var CodexProvider = class {
1436
1496
  try {
1437
1497
  if (!(0, import_fs.existsSync)(CODEX_SESSIONS_FILE)) return;
1438
1498
  const data = JSON.parse((0, import_fs.readFileSync)(CODEX_SESSIONS_FILE, "utf-8"));
1499
+ const now = Date.now();
1500
+ let expiredCount = 0;
1439
1501
  for (const [sessionId, meta] of Object.entries(data)) {
1440
- this.persistedSessions.set(sessionId, meta);
1502
+ const m = meta;
1503
+ if (now - m.lastActiveAt > SESSION_EXPIRY_MS) {
1504
+ expiredCount++;
1505
+ try {
1506
+ const eventsFile = (0, import_path.join)(CODEX_EVENTS_DIR, `${sessionId}.json`);
1507
+ if ((0, import_fs.existsSync)(eventsFile)) (0, import_fs.unlinkSync)(eventsFile);
1508
+ } catch {
1509
+ }
1510
+ continue;
1511
+ }
1512
+ this.persistedSessions.set(sessionId, m);
1513
+ }
1514
+ if (expiredCount > 0) {
1515
+ console.log(`[CodexProvider] Cleaned up ${expiredCount} expired sessions`);
1516
+ this.flushPersistedSessions();
1441
1517
  }
1442
1518
  console.log(`[CodexProvider] Loaded ${this.persistedSessions.size} persisted sessions`);
1443
1519
  } catch (err) {
@@ -1685,10 +1761,13 @@ var SessionManager = class {
1685
1761
  console.warn(`[SessionManager] Question request not found: ${requestId}`);
1686
1762
  return;
1687
1763
  }
1764
+ const { sessionId } = pending;
1688
1765
  this.pendingQuestions.delete(requestId);
1689
- this.updateSessionStatus(pending.sessionId, "running");
1690
1766
  pending.resolve(answer);
1691
1767
  console.log(`[SessionManager] Question answered: ${requestId}`);
1768
+ if (!this.hasPendingQuestionsForSession(sessionId)) {
1769
+ this.updateSessionStatus(sessionId, "running");
1770
+ }
1692
1771
  }
1693
1772
  /**
1694
1773
  * 获取指定会话的所有待回答问题(用于重连时恢复)
@@ -1703,6 +1782,7 @@ var SessionManager = class {
1703
1782
  toolUseId: pending.toolUseId,
1704
1783
  question: pending.question,
1705
1784
  options: pending.options,
1785
+ questions: pending.questions,
1706
1786
  createdAt: pending.createdAt
1707
1787
  });
1708
1788
  }
@@ -1713,6 +1793,13 @@ var SessionManager = class {
1713
1793
  isQuestionPending(requestId) {
1714
1794
  return this.pendingQuestions.has(requestId);
1715
1795
  }
1796
+ /** 检查指定会话是否有待回答问题 */
1797
+ hasPendingQuestionsForSession(sessionId) {
1798
+ for (const pending of this.pendingQuestions.values()) {
1799
+ if (pending.sessionId === sessionId) return true;
1800
+ }
1801
+ return false;
1802
+ }
1716
1803
  /**
1717
1804
  * 获取所有待回答问题(用于客户端重连时恢复状态)
1718
1805
  */
@@ -1725,6 +1812,7 @@ var SessionManager = class {
1725
1812
  toolUseId: pending.toolUseId,
1726
1813
  question: pending.question,
1727
1814
  options: pending.options,
1815
+ questions: pending.questions,
1728
1816
  createdAt: pending.createdAt
1729
1817
  });
1730
1818
  }
@@ -1792,8 +1880,8 @@ var SessionManager = class {
1792
1880
  });
1793
1881
  const unsubscribeQuestion = provider.onQuestion(
1794
1882
  sessionId,
1795
- ({ toolUseId, question, options }) => {
1796
- this.handleAskUserQuestion(sessionId, toolUseId, question, options);
1883
+ ({ toolUseId, question, options, questions }) => {
1884
+ this.handleAskUserQuestion(sessionId, toolUseId, question, options, questions);
1797
1885
  }
1798
1886
  );
1799
1887
  this.unsubscribeMap.set(sessionId, () => {
@@ -1863,7 +1951,9 @@ var SessionManager = class {
1863
1951
  stats.totalCostUsd = (stats.totalCostUsd ?? 0) + event.total_cost_usd;
1864
1952
  }
1865
1953
  this.sessionStats.set(sessionId, stats);
1866
- if (event.is_error) {
1954
+ if (this.hasPendingQuestionsForSession(sessionId)) {
1955
+ console.log(`[SessionManager] Session ${sessionId}: result received but questions pending, keeping waiting_question`);
1956
+ } else if (event.is_error) {
1867
1957
  this.updateSessionStatus(sessionId, "error");
1868
1958
  } else {
1869
1959
  this.updateSessionStatus(sessionId, "idle");
@@ -1947,19 +2037,24 @@ var SessionManager = class {
1947
2037
  /**
1948
2038
  * 处理 AskUserQuestion 事件:广播问题请求到手机,等待用户回答
1949
2039
  */
1950
- handleAskUserQuestion(sessionId, toolUseId, question, options) {
2040
+ handleAskUserQuestion(sessionId, toolUseId, question, options, questions) {
1951
2041
  const existingEntry = Array.from(this.pendingQuestions.entries()).find(
1952
2042
  ([, v]) => v.toolUseId === toolUseId
1953
2043
  );
1954
2044
  if (existingEntry) {
1955
- const [existingRequestId] = existingEntry;
2045
+ const [existingRequestId, existingPending] = existingEntry;
2046
+ existingPending.question = question;
2047
+ existingPending.options = options;
2048
+ existingPending.questions = questions;
2049
+ existingPending.createdAt = Date.now();
1956
2050
  const updatedRequest = {
1957
2051
  id: existingRequestId,
1958
2052
  sessionId,
1959
2053
  toolUseId,
1960
2054
  question,
1961
2055
  options,
1962
- createdAt: Date.now()
2056
+ questions,
2057
+ createdAt: existingPending.createdAt
1963
2058
  };
1964
2059
  this.emit({ type: "question_request", request: updatedRequest });
1965
2060
  console.log(`[SessionManager] Session ${sessionId}: AskUserQuestion updated (requestId=${existingRequestId})`);
@@ -1972,12 +2067,13 @@ var SessionManager = class {
1972
2067
  toolUseId,
1973
2068
  question,
1974
2069
  options,
2070
+ questions,
1975
2071
  createdAt: Date.now()
1976
2072
  };
1977
2073
  this.updateSessionStatus(sessionId, "waiting_question");
1978
2074
  this.emit({ type: "question_request", request });
1979
2075
  const answerPromise = new Promise((resolve) => {
1980
- this.pendingQuestions.set(requestId, { sessionId, toolUseId, question, options, createdAt: request.createdAt, resolve });
2076
+ this.pendingQuestions.set(requestId, { sessionId, toolUseId, question, options, questions, createdAt: request.createdAt, resolve });
1981
2077
  });
1982
2078
  answerPromise.then(async (answer) => {
1983
2079
  try {
@@ -4626,18 +4722,25 @@ async function start(opts = {}) {
4626
4722
  }
4627
4723
  case "load_session_history": {
4628
4724
  const historyResult = await getSessionHistory(event.projectPath, event.sessionId);
4629
- if (!historyResult.ok) {
4725
+ let historyEvents = historyResult.ok ? historyResult.value : [];
4726
+ if (historyEvents.length === 0) {
4727
+ const codexProv = providerFactory.getProvider("codex");
4728
+ if (codexProv instanceof CodexProvider && codexProv.isKnownSession(event.sessionId)) {
4729
+ historyEvents = codexProv.getSessionHistory(event.sessionId);
4730
+ }
4731
+ }
4732
+ if (!historyResult.ok && historyEvents.length === 0) {
4630
4733
  wsBridge.send(ws, {
4631
4734
  type: "error",
4632
4735
  message: t("server.readHistoryFailed", { error: historyResult.error.message }),
4633
4736
  code: "SESSION_HISTORY_ERROR",
4634
4737
  sessionId: event.sessionId
4635
4738
  });
4636
- } else if (historyResult.value.length > 0) {
4739
+ } else if (historyEvents.length > 0) {
4637
4740
  wsBridge.send(ws, {
4638
4741
  type: "session_history",
4639
4742
  sessionId: event.sessionId,
4640
- events: historyResult.value
4743
+ events: historyEvents
4641
4744
  });
4642
4745
  const activeSession = sessionManager.getActiveSessions().find((s) => s.id === event.sessionId);
4643
4746
  const isStreaming = activeSession?.status === "running" || activeSession?.status === "waiting_approval";
package/dist/server.js CHANGED
@@ -660,9 +660,33 @@ var ProcessProvider = class {
660
660
  const isQuestion = block.name === "AskUserQuestion" || block.name === "AskFollowupQuestion";
661
661
  if (!isQuestion) continue;
662
662
  const input = block.input;
663
- const question = input.question ?? "";
663
+ let question = "";
664
+ let options;
665
+ let questions;
666
+ if (typeof input.question === "string") {
667
+ question = input.question;
668
+ options = Array.isArray(input.options) ? input.options : void 0;
669
+ } else if (Array.isArray(input.questions) && input.questions.length > 0) {
670
+ questions = input.questions.map((q) => {
671
+ const item = {
672
+ question: typeof q.question === "string" ? q.question : "",
673
+ header: typeof q.header === "string" ? q.header : void 0,
674
+ multiSelect: typeof q.multiSelect === "boolean" ? q.multiSelect : void 0
675
+ };
676
+ if (Array.isArray(q.options)) {
677
+ item.options = q.options.map((o) => ({
678
+ label: typeof o.label === "string" ? o.label : String(o),
679
+ description: typeof o.description === "string" ? o.description : void 0
680
+ }));
681
+ }
682
+ return item;
683
+ });
684
+ const first = questions[0];
685
+ question = first.question;
686
+ options = first.options?.map((o) => o.label);
687
+ }
664
688
  if (!question) continue;
665
- const prevKey = `${block.id}:${question}:${JSON.stringify(input.options ?? [])}`;
689
+ const prevKey = `${block.id}:${question}:${JSON.stringify(options ?? [])}`;
666
690
  let sessionSet = this.emittedQuestionToolUseIds.get(sessionId);
667
691
  if (!sessionSet) {
668
692
  sessionSet = /* @__PURE__ */ new Set();
@@ -674,7 +698,8 @@ var ProcessProvider = class {
674
698
  this.emitter.emit(this.getQuestionEventName(sessionId), {
675
699
  toolUseId: block.id,
676
700
  question,
677
- options: input.options
701
+ options,
702
+ questions
678
703
  });
679
704
  }
680
705
  }
@@ -949,11 +974,15 @@ function isCodexAvailable() {
949
974
  // src/providers/CodexProvider.ts
950
975
  var SESSIX_DIR = (0, import_path.join)((0, import_os.homedir)(), ".sessix");
951
976
  var CODEX_SESSIONS_FILE = (0, import_path.join)(SESSIX_DIR, "codex-sessions.json");
977
+ var CODEX_EVENTS_DIR = (0, import_path.join)(SESSIX_DIR, "codex-events");
978
+ var SESSION_EXPIRY_MS = 30 * 24 * 60 * 60 * 1e3;
952
979
  var CodexProvider = class {
953
980
  activeSessions = /* @__PURE__ */ new Map();
954
981
  emitter = new import_events2.EventEmitter();
955
982
  /** 持久化的会话元数据(sessionId → metadata) */
956
983
  persistedSessions = /* @__PURE__ */ new Map();
984
+ /** 会话事件缓存(sessionId → events),用于历史回放 */
985
+ sessionEvents = /* @__PURE__ */ new Map();
957
986
  /** 自增 ID 计数器,用于生成 ClaudeStreamEvent 中的 message/block ID */
958
987
  idCounter = 0;
959
988
  constructor() {
@@ -1000,7 +1029,7 @@ var CodexProvider = class {
1000
1029
  subtype: "init",
1001
1030
  session_id: sessionId
1002
1031
  };
1003
- this.emitter.emit(this.getEventName(sessionId), initEvent);
1032
+ this.emitAndRecord(sessionId, initEvent);
1004
1033
  this.emitUserMessage(sessionId, message);
1005
1034
  proc.on("error", (err) => {
1006
1035
  console.error(`[CodexProvider] Session ${sessionId} process error:`, err.message);
@@ -1087,26 +1116,7 @@ var CodexProvider = class {
1087
1116
  };
1088
1117
  }
1089
1118
  getActiveSessions() {
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());
1119
+ return Array.from(this.activeSessions.values()).map((entry) => entry.session);
1110
1120
  }
1111
1121
  async generateSuggestion(_context) {
1112
1122
  return "";
@@ -1230,7 +1240,7 @@ var CodexProvider = class {
1230
1240
  num_turns: 1,
1231
1241
  usage: event.usage
1232
1242
  };
1233
- this.emitter.emit(this.getEventName(sessionId), resultEvent);
1243
+ this.emitAndRecord(sessionId, resultEvent);
1234
1244
  if (entry.threadId) {
1235
1245
  this.persistSession(sessionId, {
1236
1246
  threadId: entry.threadId,
@@ -1240,6 +1250,7 @@ var CodexProvider = class {
1240
1250
  lastActiveAt: Date.now()
1241
1251
  });
1242
1252
  }
1253
+ this.persistSessionEvents(sessionId);
1243
1254
  break;
1244
1255
  }
1245
1256
  case "turn.failed": {
@@ -1253,7 +1264,8 @@ var CodexProvider = class {
1253
1264
  duration_ms: entry.turnStartTime ? Date.now() - entry.turnStartTime : 0,
1254
1265
  num_turns: 1
1255
1266
  };
1256
- this.emitter.emit(this.getEventName(sessionId), failEvent);
1267
+ this.emitAndRecord(sessionId, failEvent);
1268
+ this.persistSessionEvents(sessionId);
1257
1269
  break;
1258
1270
  }
1259
1271
  }
@@ -1274,7 +1286,7 @@ var CodexProvider = class {
1274
1286
  content: [{ type: "text", text: item.text ?? "" }]
1275
1287
  }
1276
1288
  };
1277
- this.emitter.emit(this.getEventName(sessionId), msgEvent);
1289
+ this.emitAndRecord(sessionId, msgEvent);
1278
1290
  break;
1279
1291
  }
1280
1292
  case "command_execution": {
@@ -1294,7 +1306,7 @@ var CodexProvider = class {
1294
1306
  }]
1295
1307
  }
1296
1308
  };
1297
- this.emitter.emit(this.getEventName(sessionId), toolEvent);
1309
+ this.emitAndRecord(sessionId, toolEvent);
1298
1310
  const resultContent = item.aggregated_output ?? "";
1299
1311
  const isError = item.exit_code != null && item.exit_code !== 0;
1300
1312
  const toolResultEvent = {
@@ -1310,7 +1322,7 @@ var CodexProvider = class {
1310
1322
  }]
1311
1323
  }
1312
1324
  };
1313
- this.emitter.emit(this.getEventName(sessionId), toolResultEvent);
1325
+ this.emitAndRecord(sessionId, toolResultEvent);
1314
1326
  break;
1315
1327
  }
1316
1328
  case "file_change": {
@@ -1330,7 +1342,7 @@ var CodexProvider = class {
1330
1342
  }]
1331
1343
  }
1332
1344
  };
1333
- this.emitter.emit(this.getEventName(sessionId), toolEvent);
1345
+ this.emitAndRecord(sessionId, toolEvent);
1334
1346
  const toolResultEvent = {
1335
1347
  type: "user",
1336
1348
  session_id: sessionId,
@@ -1343,7 +1355,7 @@ var CodexProvider = class {
1343
1355
  }]
1344
1356
  }
1345
1357
  };
1346
- this.emitter.emit(this.getEventName(sessionId), toolResultEvent);
1358
+ this.emitAndRecord(sessionId, toolResultEvent);
1347
1359
  break;
1348
1360
  }
1349
1361
  case "reasoning": {
@@ -1357,7 +1369,7 @@ var CodexProvider = class {
1357
1369
  content: [{ type: "thinking", thinking: item.text ?? "" }]
1358
1370
  }
1359
1371
  };
1360
- this.emitter.emit(this.getEventName(sessionId), thinkEvent);
1372
+ this.emitAndRecord(sessionId, thinkEvent);
1361
1373
  break;
1362
1374
  }
1363
1375
  }
@@ -1399,7 +1411,7 @@ var CodexProvider = class {
1399
1411
  duration_ms: 0,
1400
1412
  num_turns: 0
1401
1413
  };
1402
- this.emitter.emit(this.getEventName(sessionId), syntheticResult);
1414
+ this.emitAndRecord(sessionId, syntheticResult);
1403
1415
  });
1404
1416
  }
1405
1417
  /**
@@ -1414,7 +1426,7 @@ var CodexProvider = class {
1414
1426
  content: [{ type: "text", text: message }]
1415
1427
  }
1416
1428
  };
1417
- this.emitter.emit(this.getEventName(sessionId), event);
1429
+ this.emitAndRecord(sessionId, event);
1418
1430
  }
1419
1431
  emitError(sessionId, message) {
1420
1432
  const event = {
@@ -1426,11 +1438,59 @@ var CodexProvider = class {
1426
1438
  is_error: true,
1427
1439
  num_turns: 0
1428
1440
  };
1429
- this.emitter.emit(this.getEventName(sessionId), event);
1441
+ this.emitAndRecord(sessionId, event);
1430
1442
  }
1431
1443
  getEventName(sessionId) {
1432
1444
  return `claude:${sessionId}`;
1433
1445
  }
1446
+ /**
1447
+ * 发射事件并同时记录到 sessionEvents 缓存
1448
+ */
1449
+ emitAndRecord(sessionId, event) {
1450
+ this.emitter.emit(this.getEventName(sessionId), event);
1451
+ let events = this.sessionEvents.get(sessionId);
1452
+ if (!events) {
1453
+ events = [];
1454
+ this.sessionEvents.set(sessionId, events);
1455
+ }
1456
+ events.push(event);
1457
+ }
1458
+ /**
1459
+ * 获取 Codex 会话的历史事件(供 load_session_history 调用)
1460
+ * 优先从内存读,miss 时从磁盘加载
1461
+ */
1462
+ getSessionHistory(sessionId) {
1463
+ const cached = this.sessionEvents.get(sessionId);
1464
+ if (cached && cached.length > 0) return cached;
1465
+ const filePath = (0, import_path.join)(CODEX_EVENTS_DIR, `${sessionId}.json`);
1466
+ try {
1467
+ if (!(0, import_fs.existsSync)(filePath)) return [];
1468
+ const data = JSON.parse((0, import_fs.readFileSync)(filePath, "utf-8"));
1469
+ this.sessionEvents.set(sessionId, data);
1470
+ return data;
1471
+ } catch {
1472
+ return [];
1473
+ }
1474
+ }
1475
+ /**
1476
+ * 将会话事件持久化到磁盘
1477
+ */
1478
+ persistSessionEvents(sessionId) {
1479
+ const events = this.sessionEvents.get(sessionId);
1480
+ if (!events || events.length === 0) return;
1481
+ try {
1482
+ if (!(0, import_fs.existsSync)(CODEX_EVENTS_DIR)) {
1483
+ (0, import_fs.mkdirSync)(CODEX_EVENTS_DIR, { recursive: true });
1484
+ }
1485
+ (0, import_fs.writeFileSync)(
1486
+ (0, import_path.join)(CODEX_EVENTS_DIR, `${sessionId}.json`),
1487
+ JSON.stringify(events),
1488
+ "utf-8"
1489
+ );
1490
+ } catch (err) {
1491
+ console.error(`[CodexProvider] Failed to persist events for ${sessionId}:`, err);
1492
+ }
1493
+ }
1434
1494
  // ============================================
1435
1495
  // 持久化方法
1436
1496
  // ============================================
@@ -1441,8 +1501,24 @@ var CodexProvider = class {
1441
1501
  try {
1442
1502
  if (!(0, import_fs.existsSync)(CODEX_SESSIONS_FILE)) return;
1443
1503
  const data = JSON.parse((0, import_fs.readFileSync)(CODEX_SESSIONS_FILE, "utf-8"));
1504
+ const now = Date.now();
1505
+ let expiredCount = 0;
1444
1506
  for (const [sessionId, meta] of Object.entries(data)) {
1445
- this.persistedSessions.set(sessionId, meta);
1507
+ const m = meta;
1508
+ if (now - m.lastActiveAt > SESSION_EXPIRY_MS) {
1509
+ expiredCount++;
1510
+ try {
1511
+ const eventsFile = (0, import_path.join)(CODEX_EVENTS_DIR, `${sessionId}.json`);
1512
+ if ((0, import_fs.existsSync)(eventsFile)) (0, import_fs.unlinkSync)(eventsFile);
1513
+ } catch {
1514
+ }
1515
+ continue;
1516
+ }
1517
+ this.persistedSessions.set(sessionId, m);
1518
+ }
1519
+ if (expiredCount > 0) {
1520
+ console.log(`[CodexProvider] Cleaned up ${expiredCount} expired sessions`);
1521
+ this.flushPersistedSessions();
1446
1522
  }
1447
1523
  console.log(`[CodexProvider] Loaded ${this.persistedSessions.size} persisted sessions`);
1448
1524
  } catch (err) {
@@ -1690,10 +1766,13 @@ var SessionManager = class {
1690
1766
  console.warn(`[SessionManager] Question request not found: ${requestId}`);
1691
1767
  return;
1692
1768
  }
1769
+ const { sessionId } = pending;
1693
1770
  this.pendingQuestions.delete(requestId);
1694
- this.updateSessionStatus(pending.sessionId, "running");
1695
1771
  pending.resolve(answer);
1696
1772
  console.log(`[SessionManager] Question answered: ${requestId}`);
1773
+ if (!this.hasPendingQuestionsForSession(sessionId)) {
1774
+ this.updateSessionStatus(sessionId, "running");
1775
+ }
1697
1776
  }
1698
1777
  /**
1699
1778
  * 获取指定会话的所有待回答问题(用于重连时恢复)
@@ -1708,6 +1787,7 @@ var SessionManager = class {
1708
1787
  toolUseId: pending.toolUseId,
1709
1788
  question: pending.question,
1710
1789
  options: pending.options,
1790
+ questions: pending.questions,
1711
1791
  createdAt: pending.createdAt
1712
1792
  });
1713
1793
  }
@@ -1718,6 +1798,13 @@ var SessionManager = class {
1718
1798
  isQuestionPending(requestId) {
1719
1799
  return this.pendingQuestions.has(requestId);
1720
1800
  }
1801
+ /** 检查指定会话是否有待回答问题 */
1802
+ hasPendingQuestionsForSession(sessionId) {
1803
+ for (const pending of this.pendingQuestions.values()) {
1804
+ if (pending.sessionId === sessionId) return true;
1805
+ }
1806
+ return false;
1807
+ }
1721
1808
  /**
1722
1809
  * 获取所有待回答问题(用于客户端重连时恢复状态)
1723
1810
  */
@@ -1730,6 +1817,7 @@ var SessionManager = class {
1730
1817
  toolUseId: pending.toolUseId,
1731
1818
  question: pending.question,
1732
1819
  options: pending.options,
1820
+ questions: pending.questions,
1733
1821
  createdAt: pending.createdAt
1734
1822
  });
1735
1823
  }
@@ -1797,8 +1885,8 @@ var SessionManager = class {
1797
1885
  });
1798
1886
  const unsubscribeQuestion = provider.onQuestion(
1799
1887
  sessionId,
1800
- ({ toolUseId, question, options }) => {
1801
- this.handleAskUserQuestion(sessionId, toolUseId, question, options);
1888
+ ({ toolUseId, question, options, questions }) => {
1889
+ this.handleAskUserQuestion(sessionId, toolUseId, question, options, questions);
1802
1890
  }
1803
1891
  );
1804
1892
  this.unsubscribeMap.set(sessionId, () => {
@@ -1868,7 +1956,9 @@ var SessionManager = class {
1868
1956
  stats.totalCostUsd = (stats.totalCostUsd ?? 0) + event.total_cost_usd;
1869
1957
  }
1870
1958
  this.sessionStats.set(sessionId, stats);
1871
- if (event.is_error) {
1959
+ if (this.hasPendingQuestionsForSession(sessionId)) {
1960
+ console.log(`[SessionManager] Session ${sessionId}: result received but questions pending, keeping waiting_question`);
1961
+ } else if (event.is_error) {
1872
1962
  this.updateSessionStatus(sessionId, "error");
1873
1963
  } else {
1874
1964
  this.updateSessionStatus(sessionId, "idle");
@@ -1952,19 +2042,24 @@ var SessionManager = class {
1952
2042
  /**
1953
2043
  * 处理 AskUserQuestion 事件:广播问题请求到手机,等待用户回答
1954
2044
  */
1955
- handleAskUserQuestion(sessionId, toolUseId, question, options) {
2045
+ handleAskUserQuestion(sessionId, toolUseId, question, options, questions) {
1956
2046
  const existingEntry = Array.from(this.pendingQuestions.entries()).find(
1957
2047
  ([, v]) => v.toolUseId === toolUseId
1958
2048
  );
1959
2049
  if (existingEntry) {
1960
- const [existingRequestId] = existingEntry;
2050
+ const [existingRequestId, existingPending] = existingEntry;
2051
+ existingPending.question = question;
2052
+ existingPending.options = options;
2053
+ existingPending.questions = questions;
2054
+ existingPending.createdAt = Date.now();
1961
2055
  const updatedRequest = {
1962
2056
  id: existingRequestId,
1963
2057
  sessionId,
1964
2058
  toolUseId,
1965
2059
  question,
1966
2060
  options,
1967
- createdAt: Date.now()
2061
+ questions,
2062
+ createdAt: existingPending.createdAt
1968
2063
  };
1969
2064
  this.emit({ type: "question_request", request: updatedRequest });
1970
2065
  console.log(`[SessionManager] Session ${sessionId}: AskUserQuestion updated (requestId=${existingRequestId})`);
@@ -1977,12 +2072,13 @@ var SessionManager = class {
1977
2072
  toolUseId,
1978
2073
  question,
1979
2074
  options,
2075
+ questions,
1980
2076
  createdAt: Date.now()
1981
2077
  };
1982
2078
  this.updateSessionStatus(sessionId, "waiting_question");
1983
2079
  this.emit({ type: "question_request", request });
1984
2080
  const answerPromise = new Promise((resolve) => {
1985
- this.pendingQuestions.set(requestId, { sessionId, toolUseId, question, options, createdAt: request.createdAt, resolve });
2081
+ this.pendingQuestions.set(requestId, { sessionId, toolUseId, question, options, questions, createdAt: request.createdAt, resolve });
1986
2082
  });
1987
2083
  answerPromise.then(async (answer) => {
1988
2084
  try {
@@ -4631,18 +4727,25 @@ async function start(opts = {}) {
4631
4727
  }
4632
4728
  case "load_session_history": {
4633
4729
  const historyResult = await getSessionHistory(event.projectPath, event.sessionId);
4634
- if (!historyResult.ok) {
4730
+ let historyEvents = historyResult.ok ? historyResult.value : [];
4731
+ if (historyEvents.length === 0) {
4732
+ const codexProv = providerFactory.getProvider("codex");
4733
+ if (codexProv instanceof CodexProvider && codexProv.isKnownSession(event.sessionId)) {
4734
+ historyEvents = codexProv.getSessionHistory(event.sessionId);
4735
+ }
4736
+ }
4737
+ if (!historyResult.ok && historyEvents.length === 0) {
4635
4738
  wsBridge.send(ws, {
4636
4739
  type: "error",
4637
4740
  message: t("server.readHistoryFailed", { error: historyResult.error.message }),
4638
4741
  code: "SESSION_HISTORY_ERROR",
4639
4742
  sessionId: event.sessionId
4640
4743
  });
4641
- } else if (historyResult.value.length > 0) {
4744
+ } else if (historyEvents.length > 0) {
4642
4745
  wsBridge.send(ws, {
4643
4746
  type: "session_history",
4644
4747
  sessionId: event.sessionId,
4645
- events: historyResult.value
4748
+ events: historyEvents
4646
4749
  });
4647
4750
  const activeSession = sessionManager.getActiveSessions().find((s) => s.id === event.sessionId);
4648
4751
  const isStreaming = activeSession?.status === "running" || activeSession?.status === "waiting_approval";
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "sessix-server",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "bin": {
5
- "sessix-server": "./dist/index.js"
5
+ "sessix-server": "dist/index.js"
6
6
  },
7
7
  "main": "./dist/server.js",
8
8
  "types": "./dist/server.d.ts",