sessix-server 0.3.1 → 0.3.3

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 +120 -44
  2. package/dist/server.js +120 -44
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -326,14 +326,14 @@ var import_node_os = require("os");
326
326
  var import_node_child_process = require("child_process");
327
327
  var isWindows = process.platform === "win32";
328
328
  function killProcessCrossPlatform(proc, timeoutMs = 3e3) {
329
- return new Promise((resolve2) => {
329
+ return new Promise((resolve) => {
330
330
  if (proc.exitCode !== null || proc.signalCode !== null) {
331
- resolve2();
331
+ resolve();
332
332
  return;
333
333
  }
334
334
  const onExit = () => {
335
335
  clearTimeout(timer);
336
- resolve2();
336
+ resolve();
337
337
  };
338
338
  proc.once("exit", onExit);
339
339
  if (isWindows) {
@@ -349,7 +349,7 @@ function killProcessCrossPlatform(proc, timeoutMs = 3e3) {
349
349
  proc.kill("SIGKILL");
350
350
  }
351
351
  }
352
- resolve2();
352
+ resolve();
353
353
  }, timeoutMs);
354
354
  });
355
355
  }
@@ -781,7 +781,7 @@ var ProcessProvider = class {
781
781
  const prompt = `You are an AI coding assistant. Based on the following Claude Code conversation context, suggest the most valuable next instruction for the user (give the instruction directly, no explanation, no quotes):
782
782
 
783
783
  ${context}`;
784
- return new Promise((resolve2, reject) => {
784
+ return new Promise((resolve, reject) => {
785
785
  const env = { ...process.env };
786
786
  delete env.CLAUDECODE;
787
787
  const proc = (0, import_child_process.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
@@ -796,7 +796,7 @@ ${context}`;
796
796
  });
797
797
  proc.once("exit", (code) => {
798
798
  if (code === 0) {
799
- resolve2(output.trim());
799
+ resolve(output.trim());
800
800
  } else {
801
801
  reject(new Error(`generateSuggestion process exit code: ${code}`));
802
802
  }
@@ -823,10 +823,10 @@ ${context}`;
823
823
  tool_use_id: toolUseId,
824
824
  content: answer
825
825
  });
826
- await new Promise((resolve2, reject) => {
826
+ await new Promise((resolve, reject) => {
827
827
  entry.process.stdin.write(toolResult + "\n", (err) => {
828
828
  if (err) reject(err);
829
- else resolve2();
829
+ else resolve();
830
830
  });
831
831
  });
832
832
  console.log(`[ProcessProvider] Session ${sessionId}: AskUserQuestion answered (toolUseId=${toolUseId})`);
@@ -896,15 +896,7 @@ function findCodexPath() {
896
896
  }
897
897
  function resolveCodexJsEntry(codexPath) {
898
898
  try {
899
- let realPath;
900
- try {
901
- realPath = (0, import_node_fs2.readlinkSync)(codexPath);
902
- if (!realPath.startsWith("/")) {
903
- realPath = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(codexPath), realPath);
904
- }
905
- } catch {
906
- realPath = codexPath;
907
- }
899
+ const realPath = (0, import_node_fs2.realpathSync)(codexPath);
908
900
  const head = (0, import_node_fs2.readFileSync)(realPath, { encoding: "utf-8", flag: "r" }).slice(0, 100);
909
901
  if (head.startsWith("#!/usr/bin/env node") || head.startsWith("#!/usr/bin/node")) {
910
902
  return realPath;
@@ -1004,6 +996,7 @@ var CodexProvider = class {
1004
996
  session_id: sessionId
1005
997
  };
1006
998
  this.emitter.emit(this.getEventName(sessionId), initEvent);
999
+ this.emitUserMessage(sessionId, message);
1007
1000
  proc.on("error", (err) => {
1008
1001
  console.error(`[CodexProvider] Session ${sessionId} process error:`, err.message);
1009
1002
  this.activeSessions.delete(sessionId);
@@ -1044,7 +1037,7 @@ var CodexProvider = class {
1044
1037
  summary: persisted.summary,
1045
1038
  agentType: "codex"
1046
1039
  };
1047
- const placeholderProc = (0, import_child_process2.spawn)("true", [], { stdio: "ignore" });
1040
+ const placeholderProc = (0, import_child_process2.spawn)(process.execPath, ["-e", ""], { stdio: "ignore" });
1048
1041
  entry = {
1049
1042
  session,
1050
1043
  process: placeholderProc,
@@ -1065,6 +1058,7 @@ var CodexProvider = class {
1065
1058
  if (!threadId) {
1066
1059
  console.warn(`[CodexProvider] Session ${sessionId} has no thread ID, falling back to new thread`);
1067
1060
  }
1061
+ this.emitUserMessage(sessionId, message);
1068
1062
  const proc = this.spawnCodexProcess(entry.session.projectPath, message, threadId ?? void 0);
1069
1063
  entry.session.status = "running";
1070
1064
  entry.session.lastActiveAt = Date.now();
@@ -1088,7 +1082,26 @@ var CodexProvider = class {
1088
1082
  };
1089
1083
  }
1090
1084
  getActiveSessions() {
1091
- 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());
1092
1105
  }
1093
1106
  async generateSuggestion(_context) {
1094
1107
  return "";
@@ -1213,6 +1226,15 @@ var CodexProvider = class {
1213
1226
  usage: event.usage
1214
1227
  };
1215
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
+ }
1216
1238
  break;
1217
1239
  }
1218
1240
  case "turn.failed": {
@@ -1375,6 +1397,20 @@ var CodexProvider = class {
1375
1397
  this.emitter.emit(this.getEventName(sessionId), syntheticResult);
1376
1398
  });
1377
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
+ }
1378
1414
  emitError(sessionId, message) {
1379
1415
  const event = {
1380
1416
  type: "result",
@@ -1438,6 +1474,22 @@ var CodexProvider = class {
1438
1474
  isKnownSession(sessionId) {
1439
1475
  return this.activeSessions.has(sessionId) || this.persistedSessions.has(sessionId);
1440
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
+ }
1441
1493
  };
1442
1494
 
1443
1495
  // src/providers/ProviderFactory.ts
@@ -1549,8 +1601,15 @@ var SessionManager = class {
1549
1601
  * 调用 provider.startSession(),订阅事件流,
1550
1602
  * 将 ClaudeStreamEvent 包装为 ServerEvent 转发。
1551
1603
  */
1552
- async createSession(projectPath, message, resumeSessionId, newSessionId, model, permissionMode, effort, images, agentType = "claude-code") {
1553
- const provider = this.providerFactory ? this.providerFactory.getProvider(agentType) : this.provider;
1604
+ async createSession(projectPath, message, resumeSessionId, newSessionId, model, permissionMode, effort, images, agentType) {
1605
+ let resolvedAgentType = agentType ?? "claude-code";
1606
+ if (!agentType && resumeSessionId && this.providerFactory) {
1607
+ const codexProvider = this.providerFactory.getProvider("codex");
1608
+ if (codexProvider instanceof CodexProvider && codexProvider.isKnownSession(resumeSessionId)) {
1609
+ resolvedAgentType = "codex";
1610
+ }
1611
+ }
1612
+ const provider = this.providerFactory ? this.providerFactory.getProvider(resolvedAgentType) : this.provider;
1554
1613
  const session = await provider.startSession({
1555
1614
  projectPath,
1556
1615
  message,
@@ -1561,7 +1620,7 @@ var SessionManager = class {
1561
1620
  effort,
1562
1621
  images
1563
1622
  });
1564
- this.sessionAgentType.set(session.id, agentType);
1623
+ this.sessionAgentType.set(session.id, resolvedAgentType);
1565
1624
  this.lastBroadcastStatus.set(session.id, session.status);
1566
1625
  this.sessionProjectPaths.set(session.id, projectPath);
1567
1626
  this.unsubscribeSession(session.id);
@@ -1917,8 +1976,8 @@ var SessionManager = class {
1917
1976
  };
1918
1977
  this.updateSessionStatus(sessionId, "waiting_question");
1919
1978
  this.emit({ type: "question_request", request });
1920
- const answerPromise = new Promise((resolve2) => {
1921
- this.pendingQuestions.set(requestId, { sessionId, toolUseId, question, options, createdAt: request.createdAt, resolve: resolve2 });
1979
+ const answerPromise = new Promise((resolve) => {
1980
+ this.pendingQuestions.set(requestId, { sessionId, toolUseId, question, options, createdAt: request.createdAt, resolve });
1922
1981
  });
1923
1982
  answerPromise.then(async (answer) => {
1924
1983
  try {
@@ -2160,11 +2219,11 @@ var WsBridge = class _WsBridge {
2160
2219
  * 使用此方法代替 new WsBridge(),确保 EADDRINUSE 等错误能被调用方的 try-catch 捕获。
2161
2220
  */
2162
2221
  static async create(options) {
2163
- return new Promise((resolve2, reject) => {
2222
+ return new Promise((resolve, reject) => {
2164
2223
  const bridge = new _WsBridge(options);
2165
2224
  bridge.wss.once("listening", () => {
2166
2225
  bridge.wss.on("error", (err) => console.error(`[WsBridge] ${t("ws.serverError")}:`, err));
2167
- resolve2(bridge);
2226
+ resolve(bridge);
2168
2227
  });
2169
2228
  bridge.wss.once("error", reject);
2170
2229
  });
@@ -2227,7 +2286,7 @@ var WsBridge = class _WsBridge {
2227
2286
  }
2228
2287
  /** 优雅关闭 WebSocket 服务 */
2229
2288
  close() {
2230
- return new Promise((resolve2, reject) => {
2289
+ return new Promise((resolve, reject) => {
2231
2290
  if (this.heartbeatTimer) {
2232
2291
  clearInterval(this.heartbeatTimer);
2233
2292
  this.heartbeatTimer = null;
@@ -2240,7 +2299,7 @@ var WsBridge = class _WsBridge {
2240
2299
  reject(err);
2241
2300
  } else {
2242
2301
  console.log("[WsBridge] WebSocket server closed");
2243
- resolve2();
2302
+ resolve();
2244
2303
  }
2245
2304
  });
2246
2305
  });
@@ -2380,11 +2439,11 @@ var ApprovalProxy = class _ApprovalProxy {
2380
2439
  * 异步工厂方法:等待端口监听成功后 resolve,端口占用等错误时 reject。
2381
2440
  */
2382
2441
  static async create(options) {
2383
- return new Promise((resolve2, reject) => {
2442
+ return new Promise((resolve, reject) => {
2384
2443
  const proxy = new _ApprovalProxy(options);
2385
2444
  proxy.server.once("listening", () => {
2386
2445
  proxy.server.on("error", (err) => console.error(`[ApprovalProxy] ${t("approval.serverError")}:`, err));
2387
- resolve2(proxy);
2446
+ resolve(proxy);
2388
2447
  });
2389
2448
  proxy.server.once("error", reject);
2390
2449
  });
@@ -2541,7 +2600,7 @@ var ApprovalProxy = class _ApprovalProxy {
2541
2600
  }
2542
2601
  /** 优雅关闭 HTTP 服务 */
2543
2602
  close() {
2544
- return new Promise((resolve2, reject) => {
2603
+ return new Promise((resolve, reject) => {
2545
2604
  const pendingEntries = Array.from(this.pendingApprovals.entries());
2546
2605
  for (const [, pending] of pendingEntries) {
2547
2606
  clearTimeout(pending.timer);
@@ -2553,7 +2612,7 @@ var ApprovalProxy = class _ApprovalProxy {
2553
2612
  reject(err);
2554
2613
  } else {
2555
2614
  console.log(`[ApprovalProxy] ${t("approval.httpClosed")}`);
2556
- resolve2();
2615
+ resolve();
2557
2616
  }
2558
2617
  });
2559
2618
  });
@@ -2624,13 +2683,13 @@ var ApprovalProxy = class _ApprovalProxy {
2624
2683
  return;
2625
2684
  }
2626
2685
  this.notifyApprovalRequest(approvalRequest);
2627
- const decision = await new Promise((resolve2) => {
2686
+ const decision = await new Promise((resolve) => {
2628
2687
  const timer = setTimeout(() => {
2629
2688
  console.log(`[ApprovalProxy] ${t("approval.timeout", { id: requestId })}`);
2630
2689
  this.pendingApprovals.delete(requestId);
2631
- resolve2({ decision: "allow" });
2690
+ resolve({ decision: "allow" });
2632
2691
  }, 325e3);
2633
- this.pendingApprovals.set(requestId, { resolve: resolve2, timer, request: approvalRequest });
2692
+ this.pendingApprovals.set(requestId, { resolve, timer, request: approvalRequest });
2634
2693
  });
2635
2694
  this.sendJson(res, 200, decision);
2636
2695
  } catch (err) {
@@ -2691,7 +2750,7 @@ var ApprovalProxy = class _ApprovalProxy {
2691
2750
  /** 手动解析请求的 JSON body(限制最大 1MB 防止滥用) */
2692
2751
  parseJsonBody(req) {
2693
2752
  const MAX_BODY_SIZE = 1024 * 1024;
2694
- return new Promise((resolve2, reject) => {
2753
+ return new Promise((resolve, reject) => {
2695
2754
  const chunks = [];
2696
2755
  let totalSize = 0;
2697
2756
  let destroyed = false;
@@ -2709,7 +2768,7 @@ var ApprovalProxy = class _ApprovalProxy {
2709
2768
  try {
2710
2769
  const raw = Buffer.concat(chunks).toString("utf-8");
2711
2770
  const parsed = JSON.parse(raw);
2712
- resolve2(parsed);
2771
+ resolve(parsed);
2713
2772
  } catch {
2714
2773
  reject(new Error(t("approval.invalidJson")));
2715
2774
  }
@@ -3376,12 +3435,12 @@ var DesktopNotificationChannel = class {
3376
3435
  const body = payload.body.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
3377
3436
  const sound = payload.sound ?? "Ping";
3378
3437
  const script = `display notification "${body}" with title "${title}" sound name "${sound}"`;
3379
- return new Promise((resolve2) => {
3438
+ return new Promise((resolve) => {
3380
3439
  (0, import_node_child_process6.execFile)("osascript", ["-e", script], (err) => {
3381
3440
  if (err) {
3382
3441
  console.warn("[DesktopNotificationChannel] Send notification failed:", err.message);
3383
3442
  }
3384
- resolve2();
3443
+ resolve();
3385
3444
  });
3386
3445
  });
3387
3446
  }
@@ -3581,7 +3640,7 @@ var ActivityPushChannel = class {
3581
3640
  const topic = "com.kachun.sessix.push-type.liveactivity";
3582
3641
  const jwt = this.getJWT();
3583
3642
  const payloadStr = JSON.stringify(payload);
3584
- return new Promise((resolve2, reject) => {
3643
+ return new Promise((resolve, reject) => {
3585
3644
  let client;
3586
3645
  try {
3587
3646
  client = this.getHttp2Client();
@@ -3609,7 +3668,7 @@ var ActivityPushChannel = class {
3609
3668
  });
3610
3669
  req.on("end", () => {
3611
3670
  if (statusCode === 200) {
3612
- resolve2();
3671
+ resolve();
3613
3672
  } else {
3614
3673
  if (statusCode === 0) {
3615
3674
  this.http2Client?.destroy();
@@ -3792,7 +3851,7 @@ async function getSessionHistory(projectPath, sessionId) {
3792
3851
  if (err.code === "ENOENT") return null;
3793
3852
  throw err;
3794
3853
  });
3795
- if (raw === null) return { ok: false, error: new Error("ENOENT") };
3854
+ if (raw === null) return { ok: true, value: [] };
3796
3855
  const lines = raw.split("\n").filter((l) => l.trim());
3797
3856
  const events = [];
3798
3857
  for (const line of lines) {
@@ -4282,7 +4341,7 @@ async function killPortProcess(port) {
4282
4341
  await execAsync(`kill -9 ${pids.join(" ")}`);
4283
4342
  }
4284
4343
  }
4285
- await new Promise((resolve2) => setTimeout(resolve2, 600));
4344
+ await new Promise((resolve) => setTimeout(resolve, 600));
4286
4345
  } catch {
4287
4346
  }
4288
4347
  }
@@ -4534,10 +4593,27 @@ async function start(opts = {}) {
4534
4593
  case "list_project_sessions": {
4535
4594
  const histResult = await getHistoricalSessions(event.projectPath);
4536
4595
  if (histResult.ok) {
4596
+ const sessions = histResult.value;
4597
+ const codexProvider = providerFactory.getProvider("codex");
4598
+ if (codexProvider instanceof CodexProvider) {
4599
+ const codexSessions = codexProvider.getPersistedSessionsForProject(event.projectPath);
4600
+ const existingIds = new Set(sessions.map((s) => s.sessionId));
4601
+ for (const cs of codexSessions) {
4602
+ if (!existingIds.has(cs.sessionId)) {
4603
+ sessions.push({
4604
+ sessionId: cs.sessionId,
4605
+ lastModified: cs.lastModified,
4606
+ summary: cs.summary,
4607
+ agentType: "codex"
4608
+ });
4609
+ }
4610
+ }
4611
+ sessions.sort((a, b) => b.lastModified - a.lastModified);
4612
+ }
4537
4613
  wsBridge.send(ws, {
4538
4614
  type: "project_sessions",
4539
4615
  projectPath: event.projectPath,
4540
- sessions: histResult.value
4616
+ sessions
4541
4617
  });
4542
4618
  } else {
4543
4619
  wsBridge.send(ws, {
package/dist/server.js CHANGED
@@ -331,14 +331,14 @@ var import_node_os = require("os");
331
331
  var import_node_child_process = require("child_process");
332
332
  var isWindows = process.platform === "win32";
333
333
  function killProcessCrossPlatform(proc, timeoutMs = 3e3) {
334
- return new Promise((resolve2) => {
334
+ return new Promise((resolve) => {
335
335
  if (proc.exitCode !== null || proc.signalCode !== null) {
336
- resolve2();
336
+ resolve();
337
337
  return;
338
338
  }
339
339
  const onExit = () => {
340
340
  clearTimeout(timer);
341
- resolve2();
341
+ resolve();
342
342
  };
343
343
  proc.once("exit", onExit);
344
344
  if (isWindows) {
@@ -354,7 +354,7 @@ function killProcessCrossPlatform(proc, timeoutMs = 3e3) {
354
354
  proc.kill("SIGKILL");
355
355
  }
356
356
  }
357
- resolve2();
357
+ resolve();
358
358
  }, timeoutMs);
359
359
  });
360
360
  }
@@ -786,7 +786,7 @@ var ProcessProvider = class {
786
786
  const prompt = `You are an AI coding assistant. Based on the following Claude Code conversation context, suggest the most valuable next instruction for the user (give the instruction directly, no explanation, no quotes):
787
787
 
788
788
  ${context}`;
789
- return new Promise((resolve2, reject) => {
789
+ return new Promise((resolve, reject) => {
790
790
  const env = { ...process.env };
791
791
  delete env.CLAUDECODE;
792
792
  const proc = (0, import_child_process.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
@@ -801,7 +801,7 @@ ${context}`;
801
801
  });
802
802
  proc.once("exit", (code) => {
803
803
  if (code === 0) {
804
- resolve2(output.trim());
804
+ resolve(output.trim());
805
805
  } else {
806
806
  reject(new Error(`generateSuggestion process exit code: ${code}`));
807
807
  }
@@ -828,10 +828,10 @@ ${context}`;
828
828
  tool_use_id: toolUseId,
829
829
  content: answer
830
830
  });
831
- await new Promise((resolve2, reject) => {
831
+ await new Promise((resolve, reject) => {
832
832
  entry.process.stdin.write(toolResult + "\n", (err) => {
833
833
  if (err) reject(err);
834
- else resolve2();
834
+ else resolve();
835
835
  });
836
836
  });
837
837
  console.log(`[ProcessProvider] Session ${sessionId}: AskUserQuestion answered (toolUseId=${toolUseId})`);
@@ -901,15 +901,7 @@ function findCodexPath() {
901
901
  }
902
902
  function resolveCodexJsEntry(codexPath) {
903
903
  try {
904
- let realPath;
905
- try {
906
- realPath = (0, import_node_fs2.readlinkSync)(codexPath);
907
- if (!realPath.startsWith("/")) {
908
- realPath = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(codexPath), realPath);
909
- }
910
- } catch {
911
- realPath = codexPath;
912
- }
904
+ const realPath = (0, import_node_fs2.realpathSync)(codexPath);
913
905
  const head = (0, import_node_fs2.readFileSync)(realPath, { encoding: "utf-8", flag: "r" }).slice(0, 100);
914
906
  if (head.startsWith("#!/usr/bin/env node") || head.startsWith("#!/usr/bin/node")) {
915
907
  return realPath;
@@ -1009,6 +1001,7 @@ var CodexProvider = class {
1009
1001
  session_id: sessionId
1010
1002
  };
1011
1003
  this.emitter.emit(this.getEventName(sessionId), initEvent);
1004
+ this.emitUserMessage(sessionId, message);
1012
1005
  proc.on("error", (err) => {
1013
1006
  console.error(`[CodexProvider] Session ${sessionId} process error:`, err.message);
1014
1007
  this.activeSessions.delete(sessionId);
@@ -1049,7 +1042,7 @@ var CodexProvider = class {
1049
1042
  summary: persisted.summary,
1050
1043
  agentType: "codex"
1051
1044
  };
1052
- const placeholderProc = (0, import_child_process2.spawn)("true", [], { stdio: "ignore" });
1045
+ const placeholderProc = (0, import_child_process2.spawn)(process.execPath, ["-e", ""], { stdio: "ignore" });
1053
1046
  entry = {
1054
1047
  session,
1055
1048
  process: placeholderProc,
@@ -1070,6 +1063,7 @@ var CodexProvider = class {
1070
1063
  if (!threadId) {
1071
1064
  console.warn(`[CodexProvider] Session ${sessionId} has no thread ID, falling back to new thread`);
1072
1065
  }
1066
+ this.emitUserMessage(sessionId, message);
1073
1067
  const proc = this.spawnCodexProcess(entry.session.projectPath, message, threadId ?? void 0);
1074
1068
  entry.session.status = "running";
1075
1069
  entry.session.lastActiveAt = Date.now();
@@ -1093,7 +1087,26 @@ var CodexProvider = class {
1093
1087
  };
1094
1088
  }
1095
1089
  getActiveSessions() {
1096
- 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());
1097
1110
  }
1098
1111
  async generateSuggestion(_context) {
1099
1112
  return "";
@@ -1218,6 +1231,15 @@ var CodexProvider = class {
1218
1231
  usage: event.usage
1219
1232
  };
1220
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
+ }
1221
1243
  break;
1222
1244
  }
1223
1245
  case "turn.failed": {
@@ -1380,6 +1402,20 @@ var CodexProvider = class {
1380
1402
  this.emitter.emit(this.getEventName(sessionId), syntheticResult);
1381
1403
  });
1382
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
+ }
1383
1419
  emitError(sessionId, message) {
1384
1420
  const event = {
1385
1421
  type: "result",
@@ -1443,6 +1479,22 @@ var CodexProvider = class {
1443
1479
  isKnownSession(sessionId) {
1444
1480
  return this.activeSessions.has(sessionId) || this.persistedSessions.has(sessionId);
1445
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
+ }
1446
1498
  };
1447
1499
 
1448
1500
  // src/providers/ProviderFactory.ts
@@ -1554,8 +1606,15 @@ var SessionManager = class {
1554
1606
  * 调用 provider.startSession(),订阅事件流,
1555
1607
  * 将 ClaudeStreamEvent 包装为 ServerEvent 转发。
1556
1608
  */
1557
- async createSession(projectPath, message, resumeSessionId, newSessionId, model, permissionMode, effort, images, agentType = "claude-code") {
1558
- const provider = this.providerFactory ? this.providerFactory.getProvider(agentType) : this.provider;
1609
+ async createSession(projectPath, message, resumeSessionId, newSessionId, model, permissionMode, effort, images, agentType) {
1610
+ let resolvedAgentType = agentType ?? "claude-code";
1611
+ if (!agentType && resumeSessionId && this.providerFactory) {
1612
+ const codexProvider = this.providerFactory.getProvider("codex");
1613
+ if (codexProvider instanceof CodexProvider && codexProvider.isKnownSession(resumeSessionId)) {
1614
+ resolvedAgentType = "codex";
1615
+ }
1616
+ }
1617
+ const provider = this.providerFactory ? this.providerFactory.getProvider(resolvedAgentType) : this.provider;
1559
1618
  const session = await provider.startSession({
1560
1619
  projectPath,
1561
1620
  message,
@@ -1566,7 +1625,7 @@ var SessionManager = class {
1566
1625
  effort,
1567
1626
  images
1568
1627
  });
1569
- this.sessionAgentType.set(session.id, agentType);
1628
+ this.sessionAgentType.set(session.id, resolvedAgentType);
1570
1629
  this.lastBroadcastStatus.set(session.id, session.status);
1571
1630
  this.sessionProjectPaths.set(session.id, projectPath);
1572
1631
  this.unsubscribeSession(session.id);
@@ -1922,8 +1981,8 @@ var SessionManager = class {
1922
1981
  };
1923
1982
  this.updateSessionStatus(sessionId, "waiting_question");
1924
1983
  this.emit({ type: "question_request", request });
1925
- const answerPromise = new Promise((resolve2) => {
1926
- this.pendingQuestions.set(requestId, { sessionId, toolUseId, question, options, createdAt: request.createdAt, resolve: resolve2 });
1984
+ const answerPromise = new Promise((resolve) => {
1985
+ this.pendingQuestions.set(requestId, { sessionId, toolUseId, question, options, createdAt: request.createdAt, resolve });
1927
1986
  });
1928
1987
  answerPromise.then(async (answer) => {
1929
1988
  try {
@@ -2165,11 +2224,11 @@ var WsBridge = class _WsBridge {
2165
2224
  * 使用此方法代替 new WsBridge(),确保 EADDRINUSE 等错误能被调用方的 try-catch 捕获。
2166
2225
  */
2167
2226
  static async create(options) {
2168
- return new Promise((resolve2, reject) => {
2227
+ return new Promise((resolve, reject) => {
2169
2228
  const bridge = new _WsBridge(options);
2170
2229
  bridge.wss.once("listening", () => {
2171
2230
  bridge.wss.on("error", (err) => console.error(`[WsBridge] ${t("ws.serverError")}:`, err));
2172
- resolve2(bridge);
2231
+ resolve(bridge);
2173
2232
  });
2174
2233
  bridge.wss.once("error", reject);
2175
2234
  });
@@ -2232,7 +2291,7 @@ var WsBridge = class _WsBridge {
2232
2291
  }
2233
2292
  /** 优雅关闭 WebSocket 服务 */
2234
2293
  close() {
2235
- return new Promise((resolve2, reject) => {
2294
+ return new Promise((resolve, reject) => {
2236
2295
  if (this.heartbeatTimer) {
2237
2296
  clearInterval(this.heartbeatTimer);
2238
2297
  this.heartbeatTimer = null;
@@ -2245,7 +2304,7 @@ var WsBridge = class _WsBridge {
2245
2304
  reject(err);
2246
2305
  } else {
2247
2306
  console.log("[WsBridge] WebSocket server closed");
2248
- resolve2();
2307
+ resolve();
2249
2308
  }
2250
2309
  });
2251
2310
  });
@@ -2385,11 +2444,11 @@ var ApprovalProxy = class _ApprovalProxy {
2385
2444
  * 异步工厂方法:等待端口监听成功后 resolve,端口占用等错误时 reject。
2386
2445
  */
2387
2446
  static async create(options) {
2388
- return new Promise((resolve2, reject) => {
2447
+ return new Promise((resolve, reject) => {
2389
2448
  const proxy = new _ApprovalProxy(options);
2390
2449
  proxy.server.once("listening", () => {
2391
2450
  proxy.server.on("error", (err) => console.error(`[ApprovalProxy] ${t("approval.serverError")}:`, err));
2392
- resolve2(proxy);
2451
+ resolve(proxy);
2393
2452
  });
2394
2453
  proxy.server.once("error", reject);
2395
2454
  });
@@ -2546,7 +2605,7 @@ var ApprovalProxy = class _ApprovalProxy {
2546
2605
  }
2547
2606
  /** 优雅关闭 HTTP 服务 */
2548
2607
  close() {
2549
- return new Promise((resolve2, reject) => {
2608
+ return new Promise((resolve, reject) => {
2550
2609
  const pendingEntries = Array.from(this.pendingApprovals.entries());
2551
2610
  for (const [, pending] of pendingEntries) {
2552
2611
  clearTimeout(pending.timer);
@@ -2558,7 +2617,7 @@ var ApprovalProxy = class _ApprovalProxy {
2558
2617
  reject(err);
2559
2618
  } else {
2560
2619
  console.log(`[ApprovalProxy] ${t("approval.httpClosed")}`);
2561
- resolve2();
2620
+ resolve();
2562
2621
  }
2563
2622
  });
2564
2623
  });
@@ -2629,13 +2688,13 @@ var ApprovalProxy = class _ApprovalProxy {
2629
2688
  return;
2630
2689
  }
2631
2690
  this.notifyApprovalRequest(approvalRequest);
2632
- const decision = await new Promise((resolve2) => {
2691
+ const decision = await new Promise((resolve) => {
2633
2692
  const timer = setTimeout(() => {
2634
2693
  console.log(`[ApprovalProxy] ${t("approval.timeout", { id: requestId })}`);
2635
2694
  this.pendingApprovals.delete(requestId);
2636
- resolve2({ decision: "allow" });
2695
+ resolve({ decision: "allow" });
2637
2696
  }, 325e3);
2638
- this.pendingApprovals.set(requestId, { resolve: resolve2, timer, request: approvalRequest });
2697
+ this.pendingApprovals.set(requestId, { resolve, timer, request: approvalRequest });
2639
2698
  });
2640
2699
  this.sendJson(res, 200, decision);
2641
2700
  } catch (err) {
@@ -2696,7 +2755,7 @@ var ApprovalProxy = class _ApprovalProxy {
2696
2755
  /** 手动解析请求的 JSON body(限制最大 1MB 防止滥用) */
2697
2756
  parseJsonBody(req) {
2698
2757
  const MAX_BODY_SIZE = 1024 * 1024;
2699
- return new Promise((resolve2, reject) => {
2758
+ return new Promise((resolve, reject) => {
2700
2759
  const chunks = [];
2701
2760
  let totalSize = 0;
2702
2761
  let destroyed = false;
@@ -2714,7 +2773,7 @@ var ApprovalProxy = class _ApprovalProxy {
2714
2773
  try {
2715
2774
  const raw = Buffer.concat(chunks).toString("utf-8");
2716
2775
  const parsed = JSON.parse(raw);
2717
- resolve2(parsed);
2776
+ resolve(parsed);
2718
2777
  } catch {
2719
2778
  reject(new Error(t("approval.invalidJson")));
2720
2779
  }
@@ -3381,12 +3440,12 @@ var DesktopNotificationChannel = class {
3381
3440
  const body = payload.body.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
3382
3441
  const sound = payload.sound ?? "Ping";
3383
3442
  const script = `display notification "${body}" with title "${title}" sound name "${sound}"`;
3384
- return new Promise((resolve2) => {
3443
+ return new Promise((resolve) => {
3385
3444
  (0, import_node_child_process6.execFile)("osascript", ["-e", script], (err) => {
3386
3445
  if (err) {
3387
3446
  console.warn("[DesktopNotificationChannel] Send notification failed:", err.message);
3388
3447
  }
3389
- resolve2();
3448
+ resolve();
3390
3449
  });
3391
3450
  });
3392
3451
  }
@@ -3586,7 +3645,7 @@ var ActivityPushChannel = class {
3586
3645
  const topic = "com.kachun.sessix.push-type.liveactivity";
3587
3646
  const jwt = this.getJWT();
3588
3647
  const payloadStr = JSON.stringify(payload);
3589
- return new Promise((resolve2, reject) => {
3648
+ return new Promise((resolve, reject) => {
3590
3649
  let client;
3591
3650
  try {
3592
3651
  client = this.getHttp2Client();
@@ -3614,7 +3673,7 @@ var ActivityPushChannel = class {
3614
3673
  });
3615
3674
  req.on("end", () => {
3616
3675
  if (statusCode === 200) {
3617
- resolve2();
3676
+ resolve();
3618
3677
  } else {
3619
3678
  if (statusCode === 0) {
3620
3679
  this.http2Client?.destroy();
@@ -3797,7 +3856,7 @@ async function getSessionHistory(projectPath, sessionId) {
3797
3856
  if (err.code === "ENOENT") return null;
3798
3857
  throw err;
3799
3858
  });
3800
- if (raw === null) return { ok: false, error: new Error("ENOENT") };
3859
+ if (raw === null) return { ok: true, value: [] };
3801
3860
  const lines = raw.split("\n").filter((l) => l.trim());
3802
3861
  const events = [];
3803
3862
  for (const line of lines) {
@@ -4287,7 +4346,7 @@ async function killPortProcess(port) {
4287
4346
  await execAsync(`kill -9 ${pids.join(" ")}`);
4288
4347
  }
4289
4348
  }
4290
- await new Promise((resolve2) => setTimeout(resolve2, 600));
4349
+ await new Promise((resolve) => setTimeout(resolve, 600));
4291
4350
  } catch {
4292
4351
  }
4293
4352
  }
@@ -4539,10 +4598,27 @@ async function start(opts = {}) {
4539
4598
  case "list_project_sessions": {
4540
4599
  const histResult = await getHistoricalSessions(event.projectPath);
4541
4600
  if (histResult.ok) {
4601
+ const sessions = histResult.value;
4602
+ const codexProvider = providerFactory.getProvider("codex");
4603
+ if (codexProvider instanceof CodexProvider) {
4604
+ const codexSessions = codexProvider.getPersistedSessionsForProject(event.projectPath);
4605
+ const existingIds = new Set(sessions.map((s) => s.sessionId));
4606
+ for (const cs of codexSessions) {
4607
+ if (!existingIds.has(cs.sessionId)) {
4608
+ sessions.push({
4609
+ sessionId: cs.sessionId,
4610
+ lastModified: cs.lastModified,
4611
+ summary: cs.summary,
4612
+ agentType: "codex"
4613
+ });
4614
+ }
4615
+ }
4616
+ sessions.sort((a, b) => b.lastModified - a.lastModified);
4617
+ }
4542
4618
  wsBridge.send(ws, {
4543
4619
  type: "project_sessions",
4544
4620
  projectPath: event.projectPath,
4545
- sessions: histResult.value
4621
+ sessions
4546
4622
  });
4547
4623
  } else {
4548
4624
  wsBridge.send(ws, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sessix-server",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "bin": {
5
5
  "sessix-server": "./dist/index.js"
6
6
  },