sessix-server 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +161 -51
- package/dist/server.js +161 -51
- package/package.json +1 -1
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
|
-
|
|
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(
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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) {
|
|
@@ -1601,8 +1677,15 @@ var SessionManager = class {
|
|
|
1601
1677
|
* 调用 provider.startSession(),订阅事件流,
|
|
1602
1678
|
* 将 ClaudeStreamEvent 包装为 ServerEvent 转发。
|
|
1603
1679
|
*/
|
|
1604
|
-
async createSession(projectPath, message, resumeSessionId, newSessionId, model, permissionMode, effort, images, agentType
|
|
1605
|
-
|
|
1680
|
+
async createSession(projectPath, message, resumeSessionId, newSessionId, model, permissionMode, effort, images, agentType) {
|
|
1681
|
+
let resolvedAgentType = agentType ?? "claude-code";
|
|
1682
|
+
if (!agentType && resumeSessionId && this.providerFactory) {
|
|
1683
|
+
const codexProvider = this.providerFactory.getProvider("codex");
|
|
1684
|
+
if (codexProvider instanceof CodexProvider && codexProvider.isKnownSession(resumeSessionId)) {
|
|
1685
|
+
resolvedAgentType = "codex";
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
const provider = this.providerFactory ? this.providerFactory.getProvider(resolvedAgentType) : this.provider;
|
|
1606
1689
|
const session = await provider.startSession({
|
|
1607
1690
|
projectPath,
|
|
1608
1691
|
message,
|
|
@@ -1613,7 +1696,7 @@ var SessionManager = class {
|
|
|
1613
1696
|
effort,
|
|
1614
1697
|
images
|
|
1615
1698
|
});
|
|
1616
|
-
this.sessionAgentType.set(session.id,
|
|
1699
|
+
this.sessionAgentType.set(session.id, resolvedAgentType);
|
|
1617
1700
|
this.lastBroadcastStatus.set(session.id, session.status);
|
|
1618
1701
|
this.sessionProjectPaths.set(session.id, projectPath);
|
|
1619
1702
|
this.unsubscribeSession(session.id);
|
|
@@ -1678,10 +1761,13 @@ var SessionManager = class {
|
|
|
1678
1761
|
console.warn(`[SessionManager] Question request not found: ${requestId}`);
|
|
1679
1762
|
return;
|
|
1680
1763
|
}
|
|
1764
|
+
const { sessionId } = pending;
|
|
1681
1765
|
this.pendingQuestions.delete(requestId);
|
|
1682
|
-
this.updateSessionStatus(pending.sessionId, "running");
|
|
1683
1766
|
pending.resolve(answer);
|
|
1684
1767
|
console.log(`[SessionManager] Question answered: ${requestId}`);
|
|
1768
|
+
if (!this.hasPendingQuestionsForSession(sessionId)) {
|
|
1769
|
+
this.updateSessionStatus(sessionId, "running");
|
|
1770
|
+
}
|
|
1685
1771
|
}
|
|
1686
1772
|
/**
|
|
1687
1773
|
* 获取指定会话的所有待回答问题(用于重连时恢复)
|
|
@@ -1696,6 +1782,7 @@ var SessionManager = class {
|
|
|
1696
1782
|
toolUseId: pending.toolUseId,
|
|
1697
1783
|
question: pending.question,
|
|
1698
1784
|
options: pending.options,
|
|
1785
|
+
questions: pending.questions,
|
|
1699
1786
|
createdAt: pending.createdAt
|
|
1700
1787
|
});
|
|
1701
1788
|
}
|
|
@@ -1706,6 +1793,13 @@ var SessionManager = class {
|
|
|
1706
1793
|
isQuestionPending(requestId) {
|
|
1707
1794
|
return this.pendingQuestions.has(requestId);
|
|
1708
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
|
+
}
|
|
1709
1803
|
/**
|
|
1710
1804
|
* 获取所有待回答问题(用于客户端重连时恢复状态)
|
|
1711
1805
|
*/
|
|
@@ -1718,6 +1812,7 @@ var SessionManager = class {
|
|
|
1718
1812
|
toolUseId: pending.toolUseId,
|
|
1719
1813
|
question: pending.question,
|
|
1720
1814
|
options: pending.options,
|
|
1815
|
+
questions: pending.questions,
|
|
1721
1816
|
createdAt: pending.createdAt
|
|
1722
1817
|
});
|
|
1723
1818
|
}
|
|
@@ -1785,8 +1880,8 @@ var SessionManager = class {
|
|
|
1785
1880
|
});
|
|
1786
1881
|
const unsubscribeQuestion = provider.onQuestion(
|
|
1787
1882
|
sessionId,
|
|
1788
|
-
({ toolUseId, question, options }) => {
|
|
1789
|
-
this.handleAskUserQuestion(sessionId, toolUseId, question, options);
|
|
1883
|
+
({ toolUseId, question, options, questions }) => {
|
|
1884
|
+
this.handleAskUserQuestion(sessionId, toolUseId, question, options, questions);
|
|
1790
1885
|
}
|
|
1791
1886
|
);
|
|
1792
1887
|
this.unsubscribeMap.set(sessionId, () => {
|
|
@@ -1856,7 +1951,9 @@ var SessionManager = class {
|
|
|
1856
1951
|
stats.totalCostUsd = (stats.totalCostUsd ?? 0) + event.total_cost_usd;
|
|
1857
1952
|
}
|
|
1858
1953
|
this.sessionStats.set(sessionId, stats);
|
|
1859
|
-
if (
|
|
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) {
|
|
1860
1957
|
this.updateSessionStatus(sessionId, "error");
|
|
1861
1958
|
} else {
|
|
1862
1959
|
this.updateSessionStatus(sessionId, "idle");
|
|
@@ -1940,19 +2037,24 @@ var SessionManager = class {
|
|
|
1940
2037
|
/**
|
|
1941
2038
|
* 处理 AskUserQuestion 事件:广播问题请求到手机,等待用户回答
|
|
1942
2039
|
*/
|
|
1943
|
-
handleAskUserQuestion(sessionId, toolUseId, question, options) {
|
|
2040
|
+
handleAskUserQuestion(sessionId, toolUseId, question, options, questions) {
|
|
1944
2041
|
const existingEntry = Array.from(this.pendingQuestions.entries()).find(
|
|
1945
2042
|
([, v]) => v.toolUseId === toolUseId
|
|
1946
2043
|
);
|
|
1947
2044
|
if (existingEntry) {
|
|
1948
|
-
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();
|
|
1949
2050
|
const updatedRequest = {
|
|
1950
2051
|
id: existingRequestId,
|
|
1951
2052
|
sessionId,
|
|
1952
2053
|
toolUseId,
|
|
1953
2054
|
question,
|
|
1954
2055
|
options,
|
|
1955
|
-
|
|
2056
|
+
questions,
|
|
2057
|
+
createdAt: existingPending.createdAt
|
|
1956
2058
|
};
|
|
1957
2059
|
this.emit({ type: "question_request", request: updatedRequest });
|
|
1958
2060
|
console.log(`[SessionManager] Session ${sessionId}: AskUserQuestion updated (requestId=${existingRequestId})`);
|
|
@@ -1965,12 +2067,13 @@ var SessionManager = class {
|
|
|
1965
2067
|
toolUseId,
|
|
1966
2068
|
question,
|
|
1967
2069
|
options,
|
|
2070
|
+
questions,
|
|
1968
2071
|
createdAt: Date.now()
|
|
1969
2072
|
};
|
|
1970
2073
|
this.updateSessionStatus(sessionId, "waiting_question");
|
|
1971
2074
|
this.emit({ type: "question_request", request });
|
|
1972
2075
|
const answerPromise = new Promise((resolve) => {
|
|
1973
|
-
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 });
|
|
1974
2077
|
});
|
|
1975
2078
|
answerPromise.then(async (answer) => {
|
|
1976
2079
|
try {
|
|
@@ -3844,7 +3947,7 @@ async function getSessionHistory(projectPath, sessionId) {
|
|
|
3844
3947
|
if (err.code === "ENOENT") return null;
|
|
3845
3948
|
throw err;
|
|
3846
3949
|
});
|
|
3847
|
-
if (raw === null) return { ok:
|
|
3950
|
+
if (raw === null) return { ok: true, value: [] };
|
|
3848
3951
|
const lines = raw.split("\n").filter((l) => l.trim());
|
|
3849
3952
|
const events = [];
|
|
3850
3953
|
for (const line of lines) {
|
|
@@ -4619,18 +4722,25 @@ async function start(opts = {}) {
|
|
|
4619
4722
|
}
|
|
4620
4723
|
case "load_session_history": {
|
|
4621
4724
|
const historyResult = await getSessionHistory(event.projectPath, event.sessionId);
|
|
4622
|
-
|
|
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) {
|
|
4623
4733
|
wsBridge.send(ws, {
|
|
4624
4734
|
type: "error",
|
|
4625
4735
|
message: t("server.readHistoryFailed", { error: historyResult.error.message }),
|
|
4626
4736
|
code: "SESSION_HISTORY_ERROR",
|
|
4627
4737
|
sessionId: event.sessionId
|
|
4628
4738
|
});
|
|
4629
|
-
} else if (
|
|
4739
|
+
} else if (historyEvents.length > 0) {
|
|
4630
4740
|
wsBridge.send(ws, {
|
|
4631
4741
|
type: "session_history",
|
|
4632
4742
|
sessionId: event.sessionId,
|
|
4633
|
-
events:
|
|
4743
|
+
events: historyEvents
|
|
4634
4744
|
});
|
|
4635
4745
|
const activeSession = sessionManager.getActiveSessions().find((s) => s.id === event.sessionId);
|
|
4636
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
|
-
|
|
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(
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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) {
|
|
@@ -1606,8 +1682,15 @@ var SessionManager = class {
|
|
|
1606
1682
|
* 调用 provider.startSession(),订阅事件流,
|
|
1607
1683
|
* 将 ClaudeStreamEvent 包装为 ServerEvent 转发。
|
|
1608
1684
|
*/
|
|
1609
|
-
async createSession(projectPath, message, resumeSessionId, newSessionId, model, permissionMode, effort, images, agentType
|
|
1610
|
-
|
|
1685
|
+
async createSession(projectPath, message, resumeSessionId, newSessionId, model, permissionMode, effort, images, agentType) {
|
|
1686
|
+
let resolvedAgentType = agentType ?? "claude-code";
|
|
1687
|
+
if (!agentType && resumeSessionId && this.providerFactory) {
|
|
1688
|
+
const codexProvider = this.providerFactory.getProvider("codex");
|
|
1689
|
+
if (codexProvider instanceof CodexProvider && codexProvider.isKnownSession(resumeSessionId)) {
|
|
1690
|
+
resolvedAgentType = "codex";
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
const provider = this.providerFactory ? this.providerFactory.getProvider(resolvedAgentType) : this.provider;
|
|
1611
1694
|
const session = await provider.startSession({
|
|
1612
1695
|
projectPath,
|
|
1613
1696
|
message,
|
|
@@ -1618,7 +1701,7 @@ var SessionManager = class {
|
|
|
1618
1701
|
effort,
|
|
1619
1702
|
images
|
|
1620
1703
|
});
|
|
1621
|
-
this.sessionAgentType.set(session.id,
|
|
1704
|
+
this.sessionAgentType.set(session.id, resolvedAgentType);
|
|
1622
1705
|
this.lastBroadcastStatus.set(session.id, session.status);
|
|
1623
1706
|
this.sessionProjectPaths.set(session.id, projectPath);
|
|
1624
1707
|
this.unsubscribeSession(session.id);
|
|
@@ -1683,10 +1766,13 @@ var SessionManager = class {
|
|
|
1683
1766
|
console.warn(`[SessionManager] Question request not found: ${requestId}`);
|
|
1684
1767
|
return;
|
|
1685
1768
|
}
|
|
1769
|
+
const { sessionId } = pending;
|
|
1686
1770
|
this.pendingQuestions.delete(requestId);
|
|
1687
|
-
this.updateSessionStatus(pending.sessionId, "running");
|
|
1688
1771
|
pending.resolve(answer);
|
|
1689
1772
|
console.log(`[SessionManager] Question answered: ${requestId}`);
|
|
1773
|
+
if (!this.hasPendingQuestionsForSession(sessionId)) {
|
|
1774
|
+
this.updateSessionStatus(sessionId, "running");
|
|
1775
|
+
}
|
|
1690
1776
|
}
|
|
1691
1777
|
/**
|
|
1692
1778
|
* 获取指定会话的所有待回答问题(用于重连时恢复)
|
|
@@ -1701,6 +1787,7 @@ var SessionManager = class {
|
|
|
1701
1787
|
toolUseId: pending.toolUseId,
|
|
1702
1788
|
question: pending.question,
|
|
1703
1789
|
options: pending.options,
|
|
1790
|
+
questions: pending.questions,
|
|
1704
1791
|
createdAt: pending.createdAt
|
|
1705
1792
|
});
|
|
1706
1793
|
}
|
|
@@ -1711,6 +1798,13 @@ var SessionManager = class {
|
|
|
1711
1798
|
isQuestionPending(requestId) {
|
|
1712
1799
|
return this.pendingQuestions.has(requestId);
|
|
1713
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
|
+
}
|
|
1714
1808
|
/**
|
|
1715
1809
|
* 获取所有待回答问题(用于客户端重连时恢复状态)
|
|
1716
1810
|
*/
|
|
@@ -1723,6 +1817,7 @@ var SessionManager = class {
|
|
|
1723
1817
|
toolUseId: pending.toolUseId,
|
|
1724
1818
|
question: pending.question,
|
|
1725
1819
|
options: pending.options,
|
|
1820
|
+
questions: pending.questions,
|
|
1726
1821
|
createdAt: pending.createdAt
|
|
1727
1822
|
});
|
|
1728
1823
|
}
|
|
@@ -1790,8 +1885,8 @@ var SessionManager = class {
|
|
|
1790
1885
|
});
|
|
1791
1886
|
const unsubscribeQuestion = provider.onQuestion(
|
|
1792
1887
|
sessionId,
|
|
1793
|
-
({ toolUseId, question, options }) => {
|
|
1794
|
-
this.handleAskUserQuestion(sessionId, toolUseId, question, options);
|
|
1888
|
+
({ toolUseId, question, options, questions }) => {
|
|
1889
|
+
this.handleAskUserQuestion(sessionId, toolUseId, question, options, questions);
|
|
1795
1890
|
}
|
|
1796
1891
|
);
|
|
1797
1892
|
this.unsubscribeMap.set(sessionId, () => {
|
|
@@ -1861,7 +1956,9 @@ var SessionManager = class {
|
|
|
1861
1956
|
stats.totalCostUsd = (stats.totalCostUsd ?? 0) + event.total_cost_usd;
|
|
1862
1957
|
}
|
|
1863
1958
|
this.sessionStats.set(sessionId, stats);
|
|
1864
|
-
if (
|
|
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) {
|
|
1865
1962
|
this.updateSessionStatus(sessionId, "error");
|
|
1866
1963
|
} else {
|
|
1867
1964
|
this.updateSessionStatus(sessionId, "idle");
|
|
@@ -1945,19 +2042,24 @@ var SessionManager = class {
|
|
|
1945
2042
|
/**
|
|
1946
2043
|
* 处理 AskUserQuestion 事件:广播问题请求到手机,等待用户回答
|
|
1947
2044
|
*/
|
|
1948
|
-
handleAskUserQuestion(sessionId, toolUseId, question, options) {
|
|
2045
|
+
handleAskUserQuestion(sessionId, toolUseId, question, options, questions) {
|
|
1949
2046
|
const existingEntry = Array.from(this.pendingQuestions.entries()).find(
|
|
1950
2047
|
([, v]) => v.toolUseId === toolUseId
|
|
1951
2048
|
);
|
|
1952
2049
|
if (existingEntry) {
|
|
1953
|
-
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();
|
|
1954
2055
|
const updatedRequest = {
|
|
1955
2056
|
id: existingRequestId,
|
|
1956
2057
|
sessionId,
|
|
1957
2058
|
toolUseId,
|
|
1958
2059
|
question,
|
|
1959
2060
|
options,
|
|
1960
|
-
|
|
2061
|
+
questions,
|
|
2062
|
+
createdAt: existingPending.createdAt
|
|
1961
2063
|
};
|
|
1962
2064
|
this.emit({ type: "question_request", request: updatedRequest });
|
|
1963
2065
|
console.log(`[SessionManager] Session ${sessionId}: AskUserQuestion updated (requestId=${existingRequestId})`);
|
|
@@ -1970,12 +2072,13 @@ var SessionManager = class {
|
|
|
1970
2072
|
toolUseId,
|
|
1971
2073
|
question,
|
|
1972
2074
|
options,
|
|
2075
|
+
questions,
|
|
1973
2076
|
createdAt: Date.now()
|
|
1974
2077
|
};
|
|
1975
2078
|
this.updateSessionStatus(sessionId, "waiting_question");
|
|
1976
2079
|
this.emit({ type: "question_request", request });
|
|
1977
2080
|
const answerPromise = new Promise((resolve) => {
|
|
1978
|
-
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 });
|
|
1979
2082
|
});
|
|
1980
2083
|
answerPromise.then(async (answer) => {
|
|
1981
2084
|
try {
|
|
@@ -3849,7 +3952,7 @@ async function getSessionHistory(projectPath, sessionId) {
|
|
|
3849
3952
|
if (err.code === "ENOENT") return null;
|
|
3850
3953
|
throw err;
|
|
3851
3954
|
});
|
|
3852
|
-
if (raw === null) return { ok:
|
|
3955
|
+
if (raw === null) return { ok: true, value: [] };
|
|
3853
3956
|
const lines = raw.split("\n").filter((l) => l.trim());
|
|
3854
3957
|
const events = [];
|
|
3855
3958
|
for (const line of lines) {
|
|
@@ -4624,18 +4727,25 @@ async function start(opts = {}) {
|
|
|
4624
4727
|
}
|
|
4625
4728
|
case "load_session_history": {
|
|
4626
4729
|
const historyResult = await getSessionHistory(event.projectPath, event.sessionId);
|
|
4627
|
-
|
|
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) {
|
|
4628
4738
|
wsBridge.send(ws, {
|
|
4629
4739
|
type: "error",
|
|
4630
4740
|
message: t("server.readHistoryFailed", { error: historyResult.error.message }),
|
|
4631
4741
|
code: "SESSION_HISTORY_ERROR",
|
|
4632
4742
|
sessionId: event.sessionId
|
|
4633
4743
|
});
|
|
4634
|
-
} else if (
|
|
4744
|
+
} else if (historyEvents.length > 0) {
|
|
4635
4745
|
wsBridge.send(ws, {
|
|
4636
4746
|
type: "session_history",
|
|
4637
4747
|
sessionId: event.sessionId,
|
|
4638
|
-
events:
|
|
4748
|
+
events: historyEvents
|
|
4639
4749
|
});
|
|
4640
4750
|
const activeSession = sessionManager.getActiveSessions().find((s) => s.id === event.sessionId);
|
|
4641
4751
|
const isStreaming = activeSession?.status === "running" || activeSession?.status === "waiting_approval";
|