@xcanwin/manyoyo 5.6.8 → 5.6.9

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.
@@ -16,6 +16,8 @@ const AGENT_PROMPT_TEMPLATE_MAP = {
16
16
  opencode: 'opencode run {prompt}'
17
17
  };
18
18
 
19
+ const CODEX_DANGEROUS_FLAG = '--dangerously-bypass-approvals-and-sandbox';
20
+
19
21
  function stripLeadingAssignments(commandText) {
20
22
  let rest = String(commandText || '').trim();
21
23
  const assignmentPattern = /^(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)(?:\s+|$)/;
@@ -75,8 +77,15 @@ function resolveAgentResumeArg(commandText) {
75
77
  }
76
78
 
77
79
  function resolveAgentPromptCommandTemplate(commandText) {
80
+ const normalizedCommand = String(commandText || '').trim();
78
81
  const program = resolveAgentProgram(commandText);
79
- return AGENT_PROMPT_TEMPLATE_MAP[program] || '';
82
+ const template = AGENT_PROMPT_TEMPLATE_MAP[program] || '';
83
+ if (program === 'codex' && template) {
84
+ if (normalizedCommand.includes(CODEX_DANGEROUS_FLAG)) {
85
+ return `codex exec ${CODEX_DANGEROUS_FLAG} --skip-git-repo-check {prompt}`;
86
+ }
87
+ }
88
+ return template;
80
89
  }
81
90
 
82
91
  function buildAgentResumeCommand(commandText) {
@@ -1079,6 +1079,12 @@ body.command-mode .msg.origin-agent .bubble {
1079
1079
  gap: 10px;
1080
1080
  }
1081
1081
 
1082
+ .composer-actions {
1083
+ display: flex;
1084
+ flex-direction: column;
1085
+ gap: 8px;
1086
+ }
1087
+
1082
1088
  #commandInput {
1083
1089
  width: 100%;
1084
1090
  min-height: 116px;
@@ -1098,6 +1104,10 @@ body.command-mode .msg.origin-agent .bubble {
1098
1104
  min-width: 92px;
1099
1105
  }
1100
1106
 
1107
+ #stopBtn {
1108
+ min-width: 92px;
1109
+ }
1110
+
1101
1111
  .composer-foot {
1102
1112
  margin-top: 8px;
1103
1113
  display: flex;
@@ -108,7 +108,10 @@
108
108
  </div>
109
109
  <div class="composer-inner">
110
110
  <textarea id="commandInput" placeholder="输入容器命令,例如: ls -la"></textarea>
111
- <button type="submit" id="sendBtn">发送</button>
111
+ <div class="composer-actions">
112
+ <button type="submit" id="sendBtn">发送</button>
113
+ <button type="button" id="stopBtn" class="danger-outline">停止</button>
114
+ </div>
112
115
  </div>
113
116
  <div class="composer-foot">
114
117
  <span id="composerHint">Enter 发送 · Shift/Alt + Enter 换行</span>
@@ -64,6 +64,13 @@
64
64
  sessionNodeMap: new Map(),
65
65
  sessionRenderMode: 'empty',
66
66
  messageRequestId: 0,
67
+ agentRun: {
68
+ active: false,
69
+ sessionName: '',
70
+ stopping: false,
71
+ controller: null,
72
+ traceMessageId: ''
73
+ },
67
74
  terminal: {
68
75
  term: null,
69
76
  fitAddon: null,
@@ -142,6 +149,7 @@
142
149
  const composerHint = document.getElementById('composerHint');
143
150
  const sendState = document.getElementById('sendState');
144
151
  const sendBtn = document.getElementById('sendBtn');
152
+ const stopBtn = document.getElementById('stopBtn');
145
153
  const refreshBtn = document.getElementById('refreshBtn');
146
154
  const removeBtn = document.getElementById('removeBtn');
147
155
  const removeAllBtn = document.getElementById('removeAllBtn');
@@ -185,6 +193,9 @@
185
193
  function roleName(role, message) {
186
194
  if (role === 'user') return '我';
187
195
  if (role === 'assistant') {
196
+ if (message && message.streamTrace) {
197
+ return 'AGENT 过程';
198
+ }
188
199
  if (message && message.mode === 'agent') {
189
200
  return 'AGENT 回复';
190
201
  }
@@ -357,7 +368,11 @@
357
368
 
358
369
  function resolveAgentPromptTemplate(commandText) {
359
370
  const program = resolveAgentProgram(commandText);
360
- return AGENT_PROMPT_TEMPLATE_MAP[program] || '';
371
+ const template = AGENT_PROMPT_TEMPLATE_MAP[program] || '';
372
+ if (program === 'codex' && String(commandText || '').includes('--dangerously-bypass-approvals-and-sandbox')) {
373
+ return 'codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check {prompt}';
374
+ }
375
+ return template;
361
376
  }
362
377
 
363
378
  function inferCreateAgentPromptCommand() {
@@ -541,6 +556,15 @@
541
556
  }) || null;
542
557
  }
543
558
 
559
+ function isAgentRunActiveForSession(sessionName) {
560
+ return Boolean(
561
+ state.agentRun
562
+ && state.agentRun.active
563
+ && state.agentRun.sessionName
564
+ && state.agentRun.sessionName === String(sessionName || '').trim()
565
+ );
566
+ }
567
+
544
568
  function isActiveSessionHistoryOnly() {
545
569
  const session = getActiveSession();
546
570
  return sessionStatusInfo(session && session.status).tone === 'history';
@@ -1187,11 +1211,15 @@
1187
1211
  scheduleTerminalFit(false);
1188
1212
  }
1189
1213
 
1214
+ const activeAgentRunning = isAgentRunActiveForSession(state.active);
1190
1215
  const busy = state.loadingSessions || state.loadingMessages || state.sending;
1191
1216
  refreshBtn.disabled = busy;
1192
1217
  removeBtn.disabled = !state.active || busy;
1193
1218
  removeAllBtn.disabled = !state.active || busy;
1194
1219
  sendBtn.disabled = !activityTab || !state.active || busy || (agentMode && !agentEnabled);
1220
+ if (stopBtn) {
1221
+ stopBtn.disabled = !activityTab || !agentMode || !activeAgentRunning || state.agentRun.stopping;
1222
+ }
1195
1223
  commandInput.disabled = !activityTab || !state.active || (agentMode && !agentEnabled);
1196
1224
  if (commandInput) {
1197
1225
  commandInput.placeholder = agentMode
@@ -1200,7 +1228,7 @@
1200
1228
  }
1201
1229
  if (composerHint) {
1202
1230
  composerHint.textContent = agentMode
1203
- ? 'Enter 发送提示词 · Shift/Alt + Enter 换行'
1231
+ ? 'Enter 发送提示词 · Shift/Alt + Enter 换行 · 执行中可停止'
1204
1232
  : 'Enter 发送 · Shift/Alt + Enter 换行';
1205
1233
  }
1206
1234
  if (openCreateBtn) {
@@ -1227,6 +1255,21 @@
1227
1255
  if (createCancelBtn) {
1228
1256
  createCancelBtn.disabled = state.createSubmitting;
1229
1257
  }
1258
+ if (sendState) {
1259
+ if (!state.active) {
1260
+ sendState.textContent = '未选择会话';
1261
+ sendState.classList.remove('is-active');
1262
+ } else if (activeAgentRunning && agentMode) {
1263
+ sendState.textContent = state.agentRun.stopping ? '正在停止 Agent…' : 'Agent 执行中';
1264
+ sendState.classList.add('is-active');
1265
+ } else if (busy) {
1266
+ sendState.textContent = '处理中';
1267
+ sendState.classList.add('is-active');
1268
+ } else {
1269
+ sendState.textContent = agentMode ? 'Agent 就绪' : '命令就绪';
1270
+ sendState.classList.remove('is-active');
1271
+ }
1272
+ }
1230
1273
  if (configModal) {
1231
1274
  configModal.hidden = !state.configModalOpen;
1232
1275
  }
@@ -1276,6 +1319,80 @@
1276
1319
  return data;
1277
1320
  }
1278
1321
 
1322
+ async function apiStream(url, options, handlers) {
1323
+ const requestOptions = Object.assign(
1324
+ { headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } },
1325
+ options || {}
1326
+ );
1327
+ const streamHandlers = handlers && typeof handlers === 'object' ? handlers : {};
1328
+ const response = await fetch(url, requestOptions);
1329
+ if (response.status === 401) {
1330
+ window.location.href = '/';
1331
+ throw new Error('未登录或登录已过期');
1332
+ }
1333
+ if (!response.ok) {
1334
+ let errorText = '请求失败';
1335
+ try {
1336
+ const data = await response.json();
1337
+ errorText = data && data.detail ? `${data.error || '请求失败'}: ${data.detail}` : (data.error || '请求失败');
1338
+ } catch (e) {
1339
+ errorText = '请求失败';
1340
+ }
1341
+ throw new Error(errorText);
1342
+ }
1343
+ if (!response.body || typeof response.body.getReader !== 'function') {
1344
+ throw new Error('当前浏览器不支持流式读取');
1345
+ }
1346
+
1347
+ const decoder = new window.TextDecoder();
1348
+ const reader = response.body.getReader();
1349
+ let pending = '';
1350
+
1351
+ while (true) {
1352
+ const result = await reader.read();
1353
+ if (result.done) {
1354
+ break;
1355
+ }
1356
+ pending += decoder.decode(result.value, { stream: true });
1357
+ const lines = pending.split('\n');
1358
+ pending = lines.pop() || '';
1359
+ lines.forEach(function (line) {
1360
+ const text = String(line || '').trim();
1361
+ if (!text) {
1362
+ return;
1363
+ }
1364
+ let payload = null;
1365
+ try {
1366
+ payload = JSON.parse(text);
1367
+ } catch (e) {
1368
+ payload = null;
1369
+ }
1370
+ if (!payload) {
1371
+ return;
1372
+ }
1373
+ if (typeof streamHandlers.onEvent === 'function') {
1374
+ streamHandlers.onEvent(payload);
1375
+ }
1376
+ });
1377
+ }
1378
+
1379
+ const rest = decoder.decode();
1380
+ if (rest) {
1381
+ pending += rest;
1382
+ }
1383
+ const finalText = String(pending || '').trim();
1384
+ if (finalText) {
1385
+ try {
1386
+ const payload = JSON.parse(finalText);
1387
+ if (typeof streamHandlers.onEvent === 'function') {
1388
+ streamHandlers.onEvent(payload);
1389
+ }
1390
+ } catch (e) {
1391
+ // ignore trailing non-json fragments
1392
+ }
1393
+ }
1394
+ }
1395
+
1279
1396
  async function fetchConfigSnapshot() {
1280
1397
  const snapshot = await api('/api/config');
1281
1398
  state.configSnapshot = snapshot;
@@ -1595,6 +1712,11 @@
1595
1712
  }
1596
1713
 
1597
1714
  function getMessageRenderKey(msg, index) {
1715
+ if (msg && msg.id && msg.streamTrace) {
1716
+ const content = msg.content ? String(msg.content) : '';
1717
+ const timestamp = msg.timestamp ? String(msg.timestamp) : '';
1718
+ return `id:${msg.id}|trace|${timestamp}|${content}`;
1719
+ }
1598
1720
  if (msg && msg.id) {
1599
1721
  return `id:${msg.id}`;
1600
1722
  }
@@ -1641,7 +1763,7 @@
1641
1763
  const bubble = document.createElement('div');
1642
1764
  bubble.className = 'bubble';
1643
1765
 
1644
- const shouldRenderMarkdown = Boolean(markdownRenderer && markdownRenderer.shouldRenderMessage(msg));
1766
+ const shouldRenderMarkdown = Boolean(!msg.streamTrace && markdownRenderer && markdownRenderer.shouldRenderMessage(msg));
1645
1767
  if (shouldRenderMarkdown) {
1646
1768
  const markdownNode = document.createElement('div');
1647
1769
  markdownNode.className = 'md-content';
@@ -1941,6 +2063,46 @@
1941
2063
  return -1;
1942
2064
  }
1943
2065
 
2066
+ function appendAgentTraceMessageLocal(sessionName) {
2067
+ const traceMessage = {
2068
+ id: createLocalMessageId('local-agent-trace'),
2069
+ role: 'assistant',
2070
+ content: '[执行过程]\n等待 Agent 启动…',
2071
+ timestamp: new Date().toISOString(),
2072
+ mode: 'agent',
2073
+ streamTrace: true
2074
+ };
2075
+ if (state.active === sessionName) {
2076
+ state.messages.push(traceMessage);
2077
+ }
2078
+ return traceMessage.id;
2079
+ }
2080
+
2081
+ function updateAgentTraceMessageLocal(sessionName, traceMessageId, content) {
2082
+ if (state.active !== sessionName) {
2083
+ return;
2084
+ }
2085
+ for (let i = state.messages.length - 1; i >= 0; i -= 1) {
2086
+ const message = state.messages[i];
2087
+ if (!message || String(message.id || '') !== String(traceMessageId || '')) {
2088
+ continue;
2089
+ }
2090
+ message.content = String(content || '');
2091
+ message.timestamp = new Date().toISOString();
2092
+ return;
2093
+ }
2094
+ }
2095
+
2096
+ function finalizeAgentRunState() {
2097
+ if (state.agentRun && state.agentRun.controller) {
2098
+ state.agentRun.controller = null;
2099
+ }
2100
+ state.agentRun.active = false;
2101
+ state.agentRun.stopping = false;
2102
+ state.agentRun.sessionName = '';
2103
+ state.agentRun.traceMessageId = '';
2104
+ }
2105
+
1944
2106
  function appendAssistantMessageLocal(sessionName, result, mode) {
1945
2107
  if (state.active !== sessionName) {
1946
2108
  return;
@@ -1957,6 +2119,109 @@
1957
2119
  });
1958
2120
  }
1959
2121
 
2122
+ async function sendAgentPromptStream(sessionName, inputText, pendingMessage) {
2123
+ const traceMessageId = appendAgentTraceMessageLocal(sessionName);
2124
+ const traceLines = ['[执行过程]', '等待 Agent 启动…'];
2125
+ let finalResult = null;
2126
+ let streamError = null;
2127
+
2128
+ state.agentRun.active = true;
2129
+ state.agentRun.sessionName = sessionName;
2130
+ state.agentRun.stopping = false;
2131
+ state.agentRun.controller = new window.AbortController();
2132
+ state.agentRun.traceMessageId = traceMessageId;
2133
+ renderMessages(state.messages, { stickToBottom: true });
2134
+ syncUi();
2135
+
2136
+ function pushTraceLine(text) {
2137
+ const line = String(text || '').trim();
2138
+ if (!line) {
2139
+ return;
2140
+ }
2141
+ if (traceLines[traceLines.length - 1] === line) {
2142
+ return;
2143
+ }
2144
+ traceLines.push(line);
2145
+ updateAgentTraceMessageLocal(sessionName, traceMessageId, traceLines.join('\n'));
2146
+ if (state.active === sessionName) {
2147
+ renderMessages(state.messages, { stickToBottom: true });
2148
+ }
2149
+ }
2150
+
2151
+ try {
2152
+ await apiStream('/api/sessions/' + encodeURIComponent(sessionName) + '/agent/stream', {
2153
+ method: 'POST',
2154
+ body: JSON.stringify({ prompt: inputText }),
2155
+ signal: state.agentRun.controller.signal
2156
+ }, {
2157
+ onEvent: function (event) {
2158
+ if (!event || typeof event !== 'object') {
2159
+ return;
2160
+ }
2161
+ if (event.type === 'meta') {
2162
+ const contextMode = String(event.contextMode || '').trim();
2163
+ const modeLabel = contextMode ? '上下文模式: ' + contextMode : '';
2164
+ if (modeLabel) {
2165
+ pushTraceLine(modeLabel);
2166
+ }
2167
+ if (event.resumeAttempted) {
2168
+ pushTraceLine(event.resumeSucceeded ? '会话恢复成功' : '会话恢复失败,已回退到历史注入');
2169
+ }
2170
+ return;
2171
+ }
2172
+ if (event.type === 'trace') {
2173
+ pushTraceLine(event.text || '');
2174
+ return;
2175
+ }
2176
+ if (event.type === 'result') {
2177
+ finalResult = event;
2178
+ if (event.interrupted) {
2179
+ pushTraceLine('[任务] 已停止');
2180
+ } else {
2181
+ pushTraceLine('[任务] 已完成');
2182
+ }
2183
+ return;
2184
+ }
2185
+ if (event.type === 'error') {
2186
+ streamError = new Error(event.error || 'Agent 执行失败');
2187
+ }
2188
+ }
2189
+ });
2190
+ } catch (e) {
2191
+ if (!(e && e.name === 'AbortError')) {
2192
+ streamError = e;
2193
+ }
2194
+ } finally {
2195
+ const pendingIndex = confirmPendingUserMessage(sessionName, pendingMessage.id);
2196
+ if (pendingIndex >= 0 && pendingIndex < state.messageRenderKeys.length) {
2197
+ if (pendingIndex < messagesNode.children.length) {
2198
+ const pendingRow = messagesNode.children[pendingIndex];
2199
+ if (pendingRow && pendingRow.classList.contains('pending')) {
2200
+ pendingRow.classList.remove('pending');
2201
+ }
2202
+ }
2203
+ }
2204
+ finalizeAgentRunState();
2205
+ syncUi();
2206
+ }
2207
+
2208
+ if (streamError) {
2209
+ throw streamError;
2210
+ }
2211
+ if (!finalResult) {
2212
+ throw new Error('Agent 流式响应未返回结果');
2213
+ }
2214
+
2215
+ appendAssistantMessageLocal(sessionName, finalResult, 'agent');
2216
+ if (state.active === sessionName) {
2217
+ renderMessages(state.messages, { stickToBottom: true });
2218
+ }
2219
+ bumpSessionMetaAfterSend(sessionName);
2220
+ refreshSessionsSilent({ preferredName: sessionName }).catch(function () {
2221
+ // 静默同步失败不打断当前交互
2222
+ });
2223
+ }
2224
+
1960
2225
  if (openConfigBtn) {
1961
2226
  openConfigBtn.addEventListener('click', function () {
1962
2227
  openConfigModal();
@@ -2092,32 +2357,31 @@
2092
2357
  try {
2093
2358
  commandInput.value = '';
2094
2359
  commandInput.focus();
2095
- const endpoint = mode === 'agent' ? '/agent' : '/run';
2096
- const runResult = await api('/api/sessions/' + encodeURIComponent(submitSession) + endpoint, {
2097
- method: 'POST',
2098
- body: JSON.stringify(
2099
- mode === 'agent'
2100
- ? { prompt: inputText }
2101
- : { command: inputText }
2102
- )
2103
- });
2104
- const pendingIndex = confirmPendingUserMessage(submitSession, pendingMessage.id);
2105
- if (pendingIndex >= 0 && pendingIndex < state.messageRenderKeys.length) {
2106
- if (pendingIndex < messagesNode.children.length) {
2107
- const pendingRow = messagesNode.children[pendingIndex];
2108
- if (pendingRow && pendingRow.classList.contains('pending')) {
2109
- pendingRow.classList.remove('pending');
2360
+ if (mode === 'agent') {
2361
+ await sendAgentPromptStream(submitSession, inputText, pendingMessage);
2362
+ } else {
2363
+ const runResult = await api('/api/sessions/' + encodeURIComponent(submitSession) + '/run', {
2364
+ method: 'POST',
2365
+ body: JSON.stringify({ command: inputText })
2366
+ });
2367
+ const pendingIndex = confirmPendingUserMessage(submitSession, pendingMessage.id);
2368
+ if (pendingIndex >= 0 && pendingIndex < state.messageRenderKeys.length) {
2369
+ if (pendingIndex < messagesNode.children.length) {
2370
+ const pendingRow = messagesNode.children[pendingIndex];
2371
+ if (pendingRow && pendingRow.classList.contains('pending')) {
2372
+ pendingRow.classList.remove('pending');
2373
+ }
2110
2374
  }
2111
2375
  }
2376
+ appendAssistantMessageLocal(submitSession, runResult, mode);
2377
+ if (state.active === submitSession) {
2378
+ renderMessages(state.messages, { stickToBottom: true });
2379
+ }
2380
+ bumpSessionMetaAfterSend(submitSession);
2381
+ refreshSessionsSilent({ preferredName: submitSession }).catch(function () {
2382
+ // 静默同步失败不打断当前交互
2383
+ });
2112
2384
  }
2113
- appendAssistantMessageLocal(submitSession, runResult, mode);
2114
- if (state.active === submitSession) {
2115
- renderMessages(state.messages, { stickToBottom: true });
2116
- }
2117
- bumpSessionMetaAfterSend(submitSession);
2118
- refreshSessionsSilent({ preferredName: submitSession }).catch(function () {
2119
- // 静默同步失败不打断当前交互
2120
- });
2121
2385
  } catch (e) {
2122
2386
  if (state.active === submitSession) {
2123
2387
  state.messages = state.messages.filter(function (message) {
@@ -2133,6 +2397,26 @@
2133
2397
  }
2134
2398
  });
2135
2399
 
2400
+ if (stopBtn) {
2401
+ stopBtn.addEventListener('click', async function () {
2402
+ if (!state.active || !isAgentRunActiveForSession(state.active) || state.agentRun.stopping) {
2403
+ return;
2404
+ }
2405
+ state.agentRun.stopping = true;
2406
+ syncUi();
2407
+ try {
2408
+ await api('/api/sessions/' + encodeURIComponent(state.active) + '/agent/stop', {
2409
+ method: 'POST',
2410
+ body: JSON.stringify({})
2411
+ });
2412
+ } catch (e) {
2413
+ alert(e.message);
2414
+ state.agentRun.stopping = false;
2415
+ syncUi();
2416
+ }
2417
+ });
2418
+ }
2419
+
2136
2420
  commandInput.addEventListener('keydown', function (event) {
2137
2421
  if (event.key !== 'Enter' || event.isComposing) {
2138
2422
  return;
package/lib/web/server.js CHANGED
@@ -321,6 +321,7 @@ function hasAgentConversationHistory(history) {
321
321
  for (const message of messages) {
322
322
  if (!message || typeof message !== 'object') continue;
323
323
  if (message.mode !== 'agent') continue;
324
+ if (message.streamTrace === true) continue;
324
325
  if (message.role === 'user' || message.role === 'assistant') {
325
326
  return true;
326
327
  }
@@ -339,7 +340,12 @@ function clipAgentContextMessageText(text) {
339
340
  function buildAgentPromptWithHistory(history, prompt) {
340
341
  const sessionHistory = history && Array.isArray(history.messages) ? history.messages : [];
341
342
  const relevantMessages = sessionHistory
342
- .filter(message => message && message.mode === 'agent' && (message.role === 'user' || message.role === 'assistant'))
343
+ .filter(message => (
344
+ message
345
+ && message.mode === 'agent'
346
+ && message.streamTrace !== true
347
+ && (message.role === 'user' || message.role === 'assistant')
348
+ ))
343
349
  .slice(-WEB_AGENT_CONTEXT_MAX_MESSAGES);
344
350
  if (!relevantMessages.length) {
345
351
  return String(prompt || '');
@@ -370,6 +376,198 @@ function buildAgentPromptWithHistory(history, prompt) {
370
376
  ].join('\n');
371
377
  }
372
378
 
379
+ function prepareCodexTraceDisplayLine(payload) {
380
+ if (!payload || typeof payload !== 'object') {
381
+ return '';
382
+ }
383
+
384
+ const eventType = typeof payload.type === 'string' ? payload.type : '';
385
+ const item = payload.item && typeof payload.item === 'object' && !Array.isArray(payload.item)
386
+ ? payload.item
387
+ : {};
388
+ const itemType = typeof item.type === 'string' ? item.type : '';
389
+ const text = pickFirstString(
390
+ item.title,
391
+ item.summary,
392
+ item.text,
393
+ item.name,
394
+ item.command,
395
+ payload.message,
396
+ payload.text
397
+ );
398
+ const toolName = pickFirstString(
399
+ item.name,
400
+ item.tool_name,
401
+ item.tool,
402
+ item.command
403
+ );
404
+ const commandText = pickFirstString(item.command);
405
+ const mcpServer = pickFirstString(item.server);
406
+ const mcpTool = pickFirstString(item.tool);
407
+ const itemStatus = pickFirstString(item.status);
408
+
409
+ function shortenText(value, maxChars = 140) {
410
+ const raw = clipText(stripAnsi(String(value || '')).replace(/\s+/g, ' ').trim(), maxChars);
411
+ return raw.trim();
412
+ }
413
+
414
+ function summarizeArguments(args) {
415
+ if (!args || typeof args !== 'object' || Array.isArray(args)) {
416
+ return '';
417
+ }
418
+ const parts = [];
419
+ for (const [key, value] of Object.entries(args)) {
420
+ if (value === undefined || value === null) continue;
421
+ if (typeof value === 'string') {
422
+ const textValue = value.trim();
423
+ if (!textValue) continue;
424
+ parts.push(`${key}=${shortenText(textValue, 80)}`);
425
+ continue;
426
+ }
427
+ if (typeof value === 'number' || typeof value === 'boolean') {
428
+ parts.push(`${key}=${String(value)}`);
429
+ }
430
+ }
431
+ return parts.slice(0, 3).join(', ');
432
+ }
433
+
434
+ if (eventType === 'thread.started') {
435
+ return '[会话] Codex 已开始处理';
436
+ }
437
+ if (eventType === 'thread.completed') {
438
+ return '[会话] Codex 已完成当前任务';
439
+ }
440
+ if (eventType === 'turn.started') {
441
+ return '[回合] 开始生成响应';
442
+ }
443
+ if (eventType === 'turn.completed') {
444
+ return '[回合] 响应完成';
445
+ }
446
+ if (eventType === 'item.started') {
447
+ if (itemType === 'tool_call') {
448
+ return `[工具开始] ${toolName || 'tool_call'}`;
449
+ }
450
+ if (itemType === 'command_execution') {
451
+ return `[命令开始] ${commandText || 'command_execution'}`;
452
+ }
453
+ if (itemType === 'mcp_tool_call') {
454
+ const summary = summarizeArguments(item.arguments);
455
+ return summary
456
+ ? `[MCP开始] ${mcpServer || 'mcp'}.${mcpTool || 'tool'} (${summary})`
457
+ : `[MCP开始] ${mcpServer || 'mcp'}.${mcpTool || 'tool'}`;
458
+ }
459
+ if (itemType === 'reasoning') {
460
+ return text ? `[状态] ${text}` : '[状态] Codex 正在分析';
461
+ }
462
+ if (itemType === 'agent_message') {
463
+ return text ? `[说明] ${text}` : '[回复] 正在生成最终答复';
464
+ }
465
+ return text ? `[事件开始] ${text}` : `[事件开始] ${itemType || eventType}`;
466
+ }
467
+ if (eventType === 'item.completed') {
468
+ if (itemType === 'tool_call') {
469
+ return `[工具完成] ${toolName || 'tool_call'}`;
470
+ }
471
+ if (itemType === 'command_execution') {
472
+ const suffix = itemStatus || (typeof item.exit_code === 'number' ? `exit=${item.exit_code}` : 'completed');
473
+ return `[命令完成] ${commandText || 'command_execution'} (${suffix})`;
474
+ }
475
+ if (itemType === 'mcp_tool_call') {
476
+ const summary = summarizeArguments(item.arguments);
477
+ return summary
478
+ ? `[MCP完成] ${mcpServer || 'mcp'}.${mcpTool || 'tool'} (${summary})`
479
+ : `[MCP完成] ${mcpServer || 'mcp'}.${mcpTool || 'tool'}`;
480
+ }
481
+ if (itemType === 'reasoning') {
482
+ return text ? `[状态] ${text}` : '';
483
+ }
484
+ if (itemType === 'agent_message') {
485
+ return text ? `[说明] ${text}` : '[回复] 已生成';
486
+ }
487
+ return text ? `[事件完成] ${text}` : `[事件完成] ${itemType || eventType}`;
488
+ }
489
+ if (eventType === 'error') {
490
+ return text ? `[错误] ${text}` : '[错误] Codex 返回了错误事件';
491
+ }
492
+
493
+ return `[事件] ${eventType}`;
494
+ }
495
+
496
+ async function prepareWebAgentExecution(ctx, state, containerName, prompt) {
497
+ const history = loadWebSessionHistory(state.webHistoryDir, containerName);
498
+ const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
499
+ if (normalizedTemplate !== history.agentPromptCommand) {
500
+ history.agentPromptCommand = normalizedTemplate;
501
+ saveWebSessionHistory(state.webHistoryDir, containerName, history);
502
+ }
503
+ if (!isAgentPromptCommandEnabled(history.agentPromptCommand)) {
504
+ throw new Error('当前会话未配置 agentPromptCommand');
505
+ }
506
+
507
+ await ensureWebContainer(ctx, state, containerName);
508
+ const agentMeta = getAgentRuntimeMeta(history);
509
+ const hasPriorConversation = hasAgentConversationHistory(history);
510
+ let resumeAttempted = false;
511
+ let resumeSucceeded = false;
512
+ let resumeError = '';
513
+
514
+ if (hasPriorConversation && agentMeta.resumeSupported && agentMeta.resumeCommand) {
515
+ resumeAttempted = true;
516
+ const resumeResult = await execCommandInWebContainer(ctx, containerName, agentMeta.resumeCommand);
517
+ if (resumeResult.exitCode === 0) {
518
+ resumeSucceeded = true;
519
+ } else {
520
+ resumeError = clipText(String(resumeResult.output || '(无输出)'), 1200);
521
+ }
522
+ }
523
+
524
+ const effectivePrompt = resumeSucceeded
525
+ ? prompt
526
+ : buildAgentPromptWithHistory(history, prompt);
527
+ const command = buildWebAgentExecCommand(history.agentPromptCommand, effectivePrompt, agentMeta.agentProgram);
528
+ const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
529
+
530
+ return {
531
+ history,
532
+ agentMeta,
533
+ command,
534
+ contextMode,
535
+ resumeAttempted,
536
+ resumeSucceeded,
537
+ resumeError
538
+ };
539
+ }
540
+
541
+ function finalizeWebAgentExecution(state, containerName, history, agentMeta, meta, result) {
542
+ appendWebSessionMessage(state.webHistoryDir, containerName, 'assistant', result.output, {
543
+ exitCode: result.exitCode,
544
+ mode: 'agent',
545
+ contextMode: meta.contextMode,
546
+ resumeAttempted: meta.resumeAttempted,
547
+ resumeSucceeded: meta.resumeSucceeded,
548
+ interrupted: result.interrupted === true
549
+ });
550
+ patchWebSessionAgentState(state.webHistoryDir, containerName, {
551
+ agentProgram: agentMeta.agentProgram,
552
+ resumeSupported: agentMeta.resumeSupported,
553
+ lastResumeAt: meta.resumeAttempted ? new Date().toISOString() : history.lastResumeAt || null,
554
+ lastResumeOk: meta.resumeAttempted ? meta.resumeSucceeded : history.lastResumeOk,
555
+ lastResumeError: meta.resumeAttempted ? (meta.resumeSucceeded ? '' : meta.resumeError) : history.lastResumeError || ''
556
+ });
557
+ }
558
+
559
+ function appendWebAgentTraceMessage(webHistoryDir, containerName, content, extra = {}) {
560
+ const text = String(content || '').trim();
561
+ if (!text) {
562
+ return;
563
+ }
564
+ appendWebSessionMessage(webHistoryDir, containerName, 'assistant', text, {
565
+ mode: 'agent',
566
+ streamTrace: true,
567
+ ...extra
568
+ });
569
+ }
570
+
373
571
  function secureStringEqual(a, b) {
374
572
  const aStr = String(a || '');
375
573
  const bStr = String(b || '');
@@ -1177,6 +1375,145 @@ async function execCommandInWebContainer(ctx, containerName, command) {
1177
1375
  });
1178
1376
  }
1179
1377
 
1378
+ async function execAgentInWebContainerStream(ctx, state, containerName, command, options = {}) {
1379
+ const opts = options && typeof options === 'object' ? options : {};
1380
+ const agentProgram = typeof opts.agentProgram === 'string' ? opts.agentProgram : '';
1381
+ const onEvent = typeof opts.onEvent === 'function' ? opts.onEvent : () => {};
1382
+ const process = spawn(
1383
+ ctx.dockerCmd,
1384
+ ['exec', containerName, '/bin/bash', '-lc', command],
1385
+ { stdio: ['ignore', 'pipe', 'pipe'] }
1386
+ );
1387
+
1388
+ const runState = {
1389
+ containerName,
1390
+ process,
1391
+ command,
1392
+ startedAt: new Date().toISOString(),
1393
+ stopping: false
1394
+ };
1395
+ state.agentRuns.set(containerName, runState);
1396
+
1397
+ return await new Promise((resolve, reject) => {
1398
+ const MAX_RAW_OUTPUT_CHARS = 32 * 1024 * 1024;
1399
+ let stdoutOutput = '';
1400
+ let stderrOutput = '';
1401
+ let stdoutTruncated = false;
1402
+ let stderrTruncated = false;
1403
+ let stdoutPending = '';
1404
+ let stderrPending = '';
1405
+ function appendChunk(chunk, target) {
1406
+ if (!chunk) return;
1407
+ const text = chunk.toString('utf-8');
1408
+ if (!text) return;
1409
+ if (target.value.length >= MAX_RAW_OUTPUT_CHARS) {
1410
+ target.truncated = true;
1411
+ return;
1412
+ }
1413
+ const remain = MAX_RAW_OUTPUT_CHARS - target.value.length;
1414
+ if (text.length > remain) {
1415
+ target.value += text.slice(0, remain);
1416
+ target.truncated = true;
1417
+ return;
1418
+ }
1419
+ target.value += text;
1420
+ }
1421
+
1422
+ function emitStdoutTraceLine(line) {
1423
+ const rawLine = String(line || '').trim();
1424
+ if (!rawLine) {
1425
+ return;
1426
+ }
1427
+ if (agentProgram === 'codex') {
1428
+ let payload = null;
1429
+ try {
1430
+ payload = JSON.parse(rawLine);
1431
+ } catch (e) {
1432
+ payload = null;
1433
+ }
1434
+ if (payload) {
1435
+ const display = prepareCodexTraceDisplayLine(payload);
1436
+ if (display) {
1437
+ onEvent({ type: 'trace', stream: 'stdout', text: display });
1438
+ }
1439
+ return;
1440
+ }
1441
+ if (/^OpenAI Codex\b/.test(rawLine) || /^tokens used\b/i.test(rawLine)) {
1442
+ return;
1443
+ }
1444
+ }
1445
+ onEvent({ type: 'trace', stream: 'stdout', text: rawLine });
1446
+ }
1447
+
1448
+ function emitStderrTraceLine(line) {
1449
+ const rawLine = String(line || '').trim();
1450
+ if (!rawLine) {
1451
+ return;
1452
+ }
1453
+ onEvent({ type: 'trace', stream: 'stderr', text: `[stderr] ${rawLine}` });
1454
+ }
1455
+
1456
+ function drainLines(text, carry, handleLine) {
1457
+ let pending = carry + String(text || '');
1458
+ let newlineIndex = pending.indexOf('\n');
1459
+ while (newlineIndex !== -1) {
1460
+ const line = pending.slice(0, newlineIndex).replace(/\r$/, '');
1461
+ handleLine(line);
1462
+ pending = pending.slice(newlineIndex + 1);
1463
+ newlineIndex = pending.indexOf('\n');
1464
+ }
1465
+ return pending;
1466
+ }
1467
+
1468
+ process.stdout.on('data', chunk => {
1469
+ appendChunk(chunk, {
1470
+ get value() { return stdoutOutput; },
1471
+ set value(nextValue) { stdoutOutput = nextValue; },
1472
+ get truncated() { return stdoutTruncated; },
1473
+ set truncated(nextValue) { stdoutTruncated = nextValue; }
1474
+ });
1475
+ stdoutPending = drainLines(chunk.toString('utf-8'), stdoutPending, emitStdoutTraceLine);
1476
+ });
1477
+ process.stderr.on('data', chunk => {
1478
+ appendChunk(chunk, {
1479
+ get value() { return stderrOutput; },
1480
+ set value(nextValue) { stderrOutput = nextValue; },
1481
+ get truncated() { return stderrTruncated; },
1482
+ set truncated(nextValue) { stderrTruncated = nextValue; }
1483
+ });
1484
+ stderrPending = drainLines(chunk.toString('utf-8'), stderrPending, emitStderrTraceLine);
1485
+ });
1486
+
1487
+ process.on('error', error => {
1488
+ state.agentRuns.delete(containerName);
1489
+ reject(error);
1490
+ });
1491
+ process.on('close', code => {
1492
+ state.agentRuns.delete(containerName);
1493
+ if (stdoutPending) {
1494
+ emitStdoutTraceLine(stdoutPending);
1495
+ stdoutPending = '';
1496
+ }
1497
+ if (stderrPending) {
1498
+ emitStderrTraceLine(stderrPending);
1499
+ stderrPending = '';
1500
+ }
1501
+ const exitCode = typeof code === 'number' ? code : 1;
1502
+ const clippedStdout = stdoutTruncated ? `${stdoutOutput}\n...[stdout-truncated]` : stdoutOutput;
1503
+ const clippedStderr = stderrTruncated ? `${stderrOutput}\n...[stderr-truncated]` : stderrOutput;
1504
+ const clippedRaw = `${clippedStdout}${clippedStdout && clippedStderr ? '\n' : ''}${clippedStderr}`;
1505
+ const extractedJsonAgentMessage = extractAgentMessageFromCodexJsonl(clippedStdout);
1506
+ const cleanOutputSource = extractedJsonAgentMessage || clippedRaw;
1507
+ const output = clipText(stripAnsi(cleanOutputSource).trim() || '(无输出)');
1508
+ resolve({
1509
+ exitCode,
1510
+ output,
1511
+ interrupted: exitCode !== 0 && runState.stopping === true
1512
+ });
1513
+ });
1514
+ });
1515
+ }
1516
+
1180
1517
  function readRequestBody(req) {
1181
1518
  return new Promise((resolve, reject) => {
1182
1519
  let body = '';
@@ -1213,6 +1550,24 @@ function sendJson(res, statusCode, payload, extraHeaders = {}) {
1213
1550
  res.end(JSON.stringify(payload));
1214
1551
  }
1215
1552
 
1553
+ function sendNdjson(res, payload) {
1554
+ res.write(`${JSON.stringify(payload)}\n`);
1555
+ }
1556
+
1557
+ function stopWebAgentRun(state, containerName) {
1558
+ const runState = state.agentRuns.get(containerName);
1559
+ if (!runState || !runState.process || runState.process.killed) {
1560
+ return false;
1561
+ }
1562
+ runState.stopping = true;
1563
+ try {
1564
+ runState.process.kill('SIGTERM');
1565
+ } catch (e) {
1566
+ return false;
1567
+ }
1568
+ return true;
1569
+ }
1570
+
1216
1571
  function sendHtml(res, statusCode, html, extraHeaders = {}) {
1217
1572
  res.writeHead(statusCode, {
1218
1573
  'Content-Type': 'text/html; charset=utf-8',
@@ -1807,72 +2162,156 @@ async function handleWebApi(req, res, pathname, ctx, state) {
1807
2162
  return;
1808
2163
  }
1809
2164
 
1810
- const history = loadWebSessionHistory(state.webHistoryDir, containerName);
1811
- const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
1812
- if (normalizedTemplate !== history.agentPromptCommand) {
1813
- history.agentPromptCommand = normalizedTemplate;
1814
- saveWebSessionHistory(state.webHistoryDir, containerName, history);
1815
- }
1816
- if (!isAgentPromptCommandEnabled(history.agentPromptCommand)) {
1817
- sendJson(res, 400, { error: '当前会话未配置 agentPromptCommand' });
2165
+ let prepared = null;
2166
+ try {
2167
+ prepared = await prepareWebAgentExecution(ctx, state, containerName, prompt);
2168
+ } catch (e) {
2169
+ sendJson(res, 400, { error: e && e.message ? e.message : 'Agent 执行准备失败' });
1818
2170
  return;
1819
2171
  }
1820
2172
 
1821
- await ensureWebContainer(ctx, state, containerName);
1822
- const agentMeta = getAgentRuntimeMeta(history);
1823
- const hasPriorConversation = hasAgentConversationHistory(history);
1824
- let resumeAttempted = false;
1825
- let resumeSucceeded = false;
1826
- let resumeError = '';
1827
- if (hasPriorConversation && agentMeta.resumeSupported && agentMeta.resumeCommand) {
1828
- resumeAttempted = true;
1829
- const resumeResult = await execCommandInWebContainer(ctx, containerName, agentMeta.resumeCommand);
1830
- if (resumeResult.exitCode === 0) {
1831
- resumeSucceeded = true;
1832
- } else {
1833
- resumeError = clipText(String(resumeResult.output || '(无输出)'), 1200);
1834
- }
1835
- }
1836
-
1837
- const effectivePrompt = resumeSucceeded
1838
- ? prompt
1839
- : buildAgentPromptWithHistory(history, prompt);
1840
- const command = buildWebAgentExecCommand(history.agentPromptCommand, effectivePrompt, agentMeta.agentProgram);
1841
- const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
2173
+ const { history, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
1842
2174
  appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, {
1843
2175
  mode: 'agent',
1844
2176
  contextMode
1845
2177
  });
1846
2178
  const result = await execCommandInWebContainer(ctx, containerName, command);
1847
- appendWebSessionMessage(
1848
- state.webHistoryDir,
1849
- containerName,
1850
- 'assistant',
1851
- result.output,
1852
- {
1853
- exitCode: result.exitCode,
1854
- mode: 'agent',
1855
- contextMode,
1856
- resumeAttempted,
1857
- resumeSucceeded
1858
- }
1859
- );
1860
- patchWebSessionAgentState(state.webHistoryDir, containerName, {
1861
- agentProgram: agentMeta.agentProgram,
1862
- resumeSupported: agentMeta.resumeSupported,
1863
- lastResumeAt: resumeAttempted ? new Date().toISOString() : history.lastResumeAt || null,
1864
- lastResumeOk: resumeAttempted ? resumeSucceeded : history.lastResumeOk,
1865
- lastResumeError: resumeAttempted ? (resumeSucceeded ? '' : resumeError) : history.lastResumeError || ''
1866
- });
2179
+ finalizeWebAgentExecution(state, containerName, history, agentMeta, {
2180
+ contextMode,
2181
+ resumeAttempted,
2182
+ resumeSucceeded,
2183
+ resumeError
2184
+ }, result);
1867
2185
  sendJson(res, 200, {
1868
2186
  exitCode: result.exitCode,
1869
2187
  output: result.output,
1870
2188
  contextMode,
1871
2189
  resumeAttempted,
1872
- resumeSucceeded
2190
+ resumeSucceeded,
2191
+ interrupted: result.interrupted === true
1873
2192
  });
1874
2193
  }
1875
2194
  },
2195
+ {
2196
+ method: 'POST',
2197
+ match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent\/stream$/),
2198
+ handler: async match => {
2199
+ const containerName = getValidSessionName(ctx, res, match[1]);
2200
+ if (!containerName) {
2201
+ return;
2202
+ }
2203
+
2204
+ const payload = await readJsonBody(req);
2205
+ const prompt = (payload.prompt || '').trim();
2206
+ if (!prompt) {
2207
+ sendJson(res, 400, { error: 'prompt 不能为空' });
2208
+ return;
2209
+ }
2210
+ if (state.agentRuns.has(containerName)) {
2211
+ sendJson(res, 409, { error: '当前会话已有运行中的 agent 任务' });
2212
+ return;
2213
+ }
2214
+
2215
+ let prepared = null;
2216
+ try {
2217
+ prepared = await prepareWebAgentExecution(ctx, state, containerName, prompt);
2218
+ } catch (e) {
2219
+ sendJson(res, 400, { error: e && e.message ? e.message : 'Agent 执行准备失败' });
2220
+ return;
2221
+ }
2222
+
2223
+ const { history, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
2224
+ const traceLines = ['[执行过程]'];
2225
+ appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, {
2226
+ mode: 'agent',
2227
+ contextMode
2228
+ });
2229
+
2230
+ res.writeHead(200, {
2231
+ 'Content-Type': 'application/x-ndjson; charset=utf-8',
2232
+ 'Cache-Control': 'no-store',
2233
+ 'X-Accel-Buffering': 'no'
2234
+ });
2235
+ sendNdjson(res, {
2236
+ type: 'meta',
2237
+ containerName,
2238
+ contextMode,
2239
+ resumeAttempted,
2240
+ resumeSucceeded,
2241
+ agentProgram: agentMeta.agentProgram
2242
+ });
2243
+ if (contextMode) {
2244
+ traceLines.push(`上下文模式: ${contextMode}`);
2245
+ }
2246
+ if (resumeAttempted) {
2247
+ traceLines.push(resumeSucceeded ? '会话恢复成功' : '会话恢复失败,已回退到历史注入');
2248
+ }
2249
+
2250
+ try {
2251
+ const result = await execAgentInWebContainerStream(ctx, state, containerName, command, {
2252
+ agentProgram: agentMeta.agentProgram,
2253
+ onEvent: event => {
2254
+ if (event && event.type === 'trace' && event.text) {
2255
+ traceLines.push(String(event.text));
2256
+ }
2257
+ sendNdjson(res, event);
2258
+ }
2259
+ });
2260
+ traceLines.push(result.interrupted === true ? '[任务] 已停止' : '[任务] 已完成');
2261
+ appendWebAgentTraceMessage(state.webHistoryDir, containerName, traceLines.join('\n'), {
2262
+ contextMode,
2263
+ resumeAttempted,
2264
+ resumeSucceeded,
2265
+ interrupted: result.interrupted === true
2266
+ });
2267
+ finalizeWebAgentExecution(state, containerName, history, agentMeta, {
2268
+ contextMode,
2269
+ resumeAttempted,
2270
+ resumeSucceeded,
2271
+ resumeError
2272
+ }, result);
2273
+ sendNdjson(res, {
2274
+ type: 'result',
2275
+ exitCode: result.exitCode,
2276
+ output: result.output,
2277
+ contextMode,
2278
+ resumeAttempted,
2279
+ resumeSucceeded,
2280
+ interrupted: result.interrupted === true
2281
+ });
2282
+ } catch (e) {
2283
+ traceLines.push(`[错误] ${e && e.message ? e.message : 'Agent 执行失败'}`);
2284
+ appendWebAgentTraceMessage(state.webHistoryDir, containerName, traceLines.join('\n'), {
2285
+ contextMode,
2286
+ resumeAttempted,
2287
+ resumeSucceeded,
2288
+ interrupted: true
2289
+ });
2290
+ sendNdjson(res, {
2291
+ type: 'error',
2292
+ error: e && e.message ? e.message : 'Agent 执行失败'
2293
+ });
2294
+ } finally {
2295
+ res.end();
2296
+ }
2297
+ }
2298
+ },
2299
+ {
2300
+ method: 'POST',
2301
+ match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent\/stop$/),
2302
+ handler: async match => {
2303
+ const containerName = getValidSessionName(ctx, res, match[1]);
2304
+ if (!containerName) {
2305
+ return;
2306
+ }
2307
+ const stopped = stopWebAgentRun(state, containerName);
2308
+ if (!stopped) {
2309
+ sendJson(res, 404, { error: '当前会话没有运行中的 agent 任务' });
2310
+ return;
2311
+ }
2312
+ sendJson(res, 200, { ok: true, stopping: true });
2313
+ }
2314
+ },
1876
2315
  {
1877
2316
  method: 'POST',
1878
2317
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/remove$/),
@@ -1971,7 +2410,8 @@ async function startWebServer(options) {
1971
2410
  webHistoryDir: options.webHistoryDir || path.join(os.homedir(), '.manyoyo', 'web-history'),
1972
2411
  webConfigPath: options.webConfigPath || getDefaultWebConfigPath(),
1973
2412
  authSessions: new Map(),
1974
- terminalSessions: new Map()
2413
+ terminalSessions: new Map(),
2414
+ agentRuns: new Map()
1975
2415
  };
1976
2416
 
1977
2417
  ensureWebHistoryDir(state.webHistoryDir);
@@ -2197,6 +2637,13 @@ async function startWebServer(options) {
2197
2637
  }
2198
2638
  }
2199
2639
  state.terminalSessions.clear();
2640
+ for (const runState of state.agentRuns.values()) {
2641
+ const child = runState && runState.process;
2642
+ if (child && !child.killed) {
2643
+ try { child.kill('SIGTERM'); } catch (e) {}
2644
+ }
2645
+ }
2646
+ state.agentRuns.clear();
2200
2647
 
2201
2648
  const closeHttp = () => {
2202
2649
  if (!server.listening) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.6.8",
3
+ "version": "5.6.9",
4
4
  "imageVersion": "1.9.0-common",
5
5
  "playwrightCliVersion": "0.1.1",
6
6
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",