@xcanwin/manyoyo 7.0.16 → 7.0.18

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/lib/web/server.js CHANGED
@@ -462,10 +462,9 @@ function removeContainerIdempotent(ctx, containerName) {
462
462
  }
463
463
  }
464
464
 
465
- function removeAllAgentHistoryArtifacts(webHistoryDir, containerName, agentIds) {
466
- const eventStore = new FileEventStore(webHistoryDir);
465
+ function removeAllAgentHistoryArtifacts(state, containerName, agentIds) {
467
466
  (Array.isArray(agentIds) ? agentIds : []).forEach(agentId => {
468
- eventStore.remove(buildWebSessionKey(containerName, agentId));
467
+ state.eventStore.remove(buildWebSessionKey(containerName, agentId));
469
468
  });
470
469
  }
471
470
 
@@ -623,29 +622,57 @@ function appendWebSessionMessage(webHistoryDir, sessionRefOrContainerName, role,
623
622
  return message;
624
623
  }
625
624
 
626
- function appendWebSessionControlEvent(webHistoryDir, sessionRef, type, data = {}) {
627
- const history = loadWebSessionHistory(webHistoryDir, sessionRef.containerName);
628
- const agentSession = getWebAgentSession(history, sessionRef.agentId, { create: true, includeArchived: true });
629
- const events = Array.isArray(agentSession.events) ? agentSession.events : [];
630
- const lastEvent = events.length ? events[events.length - 1] : null;
625
+ // 流式过程中高频出现的"透传"控制事件类型(见 resolveWebStreamControlEventType):
626
+ // 每个 content_delta/trace 事件都会各自触发一次,量随回合长度线性增长。会话
627
+ // 生命周期事件(session.ready/stopping/process.exited 等)不在其中,必须每次
628
+ // 都立即落盘。
629
+ const NOISY_CONTROL_EVENT_TYPES = new Set(['process.stdout', 'agent.turn.delta', 'agent.tool.observed']);
630
+ const LIVE_HISTORY_FLUSH_INTERVAL_MS = 400;
631
+
632
+ // 中间态历史落盘节流:同一 aggregateId 在节流窗口内只允许成功落盘一次,供
633
+ // appendWebSessionControlEvent(events 镜像)与 /agent/stream 路由里的
634
+ // patchWebSessionMessage(trace/content_delta 消息内容)共用同一个"落盘配额",
635
+ // 避免同一个事件被两条独立写路径各自整份重写一次 demo.json。节流只影响"实时
636
+ // 进度对轮询标签页可见的时机",不影响最终数据——两条写路径在回合结束/出错时
637
+ // 都还有一次不受节流影响的完整落盘。
638
+ function shouldFlushLiveHistory(state, aggregateId) {
639
+ const now = Date.now();
640
+ const lastFlushAt = state._liveHistoryFlushThrottle.get(aggregateId) || 0;
641
+ if (now - lastFlushAt < LIVE_HISTORY_FLUSH_INTERVAL_MS) {
642
+ return false;
643
+ }
644
+ state._liveHistoryFlushThrottle.set(aggregateId, now);
645
+ return true;
646
+ }
647
+
648
+ function appendWebSessionControlEvent(state, sessionRef, type, data = {}) {
649
+ const aggregateId = buildWebSessionKey(sessionRef.containerName, sessionRef.agentId);
650
+ // seq 必须以 state.eventStore 的内存缓存为准(而不是下面 agentSession.events
651
+ // 这份可能因落盘节流而滞后的历史 JSON 副本),否则节流生效时算出来的 seq
652
+ // 会与 eventStore 实际的 lastSeq 对不上,append() 会因"seq 必须连续递增"报错。
631
653
  const event = createControlEvent({
632
654
  type,
633
- aggregateId: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
634
- seq: lastEvent ? lastEvent.seq + 1 : 1,
655
+ aggregateId,
656
+ seq: state.eventStore.getNextSeq(aggregateId),
635
657
  data
636
658
  });
637
- const eventStore = new FileEventStore(webHistoryDir);
638
- eventStore.append(event);
659
+ state.eventStore.append(event);
660
+
661
+ const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
662
+ const agentSession = getWebAgentSession(history, sessionRef.agentId, { create: true, includeArchived: true });
663
+ const events = Array.isArray(agentSession.events) ? agentSession.events : [];
639
664
  agentSession.events = [...events, event].slice(-WEB_HISTORY_MAX_MESSAGES);
640
665
  agentSession.updatedAt = event.timestamp;
641
666
  history.updatedAt = event.timestamp;
642
- saveWebSessionHistory(webHistoryDir, sessionRef.containerName, history);
667
+ if (!NOISY_CONTROL_EVENT_TYPES.has(type) || shouldFlushLiveHistory(state, aggregateId)) {
668
+ saveWebSessionHistory(state.webHistoryDir, sessionRef.containerName, history);
669
+ }
643
670
  return event;
644
671
  }
645
672
 
646
- function loadWebSessionControlEvents(webHistoryDir, sessionRef, fallbackEvents = []) {
673
+ function loadWebSessionControlEvents(state, sessionRef, fallbackEvents = []) {
647
674
  try {
648
- const events = new FileEventStore(webHistoryDir).read(
675
+ const events = state.eventStore.read(
649
676
  buildWebSessionKey(sessionRef.containerName, sessionRef.agentId)
650
677
  );
651
678
  if (events.length) {
@@ -2069,7 +2096,7 @@ function prepareCodexTraceEvent(payload) {
2069
2096
  async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
2070
2097
  const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
2071
2098
  const agentSession = getWebAgentSession(history, sessionRef.agentId, { create: true, includeArchived: true });
2072
- const containerMap = listWebManyoyoContainers(ctx);
2099
+ const containerMap = listWebManyoyoContainers(ctx, state);
2073
2100
  const containerInfo = containerMap[sessionRef.containerName] || {};
2074
2101
  const normalizedContainerTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
2075
2102
  if (normalizedContainerTemplate !== history.agentPromptCommand) {
@@ -3009,7 +3036,7 @@ function buildCreateRuntime(ctx, state, payload) {
3009
3036
  }
3010
3037
 
3011
3038
  function resolveUniqueContainerName(ctx, state, baseName) {
3012
- const containerMap = listWebManyoyoContainers(ctx);
3039
+ const containerMap = listWebManyoyoContainers(ctx, state);
3013
3040
  const historyNames = listWebHistorySessionNames(state.webHistoryDir, ctx.isValidContainerName);
3014
3041
  const taken = new Set([...Object.keys(containerMap), ...historyNames]);
3015
3042
  // 基于历史最大编号 +1 命名,即使较小编号的副本被删除也不回收其编号,
@@ -3090,7 +3117,7 @@ async function createClonedContainer(ctx, state, sourceContainerName, requestedN
3090
3117
  let finalName = '';
3091
3118
  if (requestedName) {
3092
3119
  validateContainerNameStrict(requestedName);
3093
- const containerMap = listWebManyoyoContainers(ctx);
3120
+ const containerMap = listWebManyoyoContainers(ctx, state);
3094
3121
  const historyNames = listWebHistorySessionNames(state.webHistoryDir, ctx.isValidContainerName);
3095
3122
  const taken = new Set([...Object.keys(containerMap), ...historyNames]);
3096
3123
  if (taken.has(requestedName)) {
@@ -3154,7 +3181,20 @@ function estimateStartTimeFromStatus(status) {
3154
3181
  return null;
3155
3182
  }
3156
3183
 
3157
- function listWebManyoyoContainers(ctx) {
3184
+ // 同一进程内短 TTL 缓存:GET /api/sessions、/detail 等接口各自独立调用这个函数,
3185
+ // 容器一多时 docker ps + 批量 inspect 本身就不便宜,多标签页轮询会让它们在
3186
+ // 短时间内反复触发。2 秒内的重复调用直接复用上一次结果,不再次触碰
3187
+ // ctx.dockerExecArgs(同步 spawnSync,会独占事件循环)。
3188
+ const CONTAINER_LIST_CACHE_TTL_MS = 2000;
3189
+
3190
+ function listWebManyoyoContainers(ctx, state) {
3191
+ if (state && state._containerListCache) {
3192
+ const { timestamp, data } = state._containerListCache;
3193
+ if (Date.now() - timestamp < CONTAINER_LIST_CACHE_TTL_MS) {
3194
+ return data;
3195
+ }
3196
+ }
3197
+
3158
3198
  const output = ctx.dockerExecArgs(
3159
3199
  ['ps', '-a', '--format', '{{.Names}}\t{{.Status}}\t{{.Image}}'],
3160
3200
  { ignoreError: true }
@@ -3162,6 +3202,9 @@ function listWebManyoyoContainers(ctx) {
3162
3202
 
3163
3203
  const map = {};
3164
3204
  if (!output.trim()) {
3205
+ if (state) {
3206
+ state._containerListCache = { timestamp: Date.now(), data: map };
3207
+ }
3165
3208
  return map;
3166
3209
  }
3167
3210
 
@@ -3218,6 +3261,9 @@ function listWebManyoyoContainers(ctx) {
3218
3261
  };
3219
3262
  });
3220
3263
 
3264
+ if (state) {
3265
+ state._containerListCache = { timestamp: Date.now(), data: map };
3266
+ }
3221
3267
  return map;
3222
3268
  }
3223
3269
 
@@ -3874,9 +3920,9 @@ function sendNdjson(res, payload) {
3874
3920
  res.write(`${JSON.stringify(payload)}\n`);
3875
3921
  }
3876
3922
 
3877
- function createWebStreamEmitter(res, webHistoryDir, sessionRef) {
3923
+ function createWebStreamEmitter(res, state, sessionRef) {
3878
3924
  return (payload, type, data = {}) => {
3879
- const controlEvent = appendWebSessionControlEvent(webHistoryDir, sessionRef, type, data);
3925
+ const controlEvent = appendWebSessionControlEvent(state, sessionRef, type, data);
3880
3926
  sendNdjson(res, { ...payload, controlEvent });
3881
3927
  };
3882
3928
  }
@@ -4113,7 +4159,7 @@ function buildSessionAudit(ctx, state, sessionRef) {
4113
4159
  const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
4114
4160
  const agentSession = getWebAgentSession(history, sessionRef.agentId, { includeArchived: true })
4115
4161
  || createEmptyWebAgentSession(sessionRef.agentId);
4116
- const events = loadWebSessionControlEvents(state.webHistoryDir, sessionRef, agentSession.events);
4162
+ const events = loadWebSessionControlEvents(state, sessionRef, agentSession.events);
4117
4163
  const applied = history.applied && typeof history.applied === 'object' && !Array.isArray(history.applied)
4118
4164
  ? history.applied
4119
4165
  : {};
@@ -4781,7 +4827,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
4781
4827
  method: 'GET',
4782
4828
  match: currentPath => currentPath === '/api/system/capacity' ? [] : null,
4783
4829
  handler: async () => {
4784
- const containerMap = listWebManyoyoContainers(ctx);
4830
+ const containerMap = listWebManyoyoContainers(ctx, state);
4785
4831
  const diskPath = path.dirname(path.resolve(state.webConfigPath));
4786
4832
  const report = estimateContainerCapacity({
4787
4833
  runtimeCommand: ctx.dockerCmd || 'docker',
@@ -4858,7 +4904,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
4858
4904
  method: 'GET',
4859
4905
  match: currentPath => currentPath === '/api/sessions' ? [] : null,
4860
4906
  handler: async () => {
4861
- const containerMap = listWebManyoyoContainers(ctx);
4907
+ const containerMap = listWebManyoyoContainers(ctx, state);
4862
4908
  const names = new Set([
4863
4909
  ...Object.keys(containerMap),
4864
4910
  ...listWebHistorySessionNames(state.webHistoryDir, ctx.isValidContainerName)
@@ -5256,7 +5302,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5256
5302
  return;
5257
5303
  }
5258
5304
 
5259
- const containerMap = listWebManyoyoContainers(ctx);
5305
+ const containerMap = listWebManyoyoContainers(ctx, state);
5260
5306
  const detail = buildSessionDetail(ctx, state, containerMap, sessionRef);
5261
5307
  sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), detail });
5262
5308
  }
@@ -5269,7 +5315,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5269
5315
  if (!sessionRef) {
5270
5316
  return;
5271
5317
  }
5272
- const containerMap = listWebManyoyoContainers(ctx);
5318
+ const containerMap = listWebManyoyoContainers(ctx, state);
5273
5319
  const containerInfo = containerMap[sessionRef.containerName] || {};
5274
5320
  const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
5275
5321
  const effectiveTemplate = resolveEffectiveAgentPromptCommandForSession(
@@ -5367,7 +5413,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5367
5413
  return;
5368
5414
  }
5369
5415
 
5370
- const containerMap = listWebManyoyoContainers(ctx);
5416
+ const containerMap = listWebManyoyoContainers(ctx, state);
5371
5417
  const detail = buildSessionDetail(ctx, state, containerMap, sessionRef);
5372
5418
  sendJson(res, 200, {
5373
5419
  saved: true,
@@ -5516,7 +5562,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5516
5562
  'Cache-Control': 'no-store',
5517
5563
  'X-Accel-Buffering': 'no'
5518
5564
  });
5519
- const emitStreamEvent = createWebStreamEmitter(res, state.webHistoryDir, sessionRef);
5565
+ const emitStreamEvent = createWebStreamEmitter(res, state, sessionRef);
5520
5566
  emitStreamEvent({
5521
5567
  type: 'meta',
5522
5568
  containerName: sessionRef.containerName,
@@ -5570,11 +5616,13 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5570
5616
  if (event.traceEvent && typeof event.traceEvent === 'object') {
5571
5617
  traceEvents.push(event.traceEvent);
5572
5618
  }
5573
- patchWebSessionMessage(state.webHistoryDir, sessionRef, traceMessage && traceMessage.id, {
5574
- content: traceLines.join('\n'),
5575
- traceEvents: traceEvents.slice(),
5576
- pending: true
5577
- });
5619
+ if (shouldFlushLiveHistory(state, buildWebSessionKey(sessionRef.containerName, sessionRef.agentId))) {
5620
+ patchWebSessionMessage(state.webHistoryDir, sessionRef, traceMessage && traceMessage.id, {
5621
+ content: traceLines.join('\n'),
5622
+ traceEvents: traceEvents.slice(),
5623
+ pending: true
5624
+ });
5625
+ }
5578
5626
  }
5579
5627
  if (event && event.type === 'content_delta' && typeof event.content === 'string') {
5580
5628
  if (!streamingReplyMessageId) {
@@ -5592,7 +5640,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5592
5640
  streamingReplyMessageId = streamingReplyMessage && streamingReplyMessage.id
5593
5641
  ? streamingReplyMessage.id
5594
5642
  : '';
5595
- } else {
5643
+ } else if (shouldFlushLiveHistory(state, buildWebSessionKey(sessionRef.containerName, sessionRef.agentId))) {
5596
5644
  patchWebSessionMessage(state.webHistoryDir, sessionRef, streamingReplyMessageId, {
5597
5645
  content: event.content,
5598
5646
  pending: true
@@ -5695,7 +5743,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5695
5743
  sendJson(res, 404, { error: '当前会话没有运行中的 agent 任务' });
5696
5744
  return;
5697
5745
  }
5698
- appendWebSessionControlEvent(state.webHistoryDir, sessionRef, 'session.stopping');
5746
+ appendWebSessionControlEvent(state, sessionRef, 'session.stopping');
5699
5747
  sendJson(res, 200, { ok: true, stopping: true });
5700
5748
  }
5701
5749
  },
@@ -5714,7 +5762,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5714
5762
 
5715
5763
  if (removeHistory) {
5716
5764
  const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
5717
- removeAllAgentHistoryArtifacts(state.webHistoryDir, sessionRef.containerName, Object.keys(history.agents || {}));
5765
+ removeAllAgentHistoryArtifacts(state, sessionRef.containerName, Object.keys(history.agents || {}));
5718
5766
  removeWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
5719
5767
  } else if (removedContainer) {
5720
5768
  appendWebSessionMessage(state.webHistoryDir, sessionRef, 'system', `容器 ${sessionRef.containerName} 已删除。`);
@@ -5743,7 +5791,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5743
5791
  if (history.agents && typeof history.agents === 'object') {
5744
5792
  if (removeHistory) {
5745
5793
  delete history.agents[sessionRef.agentId];
5746
- removeAllAgentHistoryArtifacts(state.webHistoryDir, sessionRef.containerName, [sessionRef.agentId]);
5794
+ removeAllAgentHistoryArtifacts(state, sessionRef.containerName, [sessionRef.agentId]);
5747
5795
  } else if (history.agents[sessionRef.agentId]) {
5748
5796
  history.agents[sessionRef.agentId].archived = true;
5749
5797
  history.agents[sessionRef.agentId].updatedAt = new Date().toISOString();
@@ -5855,6 +5903,11 @@ async function startWebServer(options) {
5855
5903
  terminalSessions: new Map(),
5856
5904
  agentRuns: new Map()
5857
5905
  };
5906
+ // 跨请求复用同一个 FileEventStore 实例,append() 的增量缓存才能生效
5907
+ // (否则每次都 new 一个新实例,缓存永远命中不到)
5908
+ state.eventStore = new FileEventStore(state.webHistoryDir);
5909
+ // shouldFlushLiveHistory() 的节流状态:aggregateId -> 上次成功落盘的时间戳
5910
+ state._liveHistoryFlushThrottle = new Map();
5858
5911
 
5859
5912
  ensureWebHistoryDir(state.webHistoryDir);
5860
5913
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "7.0.16",
3
+ "version": "7.0.18",
4
4
  "imageVersion": "1.9.1-common",
5
5
  "playwrightCliVersion": "0.1.18",
6
6
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",