evolclaw 2.1.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +59 -30
  2. package/data/evolclaw.sample.json +15 -4
  3. package/dist/agents/claude-runner.js +685 -0
  4. package/dist/agents/codex-runner.js +315 -0
  5. package/dist/agents/gemini-runner.js +425 -0
  6. package/dist/channels/aun.js +580 -10
  7. package/dist/channels/feishu.js +888 -135
  8. package/dist/channels/wechat.js +127 -21
  9. package/dist/cli.js +519 -136
  10. package/dist/config.js +277 -25
  11. package/dist/core/agent-loader.js +39 -0
  12. package/dist/core/channel-loader.js +67 -0
  13. package/dist/core/command-handler.js +1537 -392
  14. package/dist/core/event-bus.js +32 -0
  15. package/dist/core/interaction-router.js +68 -0
  16. package/dist/core/message/message-bridge.js +216 -0
  17. package/dist/core/message/message-processor.js +1028 -0
  18. package/dist/core/message/message-queue.js +240 -0
  19. package/dist/core/message/stream-debouncer.js +122 -0
  20. package/dist/{utils → core/message}/stream-flusher.js +73 -13
  21. package/dist/{utils → core/message}/stream-idle-monitor.js +1 -1
  22. package/dist/core/permission.js +259 -0
  23. package/dist/core/session/adapters/claude-session-file-adapter.js +144 -0
  24. package/dist/core/session/adapters/codex-session-file-adapter.js +261 -0
  25. package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
  26. package/dist/core/session/session-file-adapter.js +7 -0
  27. package/dist/core/session/session-file-health.js +45 -0
  28. package/dist/core/session/session-manager.js +1072 -0
  29. package/dist/index.js +402 -252
  30. package/dist/ipc.js +106 -0
  31. package/dist/paths.js +1 -0
  32. package/dist/types.js +3 -0
  33. package/dist/utils/{platform.js → cross-platform.js} +38 -1
  34. package/dist/utils/error-utils.js +130 -5
  35. package/dist/utils/init-channel.js +649 -0
  36. package/dist/utils/init.js +190 -53
  37. package/dist/utils/logger.js +8 -3
  38. package/dist/utils/media-cache.js +207 -0
  39. package/dist/utils/migrate-project.js +122 -0
  40. package/dist/utils/rich-content-renderer.js +228 -0
  41. package/dist/utils/stats-collector.js +102 -0
  42. package/package.json +4 -2
  43. package/dist/core/agent-runner.js +0 -348
  44. package/dist/core/message-processor.js +0 -604
  45. package/dist/core/message-queue.js +0 -116
  46. package/dist/core/message-stream.js +0 -59
  47. package/dist/core/session-manager.js +0 -664
  48. package/dist/index.js.bak +0 -340
  49. package/dist/utils/init-feishu.js +0 -261
  50. package/dist/utils/init-wechat.js +0 -170
  51. package/dist/utils/markdown-to-feishu.js +0 -94
  52. package/dist/utils/permission.js +0 -43
  53. package/dist/utils/session-file-health.js +0 -68
  54. /package/dist/core/{message-cache.js → message/message-cache.js} +0 -0
@@ -0,0 +1,240 @@
1
+ import path from 'path';
2
+ import { logger } from '../../utils/logger.js';
3
+ export class MessageQueue {
4
+ queues = new Map();
5
+ processing = new Set();
6
+ externalLocks = new Map();
7
+ handler;
8
+ currentSessionKey;
9
+ currentProjectPath;
10
+ currentAgentId;
11
+ interruptCallback;
12
+ eventBus;
13
+ recentMessageIds = new Set();
14
+ DEDUP_WINDOW = 60_000; // 1 分钟窗口
15
+ constructor(handler) {
16
+ this.handler = handler;
17
+ }
18
+ setInterruptCallback(callback) {
19
+ this.interruptCallback = callback;
20
+ }
21
+ setEventBus(eventBus) {
22
+ this.eventBus = eventBus;
23
+ }
24
+ /**
25
+ * 检查消息是否应该处理(去重)
26
+ */
27
+ shouldProcess(message) {
28
+ if (!message.messageId)
29
+ return true; // 无 ID 的消息不去重
30
+ if (this.recentMessageIds.has(message.messageId)) {
31
+ logger.debug(`[Queue] Duplicate message ${message.messageId}, skipping`);
32
+ return false;
33
+ }
34
+ this.recentMessageIds.add(message.messageId);
35
+ setTimeout(() => this.recentMessageIds.delete(message.messageId), this.DEDUP_WINDOW);
36
+ return true;
37
+ }
38
+ /**
39
+ * 检查队列 key 是否属于指定 sessionKey
40
+ */
41
+ matchesSession(key, sessionKey) {
42
+ return key.startsWith(sessionKey + '::');
43
+ }
44
+ /**
45
+ * 生成项目级别的队列 key
46
+ */
47
+ getQueueKey(sessionKey, projectPath) {
48
+ const normalized = projectPath ? path.resolve(projectPath) : '';
49
+ return `${sessionKey}::${normalized}`;
50
+ }
51
+ async enqueue(sessionKey, message, projectPath, options) {
52
+ // 消息去重检查
53
+ if (!this.shouldProcess(message)) {
54
+ return Promise.resolve();
55
+ }
56
+ const queueKey = this.getQueueKey(sessionKey, projectPath);
57
+ logger.debug(`[Queue] Enqueuing message for ${queueKey}`);
58
+ return new Promise((resolve, reject) => {
59
+ if (!this.queues.has(queueKey)) {
60
+ this.queues.set(queueKey, []);
61
+ }
62
+ this.queues.get(queueKey).push({ message, projectPath, resolve, reject });
63
+ // 根据 interruptible 选项决定是否触发中断
64
+ if (this.processing.has(queueKey)) {
65
+ if (options?.interruptible !== false) {
66
+ // 单聊:保留中断行为
67
+ logger.debug(`[Queue] ${queueKey} is processing, triggering interrupt`);
68
+ this.eventBus?.publish({ type: 'message:interrupted', sessionId: sessionKey, reason: 'new_message' });
69
+ if (this.interruptCallback) {
70
+ this.interruptCallback(sessionKey, this.currentAgentId).catch(() => { });
71
+ }
72
+ }
73
+ else {
74
+ // 群聊:FIFO,不打断
75
+ logger.debug(`[Queue] ${queueKey} is processing, message queued (FIFO)`);
76
+ }
77
+ }
78
+ else {
79
+ logger.debug(`[Queue] Starting to process ${queueKey}`);
80
+ this.processNext(queueKey);
81
+ }
82
+ });
83
+ }
84
+ async processNext(queueKey) {
85
+ this.processing.add(queueKey);
86
+ logger.debug(`[Queue] Processing queue ${queueKey}`);
87
+ while (true) {
88
+ // 等待外部锁释放(/compact, /clear 等快速命令)
89
+ const lock = this.getExternalLock(queueKey);
90
+ if (lock) {
91
+ logger.debug(`[Queue] Waiting for external lock on ${queueKey}`);
92
+ await lock;
93
+ }
94
+ const queue = this.queues.get(queueKey);
95
+ if (!queue || queue.length === 0) {
96
+ logger.debug(`[Queue] Queue ${queueKey} is empty, stopping`);
97
+ this.processing.delete(queueKey);
98
+ this.currentSessionKey = undefined;
99
+ this.currentProjectPath = undefined;
100
+ return;
101
+ }
102
+ // FIFO 贪心合并:弹出队首连续同 peerId 的消息
103
+ const items = this.dequeueGreedy(queue);
104
+ const merged = items.length === 1 ? items[0] : this.mergeItems(items);
105
+ this.currentSessionKey = queueKey;
106
+ this.currentProjectPath = merged.projectPath;
107
+ this.currentAgentId = merged.message.agentId;
108
+ const resolves = items.map(i => i.resolve);
109
+ const rejects = items.map(i => i.reject);
110
+ logger.debug(`[Queue] Processing ${items.length} message(s) from ${merged.message.channel}:${merged.message.channelId}`);
111
+ try {
112
+ await this.handler(merged.message);
113
+ logger.debug(`[Queue] Message processed successfully`);
114
+ resolves.forEach(r => r());
115
+ }
116
+ catch (error) {
117
+ logger.error(`[Queue] Message processing failed:`, error);
118
+ rejects.forEach(r => r(error));
119
+ }
120
+ }
121
+ }
122
+ /**
123
+ * 贪心弹出队首连续同 peerId 的消息。
124
+ * 遇到不同 peerId 或队列为空时停止。
125
+ */
126
+ dequeueGreedy(queue) {
127
+ const first = queue.shift();
128
+ const result = [first];
129
+ const peerId = first.message.peerId;
130
+ while (queue.length > 0 && queue[0].message.peerId === peerId) {
131
+ result.push(queue.shift());
132
+ }
133
+ if (result.length > 1) {
134
+ logger.debug(`[Queue] Greedy dequeue: merged ${result.length} messages from peerId=${peerId}`);
135
+ }
136
+ return result;
137
+ }
138
+ /**
139
+ * 合并多条同 peerId 消息:
140
+ * - content: \n 连接
141
+ * - images / mentions: 扁平合并
142
+ * - messageId: 置空(合并后不代表某一条具体消息)
143
+ * - replyContext / peerName / 其余字段: 取最后一条
144
+ */
145
+ mergeItems(items) {
146
+ const contents = [];
147
+ const allImages = [];
148
+ const allMentions = [];
149
+ for (const item of items) {
150
+ const m = item.message;
151
+ contents.push(m.content);
152
+ if (m.images)
153
+ allImages.push(...m.images);
154
+ if (m.mentions)
155
+ allMentions.push(...m.mentions);
156
+ }
157
+ const last = items[items.length - 1];
158
+ const merged = {
159
+ ...last.message,
160
+ content: contents.join('\n'),
161
+ images: allImages.length > 0 ? allImages : undefined,
162
+ mentions: allMentions.length > 0 ? allMentions : undefined,
163
+ messageId: undefined,
164
+ };
165
+ return {
166
+ message: merged,
167
+ projectPath: last.projectPath,
168
+ resolve: () => { }, // 由调用方管理
169
+ reject: () => { },
170
+ };
171
+ }
172
+ getQueueLength(sessionKey) {
173
+ // 计算该 sessionKey 下所有项目队列的总长度
174
+ let total = 0;
175
+ for (const [key, queue] of this.queues.entries()) {
176
+ if (this.matchesSession(key, sessionKey)) {
177
+ total += queue.length;
178
+ }
179
+ }
180
+ return total;
181
+ }
182
+ isProcessing(sessionKey) {
183
+ // 检查该 sessionKey 下是否有任何项目队列在处理
184
+ for (const key of this.processing.keys()) {
185
+ if (this.matchesSession(key, sessionKey)) {
186
+ return true;
187
+ }
188
+ }
189
+ return false;
190
+ }
191
+ cancel(messageId) {
192
+ for (const queue of this.queues.values()) {
193
+ const idx = queue.findIndex(q => q.message.messageId === messageId);
194
+ if (idx !== -1) {
195
+ const [removed] = queue.splice(idx, 1);
196
+ removed.resolve();
197
+ logger.info(`[Queue] Cancelled queued message ${messageId}`);
198
+ return true;
199
+ }
200
+ }
201
+ return false;
202
+ }
203
+ /**
204
+ * 外部锁:快速命令(/compact, /clear)执行期间阻塞队列处理
205
+ * 返回 release 函数
206
+ */
207
+ acquireLock(sessionKey) {
208
+ let releaseFn;
209
+ const promise = new Promise(resolve => { releaseFn = resolve; });
210
+ this.externalLocks.set(sessionKey, promise);
211
+ return () => {
212
+ this.externalLocks.delete(sessionKey);
213
+ releaseFn();
214
+ };
215
+ }
216
+ /** 检查是否有外部锁 */
217
+ getExternalLock(queueKey) {
218
+ for (const [key, promise] of this.externalLocks) {
219
+ if (this.matchesSession(queueKey, key))
220
+ return promise;
221
+ }
222
+ return undefined;
223
+ }
224
+ /**
225
+ * 获取全局队列长度(所有会话的待处理消息总数)
226
+ */
227
+ getGlobalQueueLength() {
228
+ let total = 0;
229
+ for (const queue of this.queues.values()) {
230
+ total += queue.length;
231
+ }
232
+ return total;
233
+ }
234
+ /**
235
+ * 获取全局处理中队列数量
236
+ */
237
+ getGlobalProcessingCount() {
238
+ return this.processing.size;
239
+ }
240
+ }
@@ -0,0 +1,122 @@
1
+ import { logger } from '../../utils/logger.js';
2
+ /**
3
+ * 入站消息去抖器
4
+ *
5
+ * 在 debounceMs 窗口内收到的同一 session 的多条消息合并为一次 enqueue:
6
+ * - content 用 \n 连接
7
+ * - images / mentions 合并
8
+ * - replyContext / 其余字段取最后一条
9
+ *
10
+ * cancel(messageId) 可精确移除窗口中的某条消息,不影响其余消息。
11
+ */
12
+ export class StreamDebouncer {
13
+ pending = new Map();
14
+ delayMs;
15
+ maxWaitMs;
16
+ maxMessages;
17
+ constructor(debounceSeconds, maxMessages = 5) {
18
+ this.delayMs = debounceSeconds * 1000;
19
+ this.maxWaitMs = this.delayMs * 3;
20
+ this.maxMessages = maxMessages;
21
+ }
22
+ get enabled() {
23
+ return this.delayMs > 0;
24
+ }
25
+ /**
26
+ * 提交一条消息。如果窗口内已有消息则追加并重置 timer;
27
+ * 否则新建窗口。timer 到期后自动 flush。
28
+ */
29
+ submit(key, message, enqueue) {
30
+ const { content, images, mentions, messageId, replyContext, ...rest } = message;
31
+ return new Promise((resolve, reject) => {
32
+ const entry = { messageId, content, images, mentions, replyContext, rest, resolve, reject };
33
+ const win = this.pending.get(key);
34
+ if (win) {
35
+ clearTimeout(win.timer);
36
+ win.entries.push(entry);
37
+ if (win.entries.length >= this.maxMessages) {
38
+ logger.debug(`[Debounce] Max messages (${this.maxMessages}) reached for ${key}, flushing immediately`);
39
+ clearTimeout(win.maxWaitTimer);
40
+ this.flush(key, enqueue);
41
+ return;
42
+ }
43
+ win.timer = setTimeout(() => this.flush(key, enqueue), this.delayMs);
44
+ logger.debug(`[Debounce] Appended message for ${key}, ${win.entries.length} pending`);
45
+ }
46
+ else {
47
+ const timer = setTimeout(() => this.flush(key, enqueue), this.delayMs);
48
+ const maxWaitTimer = setTimeout(() => this.flush(key, enqueue), this.maxWaitMs);
49
+ this.pending.set(key, { entries: [entry], timer, maxWaitTimer });
50
+ logger.debug(`[Debounce] New window for ${key}, debounce=${this.delayMs}ms, maxWait=${this.maxWaitMs}ms`);
51
+ }
52
+ });
53
+ }
54
+ /**
55
+ * 从 debounce 窗口中撤回指定 messageId 的消息。
56
+ * 如果窗口只剩这一条,整个窗口取消。
57
+ * @returns true 如果找到并移除
58
+ */
59
+ cancel(messageId) {
60
+ for (const [key, win] of this.pending) {
61
+ const idx = win.entries.findIndex(e => e.messageId === messageId);
62
+ if (idx === -1)
63
+ continue;
64
+ // resolve 被撤回的那条(静默完成,不报错)
65
+ win.entries.splice(idx, 1)[0].resolve();
66
+ logger.info(`[Debounce] Cancelled message ${messageId} from window ${key}`);
67
+ // 窗口空了 → 整个取消
68
+ if (win.entries.length === 0) {
69
+ clearTimeout(win.timer);
70
+ clearTimeout(win.maxWaitTimer);
71
+ this.pending.delete(key);
72
+ logger.info(`[Debounce] Window ${key} empty after cancel, removed`);
73
+ }
74
+ return true;
75
+ }
76
+ return false;
77
+ }
78
+ flush(key, enqueue) {
79
+ const win = this.pending.get(key);
80
+ if (!win)
81
+ return;
82
+ this.pending.delete(key);
83
+ clearTimeout(win.timer);
84
+ clearTimeout(win.maxWaitTimer);
85
+ const { entries } = win;
86
+ // 合并:content 用 \n 连接,images/mentions 扁平合并,其余取最后一条
87
+ const allImages = [];
88
+ const allMentions = [];
89
+ const contents = [];
90
+ for (const e of entries) {
91
+ contents.push(e.content);
92
+ if (e.images)
93
+ allImages.push(...e.images);
94
+ if (e.mentions)
95
+ allMentions.push(...e.mentions);
96
+ }
97
+ const last = entries[entries.length - 1];
98
+ const merged = {
99
+ ...last.rest,
100
+ content: contents.join('\n'),
101
+ images: allImages.length > 0 ? allImages : undefined,
102
+ mentions: allMentions.length > 0 ? allMentions : undefined,
103
+ replyContext: last.replyContext,
104
+ messageId: entries.length > 1 ? undefined : last.messageId,
105
+ };
106
+ const resolves = entries.map(e => e.resolve);
107
+ const rejects = entries.map(e => e.reject);
108
+ enqueue(merged).then(() => resolves.forEach(r => r()), (e) => rejects.forEach(r => r(e)));
109
+ }
110
+ /** 当前挂起的 key 数量(用于测试/调试) */
111
+ get pendingCount() {
112
+ return this.pending.size;
113
+ }
114
+ /** 清理所有挂起的 timer(用于 shutdown) */
115
+ dispose() {
116
+ for (const win of this.pending.values()) {
117
+ clearTimeout(win.timer);
118
+ clearTimeout(win.maxWaitTimer);
119
+ }
120
+ this.pending.clear();
121
+ }
122
+ }
@@ -1,6 +1,7 @@
1
+ import { logger } from '../../utils/logger.js';
1
2
  import fs from 'fs';
2
3
  import path from 'path';
3
- import { resolvePaths } from '../paths.js';
4
+ import { resolvePaths } from '../../paths.js';
4
5
  // 诊断日志(按需启用,通过 config.debug.flusherDiag 控制)
5
6
  let diagStream = null;
6
7
  function getDiagStream() {
@@ -32,7 +33,7 @@ export class StreamFlusher {
32
33
  send;
33
34
  interval;
34
35
  buffer = '';
35
- activities = [];
36
+ queue = []; // 按入队顺序记录 activity 和 text 段
36
37
  timer;
37
38
  lastFlush = Date.now();
38
39
  allText = '';
@@ -43,6 +44,7 @@ export class StreamFlusher {
43
44
  instanceId;
44
45
  createTime = Date.now();
45
46
  diagEnabled;
47
+ sendChain = Promise.resolve(); // 串行发送队列,保证消息按序到达
46
48
  constructor(send, interval = 4000, fileMarkerPattern, diagEnabled = false) {
47
49
  this.send = send;
48
50
  this.interval = interval;
@@ -53,11 +55,14 @@ export class StreamFlusher {
53
55
  diag(this.instanceId, 'created', { interval });
54
56
  }
55
57
  addText(text) {
58
+ if (this.buffer.length === 0 && text.length > 0) {
59
+ this.queue.push({ kind: 'text' });
60
+ }
56
61
  this.buffer += text;
57
62
  this.allText += text;
58
63
  this.messageTimestamps.push(Date.now());
59
64
  if (this.diagEnabled)
60
- diag(this.instanceId, 'addText', { len: text.length, preview: text.substring(0, 60), bufLen: this.buffer.length, actCount: this.activities.length });
65
+ diag(this.instanceId, 'addText', { len: text.length, preview: text.substring(0, 60), bufLen: this.buffer.length, queueLen: this.queue.length });
61
66
  this.scheduleFlush();
62
67
  }
63
68
  addTextBlock(text) {
@@ -67,20 +72,21 @@ export class StreamFlusher {
67
72
  }
68
73
  this.buffer += text;
69
74
  this.allText += text;
75
+ this.queue.push({ kind: 'text' });
70
76
  this.messageTimestamps.push(Date.now());
71
77
  if (this.diagEnabled)
72
78
  diag(this.instanceId, 'addTextBlock', { len: text.length, preview: text.substring(0, 60), bufLen: this.buffer.length });
73
79
  this.scheduleFlush();
74
80
  }
75
81
  addActivity(desc) {
76
- this.activities.push(desc);
82
+ this.queue.push({ kind: 'activity', text: desc });
77
83
  this.messageTimestamps.push(Date.now());
78
84
  if (this.diagEnabled)
79
- diag(this.instanceId, 'addActivity', { desc: desc.substring(0, 80), actCount: this.activities.length });
85
+ diag(this.instanceId, 'addActivity', { desc: desc.substring(0, 80), queueLen: this.queue.length });
80
86
  this.scheduleFlush();
81
87
  }
82
88
  hasContent() {
83
- return this.buffer.length > 0 || this.activities.length > 0;
89
+ return this.buffer.length > 0 || this.queue.some(e => e.kind === 'activity');
84
90
  }
85
91
  hasSentContent() {
86
92
  return this.sentContent;
@@ -102,7 +108,7 @@ export class StreamFlusher {
102
108
  }
103
109
  let targetDelay;
104
110
  if (this.flushCount === 0) {
105
- targetDelay = 0;
111
+ targetDelay = 500;
106
112
  }
107
113
  else if (this.flushCount <= 3) {
108
114
  targetDelay = Math.ceil(this.interval / 2);
@@ -133,27 +139,81 @@ export class StreamFlusher {
133
139
  const maxDelay = this.interval * 2.5;
134
140
  return Math.max(minDelay, Math.min(maxDelay, dynamicDelay));
135
141
  }
142
+ /**
143
+ * 只 flush activities,保留 text buffer 不动
144
+ * 用于 complete 事件前清空 pending activities,让最终文本留给 flush(true) 发送
145
+ */
146
+ async flushActivitiesOnly() {
147
+ const hasActivities = this.queue.some(e => e.kind === 'activity');
148
+ if (!hasActivities)
149
+ return;
150
+ if (this.timer) {
151
+ clearTimeout(this.timer);
152
+ this.timer = undefined;
153
+ }
154
+ // 只取 activity 条目,保留 text 条目在 queue 中
155
+ const activities = this.queue.filter(e => e.kind === 'activity');
156
+ this.queue = this.queue.filter(e => e.kind === 'text');
157
+ let output = activities.map(e => e.text).join('\n') + '\n\n';
158
+ if (output && this.fileMarkerPattern) {
159
+ output = output.replace(this.fileMarkerPattern, '').trim();
160
+ }
161
+ if (this.diagEnabled)
162
+ diag(this.instanceId, 'flushActivitiesOnly', { outputLen: output.length });
163
+ if (output) {
164
+ const text = output;
165
+ // chain 保持不断裂:单条失败不阻塞后续(catch → resolve)
166
+ this.sendChain = this.sendChain
167
+ .then(() => this.send(text, false, false))
168
+ .catch(e => { logger.warn('[StreamFlusher] send failed:', e); });
169
+ await this.sendChain;
170
+ this.sentContent = true;
171
+ this.lastFlush = Date.now();
172
+ this.flushCount++;
173
+ }
174
+ }
136
175
  async flush(isFinal) {
137
176
  if (this.timer) {
138
177
  clearTimeout(this.timer);
139
178
  this.timer = undefined;
140
179
  }
141
180
  let output = '';
142
- if (this.activities.length > 0) {
143
- output += this.activities.join('\n') + '\n\n';
144
- this.activities = [];
181
+ const hasText = this.buffer.length > 0;
182
+ // 按入队顺序合并:遇到 text 条目时插入 buffer 内容,遇到 activity 直接追加
183
+ let textInserted = false;
184
+ for (const entry of this.queue) {
185
+ if (entry.kind === 'activity') {
186
+ // 确保 activity 前有换行分隔(text 末尾可能没有换行)
187
+ if (output && !output.endsWith('\n'))
188
+ output += '\n';
189
+ output += entry.text + '\n';
190
+ }
191
+ else if (!textInserted) {
192
+ if (output)
193
+ output += output.endsWith('\n') ? '\n' : '\n\n';
194
+ output += this.buffer;
195
+ textInserted = true;
196
+ }
145
197
  }
146
- if (this.buffer) {
198
+ // 如果 queue 为空但有 buffer(纯文本情况)
199
+ if (!textInserted && hasText) {
147
200
  output += this.buffer;
148
- this.buffer = '';
149
201
  }
202
+ this.queue = [];
203
+ this.buffer = '';
150
204
  if (output && this.fileMarkerPattern) {
151
205
  output = output.replace(this.fileMarkerPattern, '').trim();
152
206
  }
153
207
  if (this.diagEnabled)
154
208
  diag(this.instanceId, 'flush', { isFinal, outputLen: output.length, flushCount: this.flushCount, sinceLastFlush: Date.now() - this.lastFlush, preview: output.substring(0, 80) });
155
209
  if (output) {
156
- await this.send(output, isFinal);
210
+ const text = output;
211
+ const final = isFinal;
212
+ const ht = hasText;
213
+ this.sendChain = this.sendChain
214
+ .then(() => this.send(text, final, ht))
215
+ .catch(e => { logger.warn('[StreamFlusher] send failed:', e); });
216
+ await this.sendChain;
157
217
  this.sentContent = true;
158
218
  this.lastFlush = Date.now();
159
219
  this.flushCount++;
@@ -34,7 +34,7 @@ export class StreamIdleMonitor {
34
34
  this.state.lastToolStartTime = Date.now();
35
35
  this.state.totalToolCalls++;
36
36
  }
37
- if (type === 'text_delta' || type === 'result') {
37
+ if (type === 'text' || type === 'text_delta' || type === 'complete' || type === 'result') {
38
38
  this.state.hasReceivedText = true;
39
39
  }
40
40
  // 收到新事件,重置已触发级别