evolcore 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/LICENSE +21 -0
- package/MIGRATION-0.5.0.md +378 -0
- package/README.md +318 -14
- package/ROLE_ACCESS_CONTROL.md +174 -0
- package/assets/.env.template +4 -0
- package/bin/ec-safe-output.js +161 -0
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent.js +163 -0
- package/dist/agents/claude-runner.js +2565 -0
- package/dist/agents/codex-app-server-client.js +448 -0
- package/dist/agents/codex-runner.js +2682 -0
- package/dist/agents/gemini-runner.js +666 -0
- package/dist/agents/runner-types.js +75 -0
- package/dist/aun/aid/agentmd.js +216 -0
- package/dist/aun/aid/client.js +132 -0
- package/dist/aun/aid/control-aid.js +91 -0
- package/dist/aun/aid/identity.js +518 -0
- package/dist/aun/aid/index.js +4 -0
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/group-identity.js +10 -0
- package/dist/aun/msg/group-index.js +6 -0
- package/dist/aun/msg/group.js +1231 -0
- package/dist/aun/msg/history.js +123 -0
- package/dist/aun/msg/index.js +5 -0
- package/dist/aun/msg/p2p.js +393 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +137 -0
- package/dist/aun/outbox.js +168 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +25 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/service-proxy.js +225 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +68 -0
- package/dist/channels/aun.js +4164 -0
- package/dist/channels/contact-bind-code.js +134 -0
- package/dist/channels/daemon.js +422 -0
- package/dist/channels/dingtalk.js +1479 -0
- package/dist/channels/feishu.js +1865 -0
- package/dist/channels/qqbot.js +409 -0
- package/dist/channels/wechat.js +817 -0
- package/dist/channels/wecom-card.js +101 -0
- package/dist/channels/wecom-onboarding.js +82 -0
- package/dist/channels/wecom-state.js +191 -0
- package/dist/channels/wecom.js +1157 -0
- package/dist/cli/agent-command.js +642 -0
- package/dist/cli/agent.js +1059 -0
- package/dist/cli/aun-commands.js +2003 -0
- package/dist/cli/bench.js +1228 -0
- package/dist/cli/cli-argv.js +66 -0
- package/dist/cli/code-stats.js +329 -0
- package/dist/cli/command-log.js +82 -0
- package/dist/cli/config-selector.js +69 -0
- package/dist/cli/config.js +261 -0
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +62 -0
- package/dist/cli/daemon-commands.js +2750 -0
- package/dist/cli/fs-command.js +1447 -0
- package/dist/cli/handoff-command.js +302 -0
- package/dist/cli/help.js +35 -0
- package/dist/cli/index.js +374 -0
- package/dist/cli/init-channel.js +1372 -0
- package/dist/cli/init.js +590 -0
- package/dist/cli/link-rules.js +240 -0
- package/dist/cli/model.js +591 -0
- package/dist/cli/net-check.js +723 -0
- package/dist/cli/queue-command.js +150 -0
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +344 -0
- package/dist/cli/restart-monitor.js +480 -0
- package/dist/cli/stats.js +609 -0
- package/dist/cli/task-context.js +80 -0
- package/dist/cli/trigger-command.js +545 -0
- package/dist/cli/version.js +93 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/cli/watch-msg.js +673 -0
- package/dist/config/boot-log.js +266 -0
- package/dist/config/builtin-role-templates.js +42 -0
- package/dist/config/builtin-roles.js +91 -0
- package/dist/config/config-batch-get.js +11 -0
- package/dist/config/config-field-policy.js +261 -0
- package/dist/config/config-manager.js +1120 -0
- package/dist/config/config-operation-service.js +384 -0
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-book-store.js +454 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +224 -0
- package/dist/config/contact-operation-service.js +110 -0
- package/dist/config/gateway-config.js +858 -0
- package/dist/config/lifecycle.js +17 -0
- package/dist/config/mention-mode.js +27 -0
- package/dist/config/merge.js +161 -0
- package/dist/config/owner-policy.js +4 -0
- package/dist/config/peer-role-resolver.js +218 -0
- package/dist/config/resolved-config-op.js +483 -0
- package/dist/config/role-config-v4-startup.js +32 -0
- package/dist/config/role-config-v5-startup.js +27 -0
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-schema.js +105 -0
- package/dist/config/role-service.js +156 -0
- package/dist/config/role-store.js +215 -0
- package/dist/config/roles.js +64 -0
- package/dist/config/schema-registry.js +154 -0
- package/dist/config/snapshot.js +598 -0
- package/dist/config-store.js +501 -0
- package/dist/core/auth/agent-delegation.js +111 -0
- package/dist/core/auth/auth-gateway.js +166 -0
- package/dist/core/auth/authenticated-actor.js +6 -0
- package/dist/core/auth/authorization-audit.js +119 -0
- package/dist/core/auth/operation-authorizer.js +720 -0
- package/dist/core/auth/operation-catalog.js +731 -0
- package/dist/core/baseagent-loader.js +54 -0
- package/dist/core/bootstrap-service.js +175 -0
- package/dist/core/capability/capability-manager.js +316 -0
- package/dist/core/capability/providers/claude-capability-provider.js +176 -0
- package/dist/core/capability/providers/codex-capability-provider.js +148 -0
- package/dist/core/capability/providers/gemini-capability-provider.js +10 -0
- package/dist/core/capability/types.js +27 -0
- package/dist/core/causation/audit.js +103 -0
- package/dist/core/causation/aun-association.js +111 -0
- package/dist/core/causation/context.js +93 -0
- package/dist/core/causation/index.js +4 -0
- package/dist/core/causation/types.js +2 -0
- package/dist/core/channel-loader.js +277 -0
- package/dist/core/command/agent-control.js +616 -0
- package/dist/core/command/cli-intent-parser.js +225 -0
- package/dist/core/command/command-handler.js +1733 -0
- package/dist/core/command/connect-menu.js +374 -0
- package/dist/core/command/menu-handler.js +3452 -0
- package/dist/core/command/menu-protocol.js +247 -0
- package/dist/core/command/role-menu.js +1623 -0
- package/dist/core/command/slash-gate.js +148 -0
- package/dist/core/command/slash-handler.js +3066 -0
- package/dist/core/daemon-file-cache.js +222 -0
- package/dist/core/event-bus.js +32 -0
- package/dist/core/event-catalog.js +810 -0
- package/dist/core/evolagent-registry.js +545 -0
- package/dist/core/evolagent.js +342 -0
- package/dist/core/handoff/dispatcher.js +229 -0
- package/dist/core/handoff/mutex.js +46 -0
- package/dist/core/handoff/runtime.js +324 -0
- package/dist/core/handoff/store.js +537 -0
- package/dist/core/handoff/types.js +12 -0
- package/dist/core/inference/text-inference.js +173 -0
- package/dist/core/interaction-registration.js +10 -0
- package/dist/core/interaction-router.js +278 -0
- package/dist/core/message/create-status.js +67 -0
- package/dist/core/message/im-renderer.js +659 -0
- package/dist/core/message/items-formatter.js +76 -0
- package/dist/core/message/logical-queue-bridge.js +123 -0
- package/dist/core/message/message-bridge.js +874 -0
- package/dist/core/message/message-cache.js +56 -0
- package/dist/core/message/message-log.js +339 -0
- package/dist/core/message/message-processor.js +4 -0
- package/dist/core/message/message-queue.js +1446 -0
- package/dist/core/message/message-utils.js +76 -0
- package/dist/core/message/peer-mode.js +105 -0
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/message/response-depth.js +33 -0
- package/dist/core/message/response-engine.js +4020 -0
- package/dist/core/message/response-snapshot.js +83 -0
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +139 -0
- package/dist/core/message/stream-idle-monitor.js +124 -0
- package/dist/core/model/config-scope.js +162 -0
- package/dist/core/model/field-scope.js +78 -0
- package/dist/core/model/model-catalog.js +227 -0
- package/dist/core/model/model-diagnostics.js +182 -0
- package/dist/core/model/model-permission.js +90 -0
- package/dist/core/permission/approval-gateway.js +1017 -0
- package/dist/core/permission/ec-command-parser.js +347 -0
- package/dist/core/permission/execution-sandbox.js +16 -0
- package/dist/core/permission/index.js +6 -0
- package/dist/core/permission/mode.js +24 -0
- package/dist/core/permission/sandbox-runtime.js +265 -0
- package/dist/core/permission/tool-policy.js +1019 -0
- package/dist/core/permission/unix-socket-policy.js +99 -0
- package/dist/core/protected-paths.js +332 -0
- package/dist/core/relation/peer-identity.js +222 -0
- package/dist/core/relation/peer-key.js +1 -0
- package/dist/core/role/runtime-policy.js +141 -0
- package/dist/core/session/adapters/claude-session-file-adapter.js +218 -0
- package/dist/core/session/adapters/codex-session-file-adapter.js +333 -0
- package/dist/core/session/adapters/gemini-session-file-adapter.js +181 -0
- package/dist/core/session/session-file-adapter.js +7 -0
- package/dist/core/session/session-file-health.js +45 -0
- package/dist/core/session/session-fs-store.js +273 -0
- package/dist/core/session/session-key.js +24 -0
- package/dist/core/session/session-manager.js +1643 -0
- package/dist/core/session/session-mapper.js +100 -0
- package/dist/core/session/session-renew.js +314 -0
- package/dist/core/session/session-title.js +128 -0
- package/dist/core/session/session-turn-coordinator.js +205 -0
- package/dist/core/session/session-turns.js +67 -0
- package/dist/core/system-channels.js +29 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/group-rules-sync.js +345 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/kit-renderer.js +359 -0
- package/dist/eck/manifest-engine.js +446 -0
- package/dist/eck/message-renderer.js +199 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +2926 -4
- package/dist/ipc.js +777 -0
- package/dist/paths.js +262 -0
- package/dist/product.js +18 -0
- package/dist/response-system/context-builder.js +71 -0
- package/dist/response-system/coordinator.js +117 -0
- package/dist/response-system/decision-executor.js +86 -0
- package/dist/response-system/engines/v1/index.js +21 -0
- package/dist/response-system/engines/v1/interactive-flow.js +27 -0
- package/dist/response-system/engines/v1/proactive-flow.js +137 -0
- package/dist/response-system/engines/v1/types.js +1 -0
- package/dist/response-system/extensions.js +41 -0
- package/dist/response-system/index.js +6 -0
- package/dist/response-system/modes/index.js +7 -0
- package/dist/response-system/modes/single-session/index.js +72 -0
- package/dist/response-system/queues/fifo-queue.js +44 -0
- package/dist/response-system/queues/index.js +6 -0
- package/dist/response-system/queues/lifo-queue.js +42 -0
- package/dist/response-system/queues/priority-queue.js +63 -0
- package/dist/response-system/registry.js +97 -0
- package/dist/response-system/resolver.js +37 -0
- package/dist/response-system/selector.js +23 -0
- package/dist/response-system/types.js +7 -0
- package/dist/stats/billing.js +163 -0
- package/dist/stats/budget.js +93 -0
- package/dist/stats/db.js +403 -0
- package/dist/stats/eck-vars.js +89 -0
- package/dist/stats/index.js +11 -0
- package/dist/stats/normalizer.js +80 -0
- package/dist/stats/price-resolver.js +138 -0
- package/dist/stats/query.js +763 -0
- package/dist/stats/role-budget.js +168 -0
- package/dist/stats/writer.js +151 -0
- package/dist/trigger/anomaly-store.js +258 -0
- package/dist/trigger/audit.js +152 -0
- package/dist/trigger/event-source.js +119 -0
- package/dist/trigger/feedback.js +685 -0
- package/dist/trigger/history.js +290 -0
- package/dist/trigger/manager.js +294 -0
- package/dist/trigger/parser.js +595 -0
- package/dist/trigger/patch.js +155 -0
- package/dist/trigger/scheduler.js +1602 -0
- package/dist/trigger/script-executor.js +155 -0
- package/dist/trigger/state.js +145 -0
- package/dist/trigger/types.js +1 -0
- package/dist/trigger/validation.js +634 -0
- package/dist/types.js +12 -0
- package/dist/utils/aid-bind.js +313 -0
- package/dist/utils/atomic-write.js +95 -0
- package/dist/utils/avatar-upload.js +123 -0
- package/dist/utils/cross-platform.js +297 -0
- package/dist/utils/ecweb-utils.js +73 -0
- package/dist/utils/error-dict.json +153 -0
- package/dist/utils/error-utils.js +349 -0
- package/dist/utils/instance-registry.js +444 -0
- package/dist/utils/locale.js +21 -0
- package/dist/utils/log-writer.js +270 -0
- package/dist/utils/logger.js +89 -0
- package/dist/utils/markdown-to-plain-text.js +20 -0
- package/dist/utils/media-cache.js +274 -0
- package/dist/utils/model-prices.jsonl +20 -0
- package/dist/utils/npm-ops.js +210 -0
- package/dist/utils/process-introspect.js +133 -0
- package/dist/utils/process-tree-stats.js +271 -0
- package/dist/utils/project-path.js +74 -0
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/stats.js +410 -0
- package/dist/utils/system-memory.js +62 -0
- package/dist/utils/tool-summary.js +284 -0
- package/dist/utils/welcome.js +268 -0
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +66 -0
- package/kits/docs/aun/CHEATSHEET.md +19 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/aun.md +65 -0
- package/kits/docs/channels/feishu.md +56 -0
- package/kits/docs/context-assembly.md +366 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolcore/INDEX.md +68 -0
- package/kits/docs/evolcore/agent.md +77 -0
- package/kits/docs/evolcore/aid.md +52 -0
- package/kits/docs/evolcore/config.md +149 -0
- package/kits/docs/evolcore/contact.md +57 -0
- package/kits/docs/evolcore/ctl.md +46 -0
- package/kits/docs/evolcore/event.md +216 -0
- package/kits/docs/evolcore/fs-architecture.md +1215 -0
- package/kits/docs/evolcore/fs.md +110 -0
- package/kits/docs/evolcore/group-fs.md +17 -0
- package/kits/docs/evolcore/group-rules.md +226 -0
- package/kits/docs/evolcore/group.md +150 -0
- package/kits/docs/evolcore/model.md +50 -0
- package/kits/docs/evolcore/msg.md +136 -0
- package/kits/docs/evolcore/response.md +75 -0
- package/kits/docs/evolcore/rpc.md +37 -0
- package/kits/docs/evolcore/self-summary.md +29 -0
- package/kits/docs/evolcore/stats.md +83 -0
- package/kits/docs/evolcore/storage.md +50 -0
- package/kits/docs/evolcore/trigger.md +539 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +23 -0
- package/kits/docs/identity/identity-tools.md +26 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/docs/prompt-loading-architecture.md +266 -0
- package/kits/docs/venues/aun-group.md +45 -0
- package/kits/docs/venues/aun-private.md +10 -0
- package/kits/docs/venues/client-desktop.md +10 -0
- package/kits/docs/venues/client-mobile.md +10 -0
- package/kits/docs/venues/feishu-group.md +13 -0
- package/kits/docs/venues/feishu-private.md +9 -0
- package/kits/docs/venues/group.md +25 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.auxiliary.json +43 -0
- package/kits/eck_manifest.json +203 -0
- package/kits/eck_message_manifest.json +63 -0
- package/kits/migrations/README-role-config-v4.md +32 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
- package/kits/migrations/migrate-role-config-v4.mjs +623 -0
- package/kits/migrations/migrate-role-config-v5.mjs +346 -0
- package/kits/migrations/rename-config-file.mjs +99 -0
- package/kits/rules/01-overview.md +142 -0
- package/kits/rules/02-navigation.md +76 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +59 -0
- package/kits/rules/05-venue.md +44 -0
- package/kits/rules/06-channel.md +59 -0
- package/kits/schemas/_meta.json +32 -0
- package/kits/schemas/agent-config.schema.1.json +177 -0
- package/kits/schemas/agent-config.schema.2.json +239 -0
- package/kits/schemas/agent-config.schema.3.json +119 -0
- package/kits/schemas/agent-config.schema.4.json +208 -0
- package/kits/schemas/agent-config.schema.5.json +326 -0
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/contact-book.schema.1.json +36 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/daemon.schema.1.json +90 -0
- package/kits/schemas/defaults.schema.1.json +81 -0
- package/kits/schemas/menu-exec-schema-commands.md +208 -0
- package/kits/schemas/migrations/README.md +28 -0
- package/kits/schemas/relation-config.schema.1.json +158 -0
- package/kits/schemas/relation-config.schema.2.json +73 -0
- package/kits/schemas/relation-config.schema.3.json +50 -0
- package/kits/schemas/relation-config.schema.4.json +47 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/role-config.schema.1.json +201 -0
- package/kits/schemas/role-registry.schema.1.json +35 -0
- package/kits/schemas/single-session.schema.1.json +31 -0
- package/kits/templates/bootstrap-welcome.md +15 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +13 -0
- package/kits/templates/message-fragments/handoff-response-to-origin.md +10 -0
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/kits/templates/message-fragments/item.md +2 -0
- package/kits/templates/roles/admin.json +9 -0
- package/kits/templates/roles/member.json +41 -0
- package/kits/templates/roles/owner.json +9 -0
- package/kits/templates/roles/visitor.json +40 -0
- package/kits/templates/system-fragments/baseagent.md +14 -0
- package/kits/templates/system-fragments/bootstrap.md +16 -0
- package/kits/templates/system-fragments/channel.md +48 -0
- package/kits/templates/system-fragments/commands.md +28 -0
- package/kits/templates/system-fragments/identity.md +11 -0
- package/kits/templates/system-fragments/relation.md +19 -0
- package/kits/templates/system-fragments/session.md +53 -0
- package/kits/templates/system-fragments/venue.md +31 -0
- package/package.json +50 -15
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,1446 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { logger } from '../../utils/logger.js';
|
|
4
|
+
import { LogicalQueueBridge } from './logical-queue-bridge.js';
|
|
5
|
+
import { normalizeCausation } from '../causation/context.js';
|
|
6
|
+
const DEFAULT_AGENT_NAME = '<unknown>';
|
|
7
|
+
const RESTART_RESUME_CONTENT = 'evolcore 服务已重启,请继续之前未完成的任务。';
|
|
8
|
+
export class MessageQueue {
|
|
9
|
+
queues = new Map();
|
|
10
|
+
processing = new Set();
|
|
11
|
+
processingAgent = new Map(); // queueKey → agentName(处理中项目的 agent)
|
|
12
|
+
externalLocks = new Map();
|
|
13
|
+
handler;
|
|
14
|
+
activeStates = new Map(); // queueKey → active task state
|
|
15
|
+
interruptCallback;
|
|
16
|
+
interruptBarriers = new Map();
|
|
17
|
+
eventBus;
|
|
18
|
+
recentMessageIds = new Set();
|
|
19
|
+
DEDUP_WINDOW = 60_000; // 1 分钟窗口
|
|
20
|
+
interceptors = new Map();
|
|
21
|
+
mutedAgents = new Set(); // 禁言的 agent:消息照常入队,但不取出给大模型
|
|
22
|
+
persistencePath;
|
|
23
|
+
activeBatches = new Map();
|
|
24
|
+
processingStartTime = new Map(); // queueKey → 处理开始时间戳
|
|
25
|
+
queueKeyToSessionKey = new Map(); // queueKey → sessionKey(human-readable 格式)
|
|
26
|
+
droppedTriggerSessionIds = new Set();
|
|
27
|
+
/** 逻辑队列桥接:管理出队顺序(响应模式可注入自定义策略,默认 FIFO) */
|
|
28
|
+
logicalQueue = new LogicalQueueBridge();
|
|
29
|
+
constructor(handler, options) {
|
|
30
|
+
this.handler = handler;
|
|
31
|
+
this.persistencePath = options?.persistencePath;
|
|
32
|
+
}
|
|
33
|
+
/** 暴露逻辑队列桥接,供响应模式系统注入队列工厂 */
|
|
34
|
+
getLogicalQueueBridge() {
|
|
35
|
+
return this.logicalQueue;
|
|
36
|
+
}
|
|
37
|
+
setInterruptCallback(callback) {
|
|
38
|
+
this.interruptCallback = callback;
|
|
39
|
+
}
|
|
40
|
+
triggerInterrupt(queueKey, sessionKey, baseagent, evolagentName, reason) {
|
|
41
|
+
const existing = this.interruptBarriers.get(queueKey);
|
|
42
|
+
if (existing)
|
|
43
|
+
return existing;
|
|
44
|
+
if (!this.interruptCallback)
|
|
45
|
+
return Promise.resolve();
|
|
46
|
+
const barrier = this.interruptCallback(sessionKey, baseagent, evolagentName, reason)
|
|
47
|
+
.catch(error => {
|
|
48
|
+
logger.warn(`[Queue] Interrupt barrier failed for ${queueKey}: ${error instanceof Error ? error.message : String(error)}`);
|
|
49
|
+
})
|
|
50
|
+
.finally(() => {
|
|
51
|
+
if (this.interruptBarriers.get(queueKey) === barrier) {
|
|
52
|
+
this.interruptBarriers.delete(queueKey);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
this.interruptBarriers.set(queueKey, barrier);
|
|
56
|
+
return barrier;
|
|
57
|
+
}
|
|
58
|
+
setEventBus(eventBus) {
|
|
59
|
+
this.eventBus = eventBus;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 注册一次性消息拦截器:下一条来自 sessionKey 的消息不入队、不触发 interrupt,
|
|
63
|
+
* 直接传给 handler。用于 AskUserQuestion 的"手动输入"场景。
|
|
64
|
+
*/
|
|
65
|
+
interceptNext(sessionKey, handler) {
|
|
66
|
+
this.interceptors.set(sessionKey, handler);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 取消拦截器(超时或卡片被其他方式回答时调用)
|
|
70
|
+
*/
|
|
71
|
+
cancelIntercept(sessionKey) {
|
|
72
|
+
this.interceptors.delete(sessionKey);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 检查消息是否应该处理(去重)
|
|
76
|
+
*
|
|
77
|
+
* 去重 key = `${sessionKey}:${messageId}`,而非裸 messageId。
|
|
78
|
+
* MessageQueue 是进程级单例,被所有 evolagent 共享。AUN 群广播时同一条群消息
|
|
79
|
+
* 会投递给群里每个 evolagent,它们 messageId 相同但 session 不同,必须各处理一次。
|
|
80
|
+
* 裸 messageId 去重会让先入队的 agent 吞掉其他 agent 的消息。
|
|
81
|
+
*/
|
|
82
|
+
shouldProcess(sessionKey, message) {
|
|
83
|
+
if (!message.messageId)
|
|
84
|
+
return true; // 无 ID 的消息不去重
|
|
85
|
+
const dedupKey = `${sessionKey}:${message.messageId}`;
|
|
86
|
+
if (this.recentMessageIds.has(dedupKey) || this.hasEnqueuedMessage(sessionKey, message.messageId)) {
|
|
87
|
+
logger.debug(`[Queue] Duplicate message ${dedupKey}, skipping`);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
this.recentMessageIds.add(dedupKey);
|
|
91
|
+
setTimeout(() => this.recentMessageIds.delete(dedupKey), this.DEDUP_WINDOW);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
hasEnqueuedMessage(sessionKey, messageId) {
|
|
95
|
+
for (const [queueKey, queue] of this.queues) {
|
|
96
|
+
if (!this.matchesSession(queueKey, sessionKey))
|
|
97
|
+
continue;
|
|
98
|
+
if (queue.some(item => this.messageIdsFor(item).includes(messageId)))
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
for (const [queueKey, active] of this.activeBatches) {
|
|
102
|
+
if (!this.matchesSession(queueKey, sessionKey))
|
|
103
|
+
continue;
|
|
104
|
+
if (this.messageIdsFor(active).includes(messageId))
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
forgetProcessed(sessionKey, message) {
|
|
110
|
+
if (!message.messageId)
|
|
111
|
+
return;
|
|
112
|
+
this.recentMessageIds.delete(`${sessionKey}:${message.messageId}`);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 检查队列 key 是否属于指定 sessionKey
|
|
116
|
+
*/
|
|
117
|
+
matchesSession(key, sessionKey) {
|
|
118
|
+
return this.sessionKeyFromQueueKey(key) === sessionKey;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 生成队列键(格式:selfAID::sessionKey::projectPath)
|
|
122
|
+
* 不同 agent 与同一对端的队列独立,避免冲突。
|
|
123
|
+
*/
|
|
124
|
+
getQueueKey(sessionKey, projectPath, selfAID) {
|
|
125
|
+
const normalized = projectPath ? path.resolve(projectPath) : '';
|
|
126
|
+
const prefix = selfAID ? `${selfAID}::` : '';
|
|
127
|
+
return `${prefix}${sessionKey}::${normalized}`;
|
|
128
|
+
}
|
|
129
|
+
sessionKeyFromQueueKey(queueKey) {
|
|
130
|
+
// 格式:selfAID::sessionKey::projectPath 或 sessionKey::projectPath(旧格式兼容)
|
|
131
|
+
const parts = queueKey.split('::');
|
|
132
|
+
if (parts.length >= 3) {
|
|
133
|
+
// 新格式:selfAID::sessionKey::projectPath
|
|
134
|
+
return parts[1];
|
|
135
|
+
}
|
|
136
|
+
else if (parts.length === 2) {
|
|
137
|
+
// 旧格式:sessionKey::projectPath
|
|
138
|
+
return parts[0];
|
|
139
|
+
}
|
|
140
|
+
return queueKey;
|
|
141
|
+
}
|
|
142
|
+
partFromItem(item) {
|
|
143
|
+
return {
|
|
144
|
+
message: item.message,
|
|
145
|
+
projectPath: item.projectPath,
|
|
146
|
+
agentName: item.agentName,
|
|
147
|
+
role: item.role,
|
|
148
|
+
resolve: item.resolve,
|
|
149
|
+
reject: item.reject,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
itemFromPart(part) {
|
|
153
|
+
return {
|
|
154
|
+
message: part.message,
|
|
155
|
+
projectPath: part.projectPath,
|
|
156
|
+
agentName: part.agentName,
|
|
157
|
+
role: part.role,
|
|
158
|
+
resolve: part.resolve,
|
|
159
|
+
reject: part.reject,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
partsOf(item) {
|
|
163
|
+
return item.parts ?? [this.partFromItem(item)];
|
|
164
|
+
}
|
|
165
|
+
partCount(item) {
|
|
166
|
+
return item.parts?.length ?? 1;
|
|
167
|
+
}
|
|
168
|
+
queueRawMessageCount(queue) {
|
|
169
|
+
return queue.reduce((sum, item) => sum + this.partCount(item), 0);
|
|
170
|
+
}
|
|
171
|
+
messageIdsFor(item) {
|
|
172
|
+
return this.partsOf(item)
|
|
173
|
+
.map(part => part.message.messageId)
|
|
174
|
+
.filter((id) => !!id);
|
|
175
|
+
}
|
|
176
|
+
peerIdsFor(item) {
|
|
177
|
+
return this.partsOf(item).map(part => part.message.peerId);
|
|
178
|
+
}
|
|
179
|
+
serializePart(part, enqueuedAt) {
|
|
180
|
+
return {
|
|
181
|
+
message: part.message,
|
|
182
|
+
projectPath: part.projectPath,
|
|
183
|
+
agentName: part.agentName,
|
|
184
|
+
role: part.role,
|
|
185
|
+
enqueuedAt,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
serializeItem(item, enqueuedAt) {
|
|
189
|
+
return {
|
|
190
|
+
message: item.message,
|
|
191
|
+
projectPath: item.projectPath,
|
|
192
|
+
agentName: item.agentName,
|
|
193
|
+
role: item.role,
|
|
194
|
+
enqueuedAt,
|
|
195
|
+
parts: item.parts?.map(part => this.serializePart(part, enqueuedAt)),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
restoredPart(item) {
|
|
199
|
+
return {
|
|
200
|
+
message: this.normalizeRestoredMessage(item.message),
|
|
201
|
+
projectPath: item.projectPath,
|
|
202
|
+
agentName: item.agentName || DEFAULT_AGENT_NAME,
|
|
203
|
+
role: item.role,
|
|
204
|
+
resolve: () => { },
|
|
205
|
+
reject: () => { },
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
restoreSubmittedPart(item) {
|
|
209
|
+
const source = this.normalizeRestoredMessage(item.message);
|
|
210
|
+
const message = {
|
|
211
|
+
...source,
|
|
212
|
+
content: RESTART_RESUME_CONTENT,
|
|
213
|
+
images: undefined,
|
|
214
|
+
mentions: undefined,
|
|
215
|
+
mentionAids: undefined,
|
|
216
|
+
messageId: undefined,
|
|
217
|
+
batchRole: item.role,
|
|
218
|
+
items: [{
|
|
219
|
+
kind: 'restart-resume',
|
|
220
|
+
peerId: source.peerId,
|
|
221
|
+
peerName: source.peerName,
|
|
222
|
+
peerType: source.peerType,
|
|
223
|
+
peerRole: item.role,
|
|
224
|
+
sameDevice: source.sameDevice,
|
|
225
|
+
sameNetwork: source.sameNetwork,
|
|
226
|
+
sameEgressIp: source.sameEgressIp,
|
|
227
|
+
content: RESTART_RESUME_CONTENT,
|
|
228
|
+
timestamp: source.timestamp,
|
|
229
|
+
}],
|
|
230
|
+
restartResume: { submitted: true },
|
|
231
|
+
};
|
|
232
|
+
return {
|
|
233
|
+
message,
|
|
234
|
+
projectPath: item.projectPath,
|
|
235
|
+
agentName: item.agentName || DEFAULT_AGENT_NAME,
|
|
236
|
+
role: item.role,
|
|
237
|
+
resolve: () => { },
|
|
238
|
+
reject: () => { },
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
normalizeRestoredMessage(message) {
|
|
242
|
+
const causation = normalizeCausation(message.causation);
|
|
243
|
+
if (message.causation && !causation) {
|
|
244
|
+
logger.warn(`[Queue] Ignoring invalid persisted causation for message ${message.messageId ?? '<none>'}`);
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
...message,
|
|
248
|
+
causation,
|
|
249
|
+
items: message.items?.map(item => ({
|
|
250
|
+
...item,
|
|
251
|
+
causation: normalizeCausation(item.causation),
|
|
252
|
+
})),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
buildCoalescedItem(parts) {
|
|
256
|
+
if (parts.length === 1)
|
|
257
|
+
return this.itemFromPart(parts[0]);
|
|
258
|
+
const merged = this.mergeItems(parts.map(part => this.itemFromPart(part)));
|
|
259
|
+
return {
|
|
260
|
+
message: merged.message,
|
|
261
|
+
projectPath: merged.projectPath,
|
|
262
|
+
agentName: merged.agentName,
|
|
263
|
+
role: this.highestRole(parts),
|
|
264
|
+
resolve: () => parts.forEach(part => part.resolve()),
|
|
265
|
+
reject: (error) => parts.forEach(part => part.reject(error)),
|
|
266
|
+
parts,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
highestRole(items) {
|
|
270
|
+
const rank = {
|
|
271
|
+
none: 0,
|
|
272
|
+
visitor: 1,
|
|
273
|
+
member: 2,
|
|
274
|
+
admin: 3,
|
|
275
|
+
owner: 4,
|
|
276
|
+
};
|
|
277
|
+
let best;
|
|
278
|
+
for (const item of items) {
|
|
279
|
+
if (!item.role)
|
|
280
|
+
continue;
|
|
281
|
+
if (!best || rank[item.role] > rank[best])
|
|
282
|
+
best = item.role;
|
|
283
|
+
}
|
|
284
|
+
return best;
|
|
285
|
+
}
|
|
286
|
+
commonRole(items) {
|
|
287
|
+
let role;
|
|
288
|
+
for (const item of items) {
|
|
289
|
+
if (!item.role)
|
|
290
|
+
return undefined;
|
|
291
|
+
if (!role) {
|
|
292
|
+
role = item.role;
|
|
293
|
+
}
|
|
294
|
+
else if (role !== item.role) {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return role;
|
|
299
|
+
}
|
|
300
|
+
canDequeueGroupTimeline(first, next) {
|
|
301
|
+
if (first.role === undefined || next.role === undefined)
|
|
302
|
+
return false;
|
|
303
|
+
if ((first.agentName || DEFAULT_AGENT_NAME) !== (next.agentName || DEFAULT_AGENT_NAME))
|
|
304
|
+
return false;
|
|
305
|
+
if (path.resolve(first.projectPath) !== path.resolve(next.projectPath))
|
|
306
|
+
return false;
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
canDequeueAfterSubmittedResume(first, next) {
|
|
310
|
+
if (!first.message.restartResume?.submitted)
|
|
311
|
+
return false;
|
|
312
|
+
if ((first.agentName || DEFAULT_AGENT_NAME) !== (next.agentName || DEFAULT_AGENT_NAME))
|
|
313
|
+
return false;
|
|
314
|
+
if (path.resolve(first.projectPath) !== path.resolve(next.projectPath))
|
|
315
|
+
return false;
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* 写盘队列状态。每次队列结构变化(入队/出队/取消/清理)后调用。
|
|
320
|
+
* 仅在内容真正变化时写盘,文件小(数 KB),renameSync 原子替换保证崩溃一致性。
|
|
321
|
+
*/
|
|
322
|
+
persistQueues() {
|
|
323
|
+
return this.persistQueuesSync();
|
|
324
|
+
}
|
|
325
|
+
/** 立即同步写盘(语义同 persistQueues,保留供进程退出钩子显式调用) */
|
|
326
|
+
persistQueuesImmediate() {
|
|
327
|
+
this.persistQueuesSync();
|
|
328
|
+
}
|
|
329
|
+
persistQueuesSync() {
|
|
330
|
+
if (!this.persistencePath)
|
|
331
|
+
return true;
|
|
332
|
+
try {
|
|
333
|
+
const buckets = [];
|
|
334
|
+
const activeBuckets = [];
|
|
335
|
+
const enqueuedAt = Date.now();
|
|
336
|
+
for (const [queueKey, queue] of this.queues.entries()) {
|
|
337
|
+
if (queue.length === 0)
|
|
338
|
+
continue;
|
|
339
|
+
buckets.push({
|
|
340
|
+
queueKey,
|
|
341
|
+
items: queue.map(item => this.serializeItem(item, enqueuedAt)),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
for (const [queueKey, item] of this.activeBatches.entries()) {
|
|
345
|
+
activeBuckets.push({
|
|
346
|
+
queueKey,
|
|
347
|
+
items: [this.serializeItem(item, enqueuedAt)],
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
fs.mkdirSync(path.dirname(this.persistencePath), { recursive: true });
|
|
351
|
+
if (buckets.length === 0 && activeBuckets.length === 0) {
|
|
352
|
+
try {
|
|
353
|
+
fs.unlinkSync(this.persistencePath);
|
|
354
|
+
}
|
|
355
|
+
catch { }
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
const data = {
|
|
359
|
+
version: 2,
|
|
360
|
+
updatedAt: Date.now(),
|
|
361
|
+
queues: buckets,
|
|
362
|
+
active: activeBuckets,
|
|
363
|
+
};
|
|
364
|
+
const tmp = `${this.persistencePath}.tmp-${process.pid}-${Date.now()}`;
|
|
365
|
+
const fd = fs.openSync(tmp, 'w');
|
|
366
|
+
try {
|
|
367
|
+
fs.writeSync(fd, JSON.stringify(data));
|
|
368
|
+
fs.fsyncSync(fd);
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
fs.closeSync(fd);
|
|
372
|
+
}
|
|
373
|
+
fs.renameSync(tmp, this.persistencePath);
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
logger.error('[Queue] Failed to persist queue state:', error);
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
restoreBucket(bucket, restoredKeys) {
|
|
382
|
+
if (!bucket.queueKey || !Array.isArray(bucket.items))
|
|
383
|
+
return 0;
|
|
384
|
+
if (!this.queues.has(bucket.queueKey))
|
|
385
|
+
this.queues.set(bucket.queueKey, []);
|
|
386
|
+
const queue = this.queues.get(bucket.queueKey);
|
|
387
|
+
const sessionKey = this.sessionKeyFromQueueKey(bucket.queueKey);
|
|
388
|
+
let restored = 0;
|
|
389
|
+
for (const item of bucket.items) {
|
|
390
|
+
if (!item?.message || !item.projectPath)
|
|
391
|
+
continue;
|
|
392
|
+
const parts = Array.isArray(item.parts) && item.parts.length > 0
|
|
393
|
+
? item.parts
|
|
394
|
+
.filter(part => part?.message && part.projectPath && this.shouldRestoreMessage(part.message, sessionKey))
|
|
395
|
+
.map(part => this.restoredPart(part))
|
|
396
|
+
: this.shouldRestoreMessage(item.message, sessionKey) ? [this.restoredPart(item)] : [];
|
|
397
|
+
if (parts.length === 0)
|
|
398
|
+
continue;
|
|
399
|
+
const rawParts = parts
|
|
400
|
+
.sort((a, b) => (a.message.timestamp ?? 0) - (b.message.timestamp ?? 0));
|
|
401
|
+
for (const part of rawParts) {
|
|
402
|
+
const rawItem = this.itemFromPart(part);
|
|
403
|
+
queue.push(rawItem);
|
|
404
|
+
if (rawItem.message.messageId) {
|
|
405
|
+
const dedupKey = `${sessionKey}:${rawItem.message.messageId}`;
|
|
406
|
+
this.recentMessageIds.add(dedupKey);
|
|
407
|
+
setTimeout(() => this.recentMessageIds.delete(dedupKey), this.DEDUP_WINDOW);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
restored += rawParts.length;
|
|
411
|
+
}
|
|
412
|
+
if (queue.length > 0)
|
|
413
|
+
restoredKeys.add(bucket.queueKey);
|
|
414
|
+
return restored;
|
|
415
|
+
}
|
|
416
|
+
restoreSubmittedBucket(bucket, restoredKeys) {
|
|
417
|
+
if (!bucket.queueKey || !Array.isArray(bucket.items))
|
|
418
|
+
return 0;
|
|
419
|
+
if (!this.queues.has(bucket.queueKey))
|
|
420
|
+
this.queues.set(bucket.queueKey, []);
|
|
421
|
+
const queue = this.queues.get(bucket.queueKey);
|
|
422
|
+
const sessionKey = this.sessionKeyFromQueueKey(bucket.queueKey);
|
|
423
|
+
let restored = 0;
|
|
424
|
+
for (const item of bucket.items) {
|
|
425
|
+
if (!item?.message || !item.projectPath)
|
|
426
|
+
continue;
|
|
427
|
+
const rawParts = Array.isArray(item.parts) && item.parts.length > 0
|
|
428
|
+
? item.parts.filter(part => part?.message && part.projectPath && this.shouldRestoreMessage(part.message, sessionKey))
|
|
429
|
+
: this.shouldRestoreMessage(item.message, sessionKey) ? [item] : [];
|
|
430
|
+
if (rawParts.length === 0)
|
|
431
|
+
continue;
|
|
432
|
+
const first = rawParts[0];
|
|
433
|
+
const submittedPart = first.message.handoffDelivery
|
|
434
|
+
? this.restoredPart(first)
|
|
435
|
+
: this.restoreSubmittedPart(first);
|
|
436
|
+
const submittedItem = this.itemFromPart(submittedPart);
|
|
437
|
+
submittedItem.message.timestamp = first.message.timestamp;
|
|
438
|
+
queue.push(submittedItem);
|
|
439
|
+
for (const part of rawParts) {
|
|
440
|
+
if (!part.message.messageId)
|
|
441
|
+
continue;
|
|
442
|
+
const dedupKey = `${sessionKey}:${part.message.messageId}`;
|
|
443
|
+
this.recentMessageIds.add(dedupKey);
|
|
444
|
+
setTimeout(() => this.recentMessageIds.delete(dedupKey), this.DEDUP_WINDOW);
|
|
445
|
+
}
|
|
446
|
+
restored += rawParts.length;
|
|
447
|
+
}
|
|
448
|
+
if (queue.length > 0)
|
|
449
|
+
restoredKeys.add(bucket.queueKey);
|
|
450
|
+
return restored;
|
|
451
|
+
}
|
|
452
|
+
restorePersisted(startProcessing = true) {
|
|
453
|
+
if (!this.persistencePath || !fs.existsSync(this.persistencePath))
|
|
454
|
+
return 0;
|
|
455
|
+
let restored = 0;
|
|
456
|
+
try {
|
|
457
|
+
const raw = fs.readFileSync(this.persistencePath, 'utf-8');
|
|
458
|
+
const parsed = JSON.parse(raw);
|
|
459
|
+
if ((parsed.version !== 1 && parsed.version !== 2) || !Array.isArray(parsed.queues)) {
|
|
460
|
+
logger.warn('[Queue] Ignoring unsupported persisted queue file');
|
|
461
|
+
return 0;
|
|
462
|
+
}
|
|
463
|
+
const restoredKeys = new Set();
|
|
464
|
+
if (Array.isArray(parsed.active)) {
|
|
465
|
+
for (const bucket of parsed.active) {
|
|
466
|
+
restored += this.restoreSubmittedBucket(bucket, restoredKeys);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
for (const bucket of parsed.queues) {
|
|
470
|
+
restored += this.restoreBucket(bucket, restoredKeys);
|
|
471
|
+
}
|
|
472
|
+
this.persistQueuesImmediate();
|
|
473
|
+
if (restored > 0) {
|
|
474
|
+
logger.info(`[Queue] Restored ${restored} persisted message(s) from ${this.persistencePath}`);
|
|
475
|
+
}
|
|
476
|
+
if (startProcessing) {
|
|
477
|
+
for (const key of restoredKeys) {
|
|
478
|
+
if (!this.processing.has(key))
|
|
479
|
+
this.processNext(key);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
logger.error('[Queue] Failed to restore persisted queue:', error);
|
|
485
|
+
}
|
|
486
|
+
return restored;
|
|
487
|
+
}
|
|
488
|
+
startRestored() {
|
|
489
|
+
for (const [queueKey, queue] of this.queues) {
|
|
490
|
+
if (queue.length > 0 && !this.processing.has(queueKey))
|
|
491
|
+
void this.processNext(queueKey);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
consumeDroppedTriggerSessionIds() {
|
|
495
|
+
const sessionIds = [...this.droppedTriggerSessionIds];
|
|
496
|
+
this.droppedTriggerSessionIds.clear();
|
|
497
|
+
return sessionIds;
|
|
498
|
+
}
|
|
499
|
+
shouldRestoreMessage(message, sessionId) {
|
|
500
|
+
// The Trigger scheduler records open runs as daemon_restart. Replaying their
|
|
501
|
+
// queued work afterwards would execute side effects for an already-failed run.
|
|
502
|
+
if (message.source !== 'trigger' && !message.triggerMeta)
|
|
503
|
+
return true;
|
|
504
|
+
this.droppedTriggerSessionIds.add(sessionId);
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
async enqueue(sessionKey, message, projectPath, options) {
|
|
508
|
+
if (options?.signal?.aborted) {
|
|
509
|
+
return Promise.reject(abortError(options.signal.reason));
|
|
510
|
+
}
|
|
511
|
+
// 消息去重检查
|
|
512
|
+
if (!this.shouldProcess(sessionKey, message)) {
|
|
513
|
+
options?.onPersisted?.();
|
|
514
|
+
return Promise.resolve();
|
|
515
|
+
}
|
|
516
|
+
// 拦截器检查:AskUserQuestion 等场景的一次性消息拦截
|
|
517
|
+
const interceptor = this.interceptors.get(sessionKey);
|
|
518
|
+
const canAnswerInteraction = message.source === undefined
|
|
519
|
+
|| message.source === 'user'
|
|
520
|
+
|| message.source === 'card-trigger';
|
|
521
|
+
if (interceptor && canAnswerInteraction) {
|
|
522
|
+
this.interceptors.delete(sessionKey);
|
|
523
|
+
logger.debug(`[Queue] Message intercepted for ${sessionKey}`);
|
|
524
|
+
interceptor(message);
|
|
525
|
+
return Promise.resolve();
|
|
526
|
+
}
|
|
527
|
+
const queueKey = this.getQueueKey(sessionKey, projectPath, options?.selfAID);
|
|
528
|
+
// 记录 sessionKey 映射(human-readable 格式,供 getQueueItemsBySession/getQueueItemsByAgent 输出)
|
|
529
|
+
if (options?.sessionKeyField) {
|
|
530
|
+
this.queueKeyToSessionKey.set(queueKey, options.sessionKeyField);
|
|
531
|
+
}
|
|
532
|
+
const agentName = options?.agentName || DEFAULT_AGENT_NAME;
|
|
533
|
+
const isProcessing = this.processing.has(queueKey);
|
|
534
|
+
const currentQueue = this.queues.get(queueKey);
|
|
535
|
+
logger.info(`[Queue] enqueue: key=${queueKey} processing=${isProcessing} queueLen=${currentQueue?.length ?? 0} pending=${currentQueue?.length ?? 0} rawPending=${currentQueue ? this.queueRawMessageCount(currentQueue) : 0} agent=${agentName} role=${options?.role ?? '<none>'} peer=${message.peerId ?? '<none>'} msg=${message.messageId ?? '<none>'}`);
|
|
536
|
+
return new Promise((resolve, reject) => {
|
|
537
|
+
let settled = false;
|
|
538
|
+
let aborting = false;
|
|
539
|
+
let abortHandler;
|
|
540
|
+
const cleanupAbort = () => {
|
|
541
|
+
if (abortHandler)
|
|
542
|
+
options?.signal?.removeEventListener('abort', abortHandler);
|
|
543
|
+
abortHandler = undefined;
|
|
544
|
+
};
|
|
545
|
+
const resolveOnce = () => {
|
|
546
|
+
if (settled || aborting)
|
|
547
|
+
return;
|
|
548
|
+
settled = true;
|
|
549
|
+
cleanupAbort();
|
|
550
|
+
resolve();
|
|
551
|
+
};
|
|
552
|
+
const settleReject = (error) => {
|
|
553
|
+
if (settled)
|
|
554
|
+
return;
|
|
555
|
+
settled = true;
|
|
556
|
+
cleanupAbort();
|
|
557
|
+
reject(error);
|
|
558
|
+
};
|
|
559
|
+
const rejectOnce = (error) => {
|
|
560
|
+
if (aborting)
|
|
561
|
+
return;
|
|
562
|
+
settleReject(error);
|
|
563
|
+
};
|
|
564
|
+
if (!this.queues.has(queueKey)) {
|
|
565
|
+
this.queues.set(queueKey, []);
|
|
566
|
+
}
|
|
567
|
+
const queue = this.queues.get(queueKey);
|
|
568
|
+
const queuedResolve = options?.resolveOnPersist ? cleanupAbort : resolveOnce;
|
|
569
|
+
const queuedReject = options?.resolveOnPersist ? cleanupAbort : rejectOnce;
|
|
570
|
+
const queuedItem = { message, projectPath, agentName, role: options?.role, resolve: queuedResolve, reject: queuedReject };
|
|
571
|
+
queue.push(queuedItem);
|
|
572
|
+
this.logicalQueue.enqueue(queueKey, message); // 通知逻辑队列记录出队顺序
|
|
573
|
+
abortHandler = () => {
|
|
574
|
+
if (settled || aborting)
|
|
575
|
+
return;
|
|
576
|
+
aborting = true;
|
|
577
|
+
const error = abortError(options?.signal?.reason);
|
|
578
|
+
const currentQueue = this.queues.get(queueKey);
|
|
579
|
+
const queuedIndex = currentQueue?.indexOf(queuedItem) ?? -1;
|
|
580
|
+
if (currentQueue && queuedIndex >= 0) {
|
|
581
|
+
currentQueue.splice(queuedIndex, 1);
|
|
582
|
+
this.logicalQueue.removeProcessed(queueKey, message.messageId ? [message.messageId] : []);
|
|
583
|
+
this.forgetProcessed(sessionKey, message);
|
|
584
|
+
this.persistQueuesImmediate();
|
|
585
|
+
}
|
|
586
|
+
else if (message.messageId) {
|
|
587
|
+
void this.cancelActiveAndWait(message.messageId).finally(() => {
|
|
588
|
+
this.forgetProcessed(sessionKey, message);
|
|
589
|
+
settleReject(error);
|
|
590
|
+
});
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
settleReject(error);
|
|
594
|
+
};
|
|
595
|
+
options?.signal?.addEventListener('abort', abortHandler, { once: true });
|
|
596
|
+
if (options?.signal?.aborted) {
|
|
597
|
+
abortHandler();
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (!this.persistQueues()) {
|
|
601
|
+
const index = queue.indexOf(queuedItem);
|
|
602
|
+
if (index >= 0)
|
|
603
|
+
queue.splice(index, 1);
|
|
604
|
+
this.logicalQueue.removeProcessed(queueKey, message.messageId ? [message.messageId] : []);
|
|
605
|
+
this.forgetProcessed(sessionKey, message);
|
|
606
|
+
rejectOnce(new Error('failed to persist message queue'));
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
options?.onPersisted?.();
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
const index = queue.indexOf(queuedItem);
|
|
614
|
+
if (index >= 0)
|
|
615
|
+
queue.splice(index, 1);
|
|
616
|
+
this.logicalQueue.removeProcessed(queueKey, message.messageId ? [message.messageId] : []);
|
|
617
|
+
this.forgetProcessed(sessionKey, message);
|
|
618
|
+
this.persistQueues();
|
|
619
|
+
rejectOnce(error);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
if (options?.resolveOnPersist)
|
|
623
|
+
resolveOnce();
|
|
624
|
+
if (this.processing.has(queueKey)) {
|
|
625
|
+
// 打断判定:
|
|
626
|
+
// - 单聊(interruptible !== false):始终打断
|
|
627
|
+
// - 群聊(interruptible === false)+ interruptSamePeer:仅当「同一发送者连发」
|
|
628
|
+
// 且队列里没有其他人的消息时打断,避免抢占群里他人的排队消息
|
|
629
|
+
const interruptSingle = options?.interruptible !== false;
|
|
630
|
+
const activeState = this.activeStates.get(queueKey);
|
|
631
|
+
const interruptGroupSamePeer = options?.interruptible === false &&
|
|
632
|
+
options?.interruptSamePeer === true &&
|
|
633
|
+
!!message.peerId &&
|
|
634
|
+
activeState?.peerId === message.peerId &&
|
|
635
|
+
!this.hasOtherPeerQueued(queueKey, message.peerId);
|
|
636
|
+
if (interruptSingle || interruptGroupSamePeer) {
|
|
637
|
+
logger.debug(`[Queue] ${queueKey} is processing, triggering interrupt (samePeer=${interruptGroupSamePeer})`);
|
|
638
|
+
this.eventBus?.publish({
|
|
639
|
+
type: 'task:interrupted',
|
|
640
|
+
sessionId: sessionKey,
|
|
641
|
+
reason: 'new_message',
|
|
642
|
+
agentName: this.processingAgent.get(queueKey),
|
|
643
|
+
causation: this.activeBatches.get(queueKey)?.message.causation,
|
|
644
|
+
});
|
|
645
|
+
if (this.interruptCallback) {
|
|
646
|
+
void this.triggerInterrupt(queueKey, sessionKey, activeState?.baseagent, this.processingAgent.get(queueKey), 'new_message');
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
// 群聊:FIFO,不打断
|
|
651
|
+
logger.debug(`[Queue] ${queueKey} is processing, message queued (FIFO)`);
|
|
652
|
+
this.eventBus?.publish({
|
|
653
|
+
type: 'task:queued',
|
|
654
|
+
channel: message.channel,
|
|
655
|
+
channelId: message.channelId,
|
|
656
|
+
replyContext: message.replyContext,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
logger.debug(`[Queue] Starting to process ${queueKey}`);
|
|
662
|
+
void this.processNext(queueKey);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
enqueuePersisted(sessionKey, message, projectPath, options) {
|
|
667
|
+
return this.enqueue(sessionKey, message, projectPath, { ...options, resolveOnPersist: true });
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* 队列中是否存在 peerId 不同于给定值的消息(群聊同人打断守卫)。
|
|
671
|
+
* 忽略 peerId 为 undefined 的消息(系统消息等不参与打断判定)。
|
|
672
|
+
*/
|
|
673
|
+
hasOtherPeerQueued(queueKey, peerId) {
|
|
674
|
+
const q = this.queues.get(queueKey);
|
|
675
|
+
if (!q)
|
|
676
|
+
return false;
|
|
677
|
+
return q.some(item => this.peerIdsFor(item).some(queuedPeerId => !!queuedPeerId && queuedPeerId !== peerId));
|
|
678
|
+
}
|
|
679
|
+
async processNext(queueKey) {
|
|
680
|
+
this.processing.add(queueKey);
|
|
681
|
+
logger.info(`[Queue] processNext: start key=${queueKey}`);
|
|
682
|
+
while (true) {
|
|
683
|
+
const interruptBarrier = this.interruptBarriers.get(queueKey);
|
|
684
|
+
if (interruptBarrier) {
|
|
685
|
+
logger.debug(`[Queue] Waiting for interrupt barrier on ${queueKey}`);
|
|
686
|
+
await interruptBarrier;
|
|
687
|
+
}
|
|
688
|
+
// 等待外部锁释放(/compact, /clear 等快速命令)
|
|
689
|
+
const lock = this.getExternalLock(queueKey);
|
|
690
|
+
if (lock) {
|
|
691
|
+
logger.debug(`[Queue] Waiting for external lock on ${queueKey}`);
|
|
692
|
+
await lock;
|
|
693
|
+
}
|
|
694
|
+
const queue = this.queues.get(queueKey);
|
|
695
|
+
if (!queue || queue.length === 0) {
|
|
696
|
+
logger.info(`[Queue] processNext: queue empty, releasing key=${queueKey}`);
|
|
697
|
+
this.processing.delete(queueKey);
|
|
698
|
+
this.processingAgent.delete(queueKey);
|
|
699
|
+
this.activeStates.delete(queueKey);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
// 禁言:消息留在队列里,暂停消费(解禁后由 unmuteAgent 重新触发 processNext)
|
|
703
|
+
const headAgent = queue[0].agentName || DEFAULT_AGENT_NAME;
|
|
704
|
+
if (this.mutedAgents.has(headAgent)) {
|
|
705
|
+
logger.info(`[Queue] processNext: agent ${headAgent} muted, pausing key=${queueKey} (${queue.length} pending, ${this.queueRawMessageCount(queue)} raw)`);
|
|
706
|
+
this.processing.delete(queueKey);
|
|
707
|
+
this.processingAgent.delete(queueKey);
|
|
708
|
+
this.activeStates.delete(queueKey);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
// 逻辑队列重排:让响应模式的出队策略决定队首(默认 FIFO,不改变顺序)
|
|
712
|
+
this.logicalQueue.reorderPhysical(queueKey, queue);
|
|
713
|
+
// 贪心合并:群聊按完整时间线,旧路径/私聊按 peerId
|
|
714
|
+
const items = this.dequeueGreedy(queue);
|
|
715
|
+
// 同步逻辑队列:移除所有被合并处理的消息
|
|
716
|
+
if (items.length > 0) {
|
|
717
|
+
const processedIds = items.map(item => item.message.messageId).filter((id) => !!id);
|
|
718
|
+
if (processedIds.length > 0) {
|
|
719
|
+
this.logicalQueue.removeProcessed(queueKey, processedIds);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const merged = items.length === 1 ? items[0] : this.mergeItems(items);
|
|
723
|
+
const rawItems = items.flatMap(item => this.partsOf(item).map(part => this.itemFromPart(part)));
|
|
724
|
+
const rawParts = rawItems.map(item => this.partFromItem(item));
|
|
725
|
+
// activeItem 复用已合并的 message,仅额外持有 parts 用于持久化和撤回定位(不重复 merge)
|
|
726
|
+
const activeItem = rawParts.length === 1
|
|
727
|
+
? this.itemFromPart(rawParts[0])
|
|
728
|
+
: {
|
|
729
|
+
message: merged.message,
|
|
730
|
+
projectPath: merged.projectPath,
|
|
731
|
+
agentName: merged.agentName,
|
|
732
|
+
role: this.highestRole(rawParts),
|
|
733
|
+
resolve: () => rawParts.forEach(part => part.resolve()),
|
|
734
|
+
reject: (error) => rawParts.forEach(part => part.reject(error)),
|
|
735
|
+
parts: rawParts,
|
|
736
|
+
};
|
|
737
|
+
const batchRole = this.commonRole(rawItems);
|
|
738
|
+
if (batchRole && !merged.message.batchRole)
|
|
739
|
+
merged.message.batchRole = batchRole;
|
|
740
|
+
this.processingAgent.set(queueKey, merged.agentName);
|
|
741
|
+
// 记录正在执行的 messageId(用于撤回中断)
|
|
742
|
+
const activeMessageIds = new Set();
|
|
743
|
+
for (const item of rawItems) {
|
|
744
|
+
if (item.message.messageId)
|
|
745
|
+
activeMessageIds.add(item.message.messageId);
|
|
746
|
+
}
|
|
747
|
+
const activeState = {
|
|
748
|
+
baseagent: merged.message.baseagent,
|
|
749
|
+
peerId: this.uniquePeerId(rawItems),
|
|
750
|
+
messageIds: activeMessageIds,
|
|
751
|
+
};
|
|
752
|
+
this.activeStates.set(queueKey, activeState);
|
|
753
|
+
const resolves = rawItems.map(i => i.resolve);
|
|
754
|
+
const rejects = rawItems.map(i => i.reject);
|
|
755
|
+
this.activeBatches.set(queueKey, activeItem);
|
|
756
|
+
this.processingStartTime.set(queueKey, Date.now());
|
|
757
|
+
this.persistQueuesImmediate();
|
|
758
|
+
logger.info(`[Queue] processing batch: key=${queueKey} items=${rawItems.length} pending=${queue.length} rawPending=${this.queueRawMessageCount(queue)} batchRole=${merged.message.batchRole ?? '<mixed>'} peer=${activeState.peerId ?? '<multi>'} msg=${merged.message.messageId ?? '<none>'}`);
|
|
759
|
+
try {
|
|
760
|
+
await this.handler(merged.message);
|
|
761
|
+
logger.debug(`[Queue] Message processed successfully`);
|
|
762
|
+
resolves.forEach(r => r());
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
logger.error(`[Queue] Message processing failed:`, error);
|
|
766
|
+
rejects.forEach(r => r(error));
|
|
767
|
+
}
|
|
768
|
+
finally {
|
|
769
|
+
this.activeBatches.delete(queueKey);
|
|
770
|
+
this.processingStartTime.delete(queueKey);
|
|
771
|
+
this.activeStates.delete(queueKey);
|
|
772
|
+
this.persistQueuesImmediate();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* 贪心弹出队首可合并消息。
|
|
778
|
+
* 群聊入队携带 role,此时取出当前所有群聊 pending 并按时间线合并;
|
|
779
|
+
* role 只作为逐条元数据,不作为分批边界。未携带 role 的旧路径/私聊按 peerId 合并。
|
|
780
|
+
*/
|
|
781
|
+
dequeueGreedy(queue) {
|
|
782
|
+
const first = queue.shift();
|
|
783
|
+
const result = [first];
|
|
784
|
+
if (this.isMergeBarrier(first.message))
|
|
785
|
+
return result;
|
|
786
|
+
const mergeSubmittedResume = first.message.restartResume?.submitted === true;
|
|
787
|
+
const mergeGroupTimeline = first.role !== undefined;
|
|
788
|
+
const mergeKey = mergeGroupTimeline ? '<group-timeline>' : first.message.peerId;
|
|
789
|
+
if (mergeSubmittedResume) {
|
|
790
|
+
const remaining = [];
|
|
791
|
+
for (const item of queue) {
|
|
792
|
+
if (!this.isMergeBarrier(item.message) && this.canDequeueAfterSubmittedResume(first, item)) {
|
|
793
|
+
result.push(item);
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
remaining.push(item);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
queue.length = 0;
|
|
800
|
+
queue.push(...remaining);
|
|
801
|
+
result.sort((a, b) => {
|
|
802
|
+
if (a.message.restartResume?.submitted)
|
|
803
|
+
return -1;
|
|
804
|
+
if (b.message.restartResume?.submitted)
|
|
805
|
+
return 1;
|
|
806
|
+
return this.firstTimestamp(a) - this.firstTimestamp(b);
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
else if (mergeGroupTimeline) {
|
|
810
|
+
const remaining = [];
|
|
811
|
+
for (const item of queue) {
|
|
812
|
+
if (!this.isMergeBarrier(item.message) && this.canDequeueGroupTimeline(first, item)) {
|
|
813
|
+
result.push(item);
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
remaining.push(item);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
queue.length = 0;
|
|
820
|
+
queue.push(...remaining);
|
|
821
|
+
result.sort((a, b) => this.firstTimestamp(a) - this.firstTimestamp(b));
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
while (queue.length > 0 && !this.isMergeBarrier(queue[0].message) && queue[0].message.peerId === mergeKey) {
|
|
825
|
+
result.push(queue.shift());
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (result.length > 1) {
|
|
829
|
+
logger.info(`[Queue] Greedy dequeue: merged ${result.length} pending item(s) by ${mergeSubmittedResume ? 'restart-resume' : (mergeGroupTimeline ? 'group-timeline' : 'peerId')}=${mergeKey ?? '<none>'}`);
|
|
830
|
+
}
|
|
831
|
+
return result;
|
|
832
|
+
}
|
|
833
|
+
isMergeBarrier(message) {
|
|
834
|
+
return !!message.handoffDelivery || !!message.triggerMeta || !!message.causation?.trigger?.path.length;
|
|
835
|
+
}
|
|
836
|
+
firstTimestamp(item) {
|
|
837
|
+
const parts = this.partsOf(item);
|
|
838
|
+
let ts = Number.POSITIVE_INFINITY;
|
|
839
|
+
for (const part of parts)
|
|
840
|
+
ts = Math.min(ts, part.message.timestamp ?? 0);
|
|
841
|
+
return Number.isFinite(ts) ? ts : 0;
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* 当前在途任务只有一个真实发送者时才启用 same-peer 打断。
|
|
845
|
+
* 多发送者群聊批次返回 undefined,避免其中某个成员的新消息不断打断聚合推理。
|
|
846
|
+
*/
|
|
847
|
+
uniquePeerId(items) {
|
|
848
|
+
const peers = new Set(items.map(item => item.message.peerId).filter(Boolean));
|
|
849
|
+
if (peers.size !== 1)
|
|
850
|
+
return undefined;
|
|
851
|
+
return [...peers][0];
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* 合并多条同 peerId 或群聊时间线消息:
|
|
855
|
+
* - content: \n 连接(兜底用,渲染层优先用 items)
|
|
856
|
+
* - items: 保留每条子消息(含各自 peer/timestamp),供消息渲染层逐条渲染
|
|
857
|
+
* - images / mentions: 扁平合并
|
|
858
|
+
* - messageId: 取最新一条的 messageId(用于 thought 锚定与中断追踪)
|
|
859
|
+
* - sourceMessageIds: 保留全部原始入站消息 ID(用于渠道确认状态批量迁移)
|
|
860
|
+
* - replyContext / peerName / 其余字段: 取最后一条
|
|
861
|
+
*/
|
|
862
|
+
mergeItems(items) {
|
|
863
|
+
const contents = [];
|
|
864
|
+
const allImages = [];
|
|
865
|
+
const allMentions = [];
|
|
866
|
+
const subMessages = [];
|
|
867
|
+
let mergedPeerType;
|
|
868
|
+
const sourceMessageIds = [];
|
|
869
|
+
const seenSourceMessageIds = new Set();
|
|
870
|
+
let hasRestartResume = false;
|
|
871
|
+
let hasMessagesAfterRestartResume = false;
|
|
872
|
+
for (const item of items) {
|
|
873
|
+
const m = item.message;
|
|
874
|
+
if (hasRestartResume && !m.restartResume?.submitted) {
|
|
875
|
+
hasMessagesAfterRestartResume = true;
|
|
876
|
+
}
|
|
877
|
+
if (m.restartResume?.submitted) {
|
|
878
|
+
hasRestartResume = true;
|
|
879
|
+
}
|
|
880
|
+
contents.push(m.content);
|
|
881
|
+
if (m.images)
|
|
882
|
+
allImages.push(...m.images);
|
|
883
|
+
if (m.mentions)
|
|
884
|
+
allMentions.push(...m.mentions);
|
|
885
|
+
for (const id of [...(m.sourceMessageIds ?? []), ...(m.messageId ? [m.messageId] : [])]) {
|
|
886
|
+
if (!seenSourceMessageIds.has(id)) {
|
|
887
|
+
seenSourceMessageIds.add(id);
|
|
888
|
+
sourceMessageIds.push(id);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (m.peerType && m.peerType !== 'human') {
|
|
892
|
+
mergedPeerType = m.peerType;
|
|
893
|
+
}
|
|
894
|
+
else if (!mergedPeerType && m.peerType) {
|
|
895
|
+
mergedPeerType = m.peerType;
|
|
896
|
+
}
|
|
897
|
+
// 逐条保留发送者、时刻、图片;若该条已自带 items(罕见),展开保留细粒度
|
|
898
|
+
if (m.items && m.items.length > 0) {
|
|
899
|
+
subMessages.push(...m.items.map(sub => ({ ...sub, peerRole: sub.peerRole ?? item.role })));
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
subMessages.push({
|
|
903
|
+
peerId: m.peerId, peerName: m.peerName, peerType: m.peerType,
|
|
904
|
+
peerRole: item.role,
|
|
905
|
+
sameDevice: m.sameDevice, sameNetwork: m.sameNetwork, sameEgressIp: m.sameEgressIp,
|
|
906
|
+
encrypted: m.encrypted,
|
|
907
|
+
content: m.content, timestamp: m.timestamp,
|
|
908
|
+
causation: m.causation,
|
|
909
|
+
images: m.images && m.images.length > 0 ? m.images : undefined,
|
|
910
|
+
mentionAids: m.mentionAids && m.mentionAids.length > 0 ? m.mentionAids : undefined,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
const last = items[items.length - 1];
|
|
915
|
+
const role = this.commonRole(items);
|
|
916
|
+
// 保留最新一条的 messageId(若最后一条无 ID 则回退到前面已有的 ID)
|
|
917
|
+
let latestMessageId;
|
|
918
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
919
|
+
if (items[i].message.messageId) {
|
|
920
|
+
latestMessageId = items[i].message.messageId;
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
const merged = {
|
|
925
|
+
...last.message,
|
|
926
|
+
content: contents.join('\n'),
|
|
927
|
+
items: subMessages,
|
|
928
|
+
images: allImages.length > 0 ? allImages : undefined,
|
|
929
|
+
mentions: allMentions.length > 0 ? allMentions : undefined,
|
|
930
|
+
messageId: latestMessageId,
|
|
931
|
+
sourceMessageIds: sourceMessageIds.length > 0 ? sourceMessageIds : undefined,
|
|
932
|
+
// 密文优先:合并批次内任一条密文,则整轮(interactive 自动回复的单值出站)按密文。
|
|
933
|
+
// 任一条 encrypted===true 即 true;否则若有明确的 false 则 false;全 undefined(非 aun)则 undefined。
|
|
934
|
+
encrypted: subMessages.some(s => s.encrypted === true)
|
|
935
|
+
? true
|
|
936
|
+
: (subMessages.some(s => s.encrypted === false) ? false : undefined),
|
|
937
|
+
batchRole: role,
|
|
938
|
+
peerType: mergedPeerType,
|
|
939
|
+
restartResume: hasRestartResume
|
|
940
|
+
? { submitted: true, pendingInterrupted: hasMessagesAfterRestartResume }
|
|
941
|
+
: undefined,
|
|
942
|
+
};
|
|
943
|
+
return {
|
|
944
|
+
message: merged,
|
|
945
|
+
projectPath: last.projectPath,
|
|
946
|
+
agentName: last.agentName,
|
|
947
|
+
resolve: () => { }, // 由调用方管理
|
|
948
|
+
reject: () => { },
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
getQueueLength(sessionKey) {
|
|
952
|
+
// 计算该 sessionKey 下所有项目队列的总长度
|
|
953
|
+
let total = 0;
|
|
954
|
+
for (const [key, queue] of this.queues.entries()) {
|
|
955
|
+
if (this.matchesSession(key, sessionKey)) {
|
|
956
|
+
total += queue.length;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return total;
|
|
960
|
+
}
|
|
961
|
+
isProcessing(sessionKey) {
|
|
962
|
+
// 检查该 sessionKey 下是否有任何项目队列在处理
|
|
963
|
+
for (const key of this.processing.keys()) {
|
|
964
|
+
if (this.matchesSession(key, sessionKey)) {
|
|
965
|
+
return true;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* 检查指定 channel 下是否有任何 session 在处理。
|
|
972
|
+
* queueKey 格式为 `${sessionKey}::${projectPath}`,其中 sessionKey
|
|
973
|
+
* 形如 `${channelName}-${channelId}-${ts}`,因此匹配 `${channelName}-` 前缀。
|
|
974
|
+
*/
|
|
975
|
+
isChannelProcessing(channelName) {
|
|
976
|
+
const prefix = `${channelName}-`;
|
|
977
|
+
for (const key of this.processing.keys()) {
|
|
978
|
+
if (key.startsWith(prefix) || key.startsWith(`${channelName}::`)) {
|
|
979
|
+
return true;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
cancel(messageId) {
|
|
985
|
+
for (const queue of this.queues.values()) {
|
|
986
|
+
const idx = queue.findIndex(q => this.messageIdsFor(q).includes(messageId));
|
|
987
|
+
if (idx === -1)
|
|
988
|
+
continue;
|
|
989
|
+
const target = queue[idx];
|
|
990
|
+
const parts = this.partsOf(target);
|
|
991
|
+
const partIdx = parts.findIndex(part => part.message.messageId === messageId);
|
|
992
|
+
if (partIdx === -1)
|
|
993
|
+
continue;
|
|
994
|
+
if (parts.length === 1) {
|
|
995
|
+
const [removed] = queue.splice(idx, 1);
|
|
996
|
+
removed.resolve();
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
const [removed] = parts.splice(partIdx, 1);
|
|
1000
|
+
removed.resolve();
|
|
1001
|
+
queue[idx] = this.buildCoalescedItem(parts);
|
|
1002
|
+
}
|
|
1003
|
+
this.persistQueuesImmediate();
|
|
1004
|
+
logger.info(`[Queue] Cancelled queued message ${messageId}`);
|
|
1005
|
+
return true;
|
|
1006
|
+
}
|
|
1007
|
+
return false;
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* 撤回正在执行的消息:如果 messageId 正在处理中,触发 interrupt。
|
|
1011
|
+
* @returns true 如果找到并触发了中断
|
|
1012
|
+
*/
|
|
1013
|
+
cancelActive(messageId) {
|
|
1014
|
+
const cancellation = this.beginActiveCancellation(messageId);
|
|
1015
|
+
if (cancellation.barrier)
|
|
1016
|
+
void cancellation.barrier;
|
|
1017
|
+
return cancellation.found;
|
|
1018
|
+
}
|
|
1019
|
+
/** Cancel an active message and wait until the runner interrupt barrier settles. */
|
|
1020
|
+
async cancelActiveAndWait(messageId) {
|
|
1021
|
+
const cancellation = this.beginActiveCancellation(messageId);
|
|
1022
|
+
await cancellation.barrier;
|
|
1023
|
+
return cancellation.found;
|
|
1024
|
+
}
|
|
1025
|
+
beginActiveCancellation(messageId) {
|
|
1026
|
+
for (const [queueKey, activeState] of this.activeStates.entries()) {
|
|
1027
|
+
if (!activeState.messageIds.has(messageId))
|
|
1028
|
+
continue;
|
|
1029
|
+
const active = this.activeBatches.get(queueKey);
|
|
1030
|
+
if (active && this.messageIdsFor(active).includes(messageId)) {
|
|
1031
|
+
const parts = this.partsOf(active);
|
|
1032
|
+
const remaining = parts.filter(part => part.message.messageId !== messageId);
|
|
1033
|
+
if (remaining.length === 0) {
|
|
1034
|
+
this.activeBatches.delete(queueKey);
|
|
1035
|
+
this.processingStartTime.delete(queueKey);
|
|
1036
|
+
activeState.messageIds.clear();
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
this.activeBatches.set(queueKey, this.buildCoalescedItem(remaining));
|
|
1040
|
+
activeState.messageIds = new Set(remaining
|
|
1041
|
+
.map(part => part.message.messageId)
|
|
1042
|
+
.filter((id) => !!id));
|
|
1043
|
+
}
|
|
1044
|
+
this.persistQueuesImmediate();
|
|
1045
|
+
}
|
|
1046
|
+
const sessionKey = this.sessionKeyFromQueueKey(queueKey);
|
|
1047
|
+
const agentName = this.processingAgent.get(queueKey);
|
|
1048
|
+
logger.info(`[Queue] Recalled active message ${messageId}, interrupting session ${sessionKey}`);
|
|
1049
|
+
this.eventBus?.publish({
|
|
1050
|
+
type: 'task:interrupted',
|
|
1051
|
+
sessionId: sessionKey,
|
|
1052
|
+
reason: 'recalled',
|
|
1053
|
+
agentName,
|
|
1054
|
+
causation: active?.message.causation,
|
|
1055
|
+
});
|
|
1056
|
+
const barrier = this.interruptCallback
|
|
1057
|
+
? this.triggerInterrupt(queueKey, sessionKey, activeState.baseagent, agentName, 'recalled')
|
|
1058
|
+
: Promise.resolve();
|
|
1059
|
+
return { found: true, barrier };
|
|
1060
|
+
}
|
|
1061
|
+
return { found: false, barrier: Promise.resolve() };
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* 外部锁:快速命令(/compact, /clear)执行期间阻塞队列处理
|
|
1065
|
+
* 返回 release 函数
|
|
1066
|
+
*/
|
|
1067
|
+
acquireLock(sessionKey) {
|
|
1068
|
+
let releaseFn;
|
|
1069
|
+
const promise = new Promise(resolve => { releaseFn = resolve; });
|
|
1070
|
+
this.externalLocks.set(sessionKey, promise);
|
|
1071
|
+
return () => {
|
|
1072
|
+
this.externalLocks.delete(sessionKey);
|
|
1073
|
+
releaseFn();
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
/** 检查是否有外部锁 */
|
|
1077
|
+
getExternalLock(queueKey) {
|
|
1078
|
+
for (const [key, promise] of this.externalLocks) {
|
|
1079
|
+
if (this.matchesSession(queueKey, key))
|
|
1080
|
+
return promise;
|
|
1081
|
+
}
|
|
1082
|
+
return undefined;
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* 获取全局队列长度(所有会话的待处理消息总数)
|
|
1086
|
+
*/
|
|
1087
|
+
getGlobalQueueLength() {
|
|
1088
|
+
let total = 0;
|
|
1089
|
+
for (const queue of this.queues.values()) {
|
|
1090
|
+
total += queue.length;
|
|
1091
|
+
}
|
|
1092
|
+
return total;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* 获取全局处理中队列数量
|
|
1096
|
+
*/
|
|
1097
|
+
getGlobalProcessingCount() {
|
|
1098
|
+
return this.processing.size;
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* 获取指定 agent 的待处理消息数量。
|
|
1102
|
+
* agent 维度按 enqueue 时传入的 agentName 计数。
|
|
1103
|
+
* 可传入 excludeSessionKey 排除某个会话。
|
|
1104
|
+
*/
|
|
1105
|
+
getQueueLengthByAgent(agentName, excludeSessionKey) {
|
|
1106
|
+
let total = 0;
|
|
1107
|
+
for (const [key, queue] of this.queues.entries()) {
|
|
1108
|
+
if (excludeSessionKey && this.matchesSession(key, excludeSessionKey))
|
|
1109
|
+
continue;
|
|
1110
|
+
for (const item of queue) {
|
|
1111
|
+
if ((item.agentName || DEFAULT_AGENT_NAME) === agentName)
|
|
1112
|
+
total++;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return total;
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* 获取指定 agent 的处理中队列数量。
|
|
1119
|
+
* 可传入 excludeSessionKey 排除某个会话(用于 restart 等操作排除自身)。
|
|
1120
|
+
*/
|
|
1121
|
+
getProcessingCountByAgent(agentName, excludeSessionKey) {
|
|
1122
|
+
let total = 0;
|
|
1123
|
+
for (const [key, a] of this.processingAgent.entries()) {
|
|
1124
|
+
if (excludeSessionKey && this.matchesSession(key, excludeSessionKey))
|
|
1125
|
+
continue;
|
|
1126
|
+
if ((a || DEFAULT_AGENT_NAME) === agentName)
|
|
1127
|
+
total++;
|
|
1128
|
+
}
|
|
1129
|
+
return total;
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* 获取指定 agent 的处理中队列详情列表(用于阻塞提示,帮助 agent 判断等待时长)。
|
|
1133
|
+
* 每项包含 queueKey(可从中解析 sessionId 和路径)和 agentName。
|
|
1134
|
+
*/
|
|
1135
|
+
getProcessingDetailsByAgent(agentName, excludeSessionKey) {
|
|
1136
|
+
const results = [];
|
|
1137
|
+
for (const [key, a] of this.processingAgent.entries()) {
|
|
1138
|
+
if (excludeSessionKey && this.matchesSession(key, excludeSessionKey))
|
|
1139
|
+
continue;
|
|
1140
|
+
if ((a || DEFAULT_AGENT_NAME) === agentName) {
|
|
1141
|
+
results.push({ queueKey: key, agentName: a || DEFAULT_AGENT_NAME });
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return results;
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* 清空指定 agent 的待处理消息(不影响正在处理中的消息)。
|
|
1148
|
+
* 被移除的消息直接 resolve(与 cancel 一致),让 enqueue 的等待方正常解除阻塞。
|
|
1149
|
+
* @returns 被清除的消息数量
|
|
1150
|
+
*/
|
|
1151
|
+
clearByAgent(agentName) {
|
|
1152
|
+
let cleared = 0;
|
|
1153
|
+
for (const queue of this.queues.values()) {
|
|
1154
|
+
for (let i = queue.length - 1; i >= 0; i--) {
|
|
1155
|
+
if ((queue[i].agentName || DEFAULT_AGENT_NAME) === agentName) {
|
|
1156
|
+
const [removed] = queue.splice(i, 1);
|
|
1157
|
+
removed.resolve();
|
|
1158
|
+
cleared++;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if (cleared > 0) {
|
|
1163
|
+
this.persistQueuesImmediate();
|
|
1164
|
+
logger.info(`[Queue] Cleared ${cleared} pending message(s) for agent ${agentName}`);
|
|
1165
|
+
}
|
|
1166
|
+
return cleared;
|
|
1167
|
+
}
|
|
1168
|
+
/** 禁言 agent:后续消息照常入队,但 processNext 不再取出处理。 */
|
|
1169
|
+
muteAgent(agentName) {
|
|
1170
|
+
this.mutedAgents.add(agentName);
|
|
1171
|
+
logger.info(`[Queue] Muted agent ${agentName}`);
|
|
1172
|
+
}
|
|
1173
|
+
/** 解除禁言:重新触发该 agent 已积压、且当前未在处理的队列。 */
|
|
1174
|
+
unmuteAgent(agentName) {
|
|
1175
|
+
if (!this.mutedAgents.delete(agentName))
|
|
1176
|
+
return;
|
|
1177
|
+
logger.info(`[Queue] Unmuted agent ${agentName}, resuming queued messages`);
|
|
1178
|
+
for (const [key, queue] of this.queues) {
|
|
1179
|
+
if (queue.length > 0 && !this.processing.has(key)) {
|
|
1180
|
+
const headAgent = queue[0].agentName || DEFAULT_AGENT_NAME;
|
|
1181
|
+
if (headAgent === agentName)
|
|
1182
|
+
this.processNext(key);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
isAgentMuted(agentName) {
|
|
1187
|
+
return this.mutedAgents.has(agentName);
|
|
1188
|
+
}
|
|
1189
|
+
/** 中断指定 agent 所有正在处理中的会话。 */
|
|
1190
|
+
interruptByAgent(agentName) {
|
|
1191
|
+
let interrupted = 0;
|
|
1192
|
+
for (const [queueKey, name] of this.processingAgent) {
|
|
1193
|
+
if ((name || DEFAULT_AGENT_NAME) === agentName) {
|
|
1194
|
+
interrupted++;
|
|
1195
|
+
const sessionKey = this.sessionKeyFromQueueKey(queueKey);
|
|
1196
|
+
const activeState = this.activeStates.get(queueKey);
|
|
1197
|
+
logger.info(`[Queue] Interrupting session ${sessionKey} for agent ${agentName}`);
|
|
1198
|
+
this.eventBus?.publish({
|
|
1199
|
+
type: 'task:interrupted',
|
|
1200
|
+
sessionId: sessionKey,
|
|
1201
|
+
reason: 'stop',
|
|
1202
|
+
agentName: name,
|
|
1203
|
+
causation: this.activeBatches.get(queueKey)?.message.causation,
|
|
1204
|
+
});
|
|
1205
|
+
if (this.interruptCallback) {
|
|
1206
|
+
void this.triggerInterrupt(queueKey, sessionKey, activeState?.baseagent, name, 'stop');
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return interrupted;
|
|
1211
|
+
}
|
|
1212
|
+
// ── Queue query/management methods ──
|
|
1213
|
+
/**
|
|
1214
|
+
* 解析 sessionKey 的 channelType 和 channelId 组件。
|
|
1215
|
+
* sessionKey 格式:channelType#urlEncode(channelId)#urlEncode(threadId)
|
|
1216
|
+
*/
|
|
1217
|
+
parseSessionKey(sessionKey) {
|
|
1218
|
+
const parts = sessionKey.split('#');
|
|
1219
|
+
const channelType = parts[0] || '';
|
|
1220
|
+
let channelId = '';
|
|
1221
|
+
if (parts.length > 1) {
|
|
1222
|
+
try {
|
|
1223
|
+
channelId = decodeURIComponent(parts[1]);
|
|
1224
|
+
}
|
|
1225
|
+
catch {
|
|
1226
|
+
channelId = parts[1];
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
return { channelType, channelId };
|
|
1230
|
+
}
|
|
1231
|
+
/** 从 queueKey 提取 projectPath */
|
|
1232
|
+
projectPathFromQueueKey(queueKey) {
|
|
1233
|
+
const parts = queueKey.split('::');
|
|
1234
|
+
if (parts.length >= 3)
|
|
1235
|
+
return parts.slice(2).join('::');
|
|
1236
|
+
if (parts.length === 2)
|
|
1237
|
+
return parts[1];
|
|
1238
|
+
return queueKey;
|
|
1239
|
+
}
|
|
1240
|
+
/** 截断消息内容用于预览 */
|
|
1241
|
+
truncatePreview(content, maxLen = 80) {
|
|
1242
|
+
if (!content)
|
|
1243
|
+
return '';
|
|
1244
|
+
const cleaned = content.replace(/\s+/g, ' ').trim();
|
|
1245
|
+
return cleaned.length > maxLen ? cleaned.slice(0, maxLen) + '...' : cleaned;
|
|
1246
|
+
}
|
|
1247
|
+
/** 构建 QueueItemSnapshot */
|
|
1248
|
+
buildSnapshot(status, queueKey, sessionKey, item) {
|
|
1249
|
+
const { channelType, channelId } = this.parseSessionKey(sessionKey);
|
|
1250
|
+
const startTime = this.processingStartTime.get(queueKey);
|
|
1251
|
+
const elapsedMs = status === 'active' && startTime != null
|
|
1252
|
+
? Date.now() - startTime
|
|
1253
|
+
: undefined;
|
|
1254
|
+
return {
|
|
1255
|
+
status,
|
|
1256
|
+
sessionKey,
|
|
1257
|
+
channelType,
|
|
1258
|
+
channelId,
|
|
1259
|
+
projectPath: this.projectPathFromQueueKey(queueKey),
|
|
1260
|
+
peerName: item.message.peerName,
|
|
1261
|
+
preview: this.truncatePreview(item.message.content),
|
|
1262
|
+
messageId: item.message.messageId,
|
|
1263
|
+
elapsedMs,
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* 通过 human-readable sessionKey 反查 sessionId(cli interrupt 用)
|
|
1268
|
+
*/
|
|
1269
|
+
findSessionIdBySessionKey(sessionKey) {
|
|
1270
|
+
for (const [queueKey, sk] of this.queueKeyToSessionKey.entries()) {
|
|
1271
|
+
if (sk === sessionKey)
|
|
1272
|
+
return this.sessionKeyFromQueueKey(queueKey);
|
|
1273
|
+
}
|
|
1274
|
+
return undefined;
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* 按 sessionId 查询队列(ctl 用)
|
|
1278
|
+
* @param sessionId - Session.id(meta_YYYYMMDD_TS)
|
|
1279
|
+
* @returns 仅返回 pending 状态的消息
|
|
1280
|
+
*/
|
|
1281
|
+
getQueueItemsBySession(sessionId) {
|
|
1282
|
+
const result = [];
|
|
1283
|
+
for (const [queueKey, queue] of this.queues.entries()) {
|
|
1284
|
+
if (!this.matchesSession(queueKey, sessionId))
|
|
1285
|
+
continue;
|
|
1286
|
+
const sessionKey = this.queueKeyToSessionKey.get(queueKey)
|
|
1287
|
+
|| this.sessionKeyFromQueueKey(queueKey);
|
|
1288
|
+
for (const item of queue) {
|
|
1289
|
+
result.push(this.buildSnapshot('pending', queueKey, sessionKey, item));
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return result;
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* 按 agent 查询队列(cli 用)
|
|
1296
|
+
* @param agentName - agent 内部名称
|
|
1297
|
+
* @returns 返回所有状态(active + pending)的消息
|
|
1298
|
+
*/
|
|
1299
|
+
getQueueItemsByAgent(agentName) {
|
|
1300
|
+
const result = [];
|
|
1301
|
+
// active 项
|
|
1302
|
+
for (const [queueKey, item] of this.activeBatches.entries()) {
|
|
1303
|
+
const itemAgent = item.agentName || DEFAULT_AGENT_NAME;
|
|
1304
|
+
if (itemAgent !== agentName)
|
|
1305
|
+
continue;
|
|
1306
|
+
const sessionKey = this.queueKeyToSessionKey.get(queueKey)
|
|
1307
|
+
|| this.sessionKeyFromQueueKey(queueKey);
|
|
1308
|
+
result.push(this.buildSnapshot('active', queueKey, sessionKey, item));
|
|
1309
|
+
}
|
|
1310
|
+
// pending 项
|
|
1311
|
+
for (const [queueKey, queue] of this.queues.entries()) {
|
|
1312
|
+
for (const item of queue) {
|
|
1313
|
+
const itemAgent = item.agentName || DEFAULT_AGENT_NAME;
|
|
1314
|
+
if (itemAgent !== agentName)
|
|
1315
|
+
continue;
|
|
1316
|
+
const sessionKey = this.queueKeyToSessionKey.get(queueKey)
|
|
1317
|
+
|| this.sessionKeyFromQueueKey(queueKey);
|
|
1318
|
+
result.push(this.buildSnapshot('pending', queueKey, sessionKey, item));
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return result;
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* 按 sessionId 清空待处理消息(ctl 用)
|
|
1325
|
+
* @returns 被清除的消息数量
|
|
1326
|
+
*/
|
|
1327
|
+
clearBySession(sessionId) {
|
|
1328
|
+
let cleared = 0;
|
|
1329
|
+
for (const [queueKey, queue] of this.queues.entries()) {
|
|
1330
|
+
if (!this.matchesSession(queueKey, sessionId))
|
|
1331
|
+
continue;
|
|
1332
|
+
while (queue.length > 0) {
|
|
1333
|
+
const item = queue.shift();
|
|
1334
|
+
item.resolve();
|
|
1335
|
+
cleared++;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (cleared > 0) {
|
|
1339
|
+
this.persistQueuesImmediate();
|
|
1340
|
+
logger.info(`[Queue] Cleared ${cleared} pending message(s) for sessionId ${sessionId}`);
|
|
1341
|
+
}
|
|
1342
|
+
return cleared;
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* 按 messageId 取消消息(sessionId 作用域,ctl 用)
|
|
1346
|
+
* @returns 是否成功
|
|
1347
|
+
*/
|
|
1348
|
+
cancelMessageByIdInSession(sessionId, messageId) {
|
|
1349
|
+
for (const [queueKey, queue] of this.queues.entries()) {
|
|
1350
|
+
if (!this.matchesSession(queueKey, sessionId))
|
|
1351
|
+
continue;
|
|
1352
|
+
const idx = queue.findIndex(q => this.messageIdsFor(q).includes(messageId));
|
|
1353
|
+
if (idx === -1)
|
|
1354
|
+
continue;
|
|
1355
|
+
const target = queue[idx];
|
|
1356
|
+
const parts = this.partsOf(target);
|
|
1357
|
+
const partIdx = parts.findIndex(part => part.message.messageId === messageId);
|
|
1358
|
+
if (partIdx === -1)
|
|
1359
|
+
continue;
|
|
1360
|
+
if (parts.length === 1) {
|
|
1361
|
+
const [removed] = queue.splice(idx, 1);
|
|
1362
|
+
removed.resolve();
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
const [removed] = parts.splice(partIdx, 1);
|
|
1366
|
+
removed.resolve();
|
|
1367
|
+
queue[idx] = this.buildCoalescedItem(parts);
|
|
1368
|
+
}
|
|
1369
|
+
this.persistQueuesImmediate();
|
|
1370
|
+
logger.info(`[Queue] Cancelled queued message ${messageId} in session ${sessionId}`);
|
|
1371
|
+
return true;
|
|
1372
|
+
}
|
|
1373
|
+
return false;
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* 按 messageId 取消消息(agent 作用域,cli 用)
|
|
1377
|
+
* @returns 是否成功
|
|
1378
|
+
*/
|
|
1379
|
+
cancelMessageById(agentName, messageId) {
|
|
1380
|
+
for (const [queueKey, queue] of this.queues.entries()) {
|
|
1381
|
+
const idx = queue.findIndex(q => {
|
|
1382
|
+
if ((q.agentName || DEFAULT_AGENT_NAME) !== agentName)
|
|
1383
|
+
return false;
|
|
1384
|
+
return this.messageIdsFor(q).includes(messageId);
|
|
1385
|
+
});
|
|
1386
|
+
if (idx === -1)
|
|
1387
|
+
continue;
|
|
1388
|
+
const target = queue[idx];
|
|
1389
|
+
const parts = this.partsOf(target);
|
|
1390
|
+
const partIdx = parts.findIndex(part => part.message.messageId === messageId);
|
|
1391
|
+
if (partIdx === -1)
|
|
1392
|
+
continue;
|
|
1393
|
+
if (parts.length === 1) {
|
|
1394
|
+
const [removed] = queue.splice(idx, 1);
|
|
1395
|
+
removed.resolve();
|
|
1396
|
+
}
|
|
1397
|
+
else {
|
|
1398
|
+
const [removed] = parts.splice(partIdx, 1);
|
|
1399
|
+
removed.resolve();
|
|
1400
|
+
queue[idx] = this.buildCoalescedItem(parts);
|
|
1401
|
+
}
|
|
1402
|
+
this.persistQueuesImmediate();
|
|
1403
|
+
logger.info(`[Queue] Cancelled queued message ${messageId} for agent ${agentName}`);
|
|
1404
|
+
return true;
|
|
1405
|
+
}
|
|
1406
|
+
return false;
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* 打断 session 的处理中任务(ctl 用)
|
|
1410
|
+
* 一个 session 只可能有一个 queueKey。
|
|
1411
|
+
* @returns 是否有任务被打断
|
|
1412
|
+
*/
|
|
1413
|
+
async interruptBySession(sessionId) {
|
|
1414
|
+
let targetQueueKey;
|
|
1415
|
+
let targetAgentName;
|
|
1416
|
+
for (const queueKey of this.processing) {
|
|
1417
|
+
if (this.matchesSession(queueKey, sessionId)) {
|
|
1418
|
+
targetQueueKey = queueKey;
|
|
1419
|
+
targetAgentName = this.processingAgent.get(queueKey);
|
|
1420
|
+
break;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
if (!targetQueueKey)
|
|
1424
|
+
return false;
|
|
1425
|
+
const sessionKey = this.sessionKeyFromQueueKey(targetQueueKey);
|
|
1426
|
+
logger.info(`[Queue] Interrupting session ${sessionKey} (queueKey=${targetQueueKey})`);
|
|
1427
|
+
this.eventBus?.publish({
|
|
1428
|
+
type: 'task:interrupted',
|
|
1429
|
+
sessionId: sessionKey,
|
|
1430
|
+
reason: 'new_message',
|
|
1431
|
+
agentName: targetAgentName,
|
|
1432
|
+
causation: this.activeBatches.get(targetQueueKey)?.message.causation,
|
|
1433
|
+
});
|
|
1434
|
+
if (this.interruptCallback) {
|
|
1435
|
+
await this.triggerInterrupt(targetQueueKey, sessionKey, this.activeStates.get(targetQueueKey)?.baseagent, targetAgentName, 'stop');
|
|
1436
|
+
}
|
|
1437
|
+
return true;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
function abortError(reason) {
|
|
1441
|
+
if (reason instanceof Error)
|
|
1442
|
+
return reason;
|
|
1443
|
+
const error = new Error(typeof reason === 'string' && reason ? reason : 'message delivery aborted');
|
|
1444
|
+
error.name = 'AbortError';
|
|
1445
|
+
return error;
|
|
1446
|
+
}
|