evolcore 0.0.20 → 0.0.21
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 +22 -0
- package/README.md +58 -9
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +379 -108
- package/dist/agents/codex-app-server-client.js +10 -2
- package/dist/agents/codex-runner.js +402 -135
- package/dist/agents/ecagent-runner.js +171 -61
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +59 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +72 -6
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +5 -0
- package/dist/aun/outbox.js +182 -80
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +409 -88
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +69 -115
- package/dist/cli/init.js +27 -15
- package/dist/cli/task-context.js +46 -0
- package/dist/cli/trigger-command.js +1 -1
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +16 -5
- package/dist/config/config-manager.js +135 -17
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +20 -9
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +151 -9
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +31 -1
- package/dist/core/auth/auth-gateway.js +33 -4
- package/dist/core/auth/authorization-audit.js +150 -2
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-service.js +6 -2
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +290 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +279 -80
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +350 -32
- package/dist/core/event-catalog.js +5 -0
- package/dist/core/evolagent.js +4 -0
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +10 -0
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +83 -84
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +124 -10
- package/dist/core/message/message-log.js +14 -7
- package/dist/core/message/message-queue.js +206 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +486 -68
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +410 -54
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +159 -1
- package/dist/core/permission/tool-policy.js +575 -21
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +299 -30
- package/dist/core/session/session-renew.js +19 -12
- package/dist/core/session/session-turn-coordinator.js +11 -4
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/index.js +253 -46
- package/dist/ipc.js +374 -24
- package/dist/paths.js +64 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +56 -5
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +3 -2
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +21 -2
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/schemas/_meta.json +4 -2
- package/kits/schemas/agent-config.schema.11.json +13 -0
- package/kits/schemas/daemon.schema.5.json +0 -1
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +15 -3
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/package.json +6 -3
- package/skills/eclink/SKILL.md +2 -0
- package/dist/config/aun-gateway-config.js +0 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { hasModelSwitcher, hasPermissionController } from '../../agents/runner-types.js';
|
|
2
2
|
import { getCodexEfforts } from '../../agents/codex-runner.js';
|
|
3
|
+
import { normalizeBaseagent } from '../../agents/baseagent.js';
|
|
3
4
|
import { buildEnvelope, isDeliveryTargetForChannel } from '../message/message-utils.js';
|
|
4
5
|
import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
|
|
5
6
|
import { logger } from '../../utils/logger.js';
|
|
@@ -17,14 +18,15 @@ import { resolvePermissionMode, writeScope } from '../model/config-scope.js';
|
|
|
17
18
|
import { formatPeerKey } from '../relation/peer-identity.js';
|
|
18
19
|
import { modelMatches } from '../model/model-catalog.js';
|
|
19
20
|
import { formatModelCheck, runModelCheck } from '../model/model-diagnostics.js';
|
|
20
|
-
import { filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
|
|
21
|
+
import { constrainResolvedModelForRole, filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
|
|
22
|
+
import { resolveRuntimeStringField } from '../role/runtime-policy.js';
|
|
21
23
|
import { displaySessionTitle } from '../session/session-title.js';
|
|
22
24
|
import { chatmodeFieldForPeer, resolveChatModeForField } from '../message/peer-mode.js';
|
|
23
25
|
import { normalizePermissionMode as normalizePermissionModeContract, PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
|
|
24
26
|
import { isManagementRole } from '../../config/builtin-roles.js';
|
|
25
27
|
import { isSystemControlChannel } from '../system-channels.js';
|
|
26
28
|
import { spawnDetachedNode } from '../../utils/cross-platform.js';
|
|
27
|
-
import { guardIdleCommand, guardKnownCommand, guardRoleCommand, guardThreadCommand, isRecognizedSlashCommand, normalizeSlashContent, } from './slash-gate.js';
|
|
29
|
+
import { guardIdleCommand, guardKnownCommand, guardRoleCommand, guardThreadCommand, isRecognizedSlashCommand, normalizeSlashContent, probeSessionActivity, } from './slash-gate.js';
|
|
28
30
|
const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
29
31
|
const PERMISSION_MODE_KEYS = PUBLIC_PERMISSION_MODES;
|
|
30
32
|
function defaultSlashChatmode(field) {
|
|
@@ -114,7 +116,7 @@ function resolveSlashRelationTarget(params) {
|
|
|
114
116
|
const actualChatType = params.session?.chatType || params.chatType;
|
|
115
117
|
const peerId = actualChatType === 'group'
|
|
116
118
|
? (params.session?.metadata?.groupId || params.channelId)
|
|
117
|
-
: (params.userId || params.session?.metadata?.peerId);
|
|
119
|
+
: (params.userId || params.session?.metadata?.peerId || params.session?.channelId || params.channelId);
|
|
118
120
|
if (!peerId)
|
|
119
121
|
return { error: 'missing current peer id', code: 'MISSING_PEER' };
|
|
120
122
|
const channelType = params.session?.channelType || this.resolveChannelType?.(params.channel) || params.channel.split('#')[0];
|
|
@@ -419,10 +421,30 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
419
421
|
// 权限检查:区分用户级命令和管理级命令
|
|
420
422
|
const isOwner = identity.role === 'owner';
|
|
421
423
|
const isAdmin = isManagementRole(identity.role);
|
|
422
|
-
|
|
424
|
+
// A topic's latest snapshot is authoritative and intentionally strict. Do
|
|
425
|
+
// not let a corrupt committed record turn a backend-management command into
|
|
426
|
+
// an unhandled quick-command failure; /renew will report it as UNKNOWN and
|
|
427
|
+
// leave the binding untouched.
|
|
428
|
+
let topicAuthSession;
|
|
429
|
+
let topicAuthSessionLookupFailed = false;
|
|
430
|
+
if (threadId) {
|
|
431
|
+
try {
|
|
432
|
+
topicAuthSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
topicAuthSessionLookupFailed = true;
|
|
436
|
+
logger.warn(`[CommandHandler] topic session lookup failed: thread=${threadId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const activeChatType = topicAuthSession?.chatType
|
|
440
|
+
|| activeSession?.chatType
|
|
441
|
+
|| (chatType === 'group' ? 'group' : 'private');
|
|
423
442
|
const getExistingSessionForCommand = async () => {
|
|
424
|
-
if (threadId)
|
|
425
|
-
|
|
443
|
+
if (threadId) {
|
|
444
|
+
if (topicAuthSessionLookupFailed)
|
|
445
|
+
return undefined;
|
|
446
|
+
return topicAuthSession ?? await this.sessionManager.getThreadSession(channel, channelId, threadId);
|
|
447
|
+
}
|
|
426
448
|
return activeSession;
|
|
427
449
|
};
|
|
428
450
|
const getEffectiveChatmode = (session, fallbackChatType = activeChatType) => {
|
|
@@ -497,8 +519,103 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
497
519
|
agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
|
|
498
520
|
});
|
|
499
521
|
await this.processor.interruptSession(commandSession.id, 'stop');
|
|
522
|
+
this.processor?.clearPauseSession?.(commandSession.id);
|
|
500
523
|
}
|
|
501
524
|
};
|
|
525
|
+
/**
|
|
526
|
+
* Boundary-scoped activity probe shared by /renew and /compact. The queue
|
|
527
|
+
* snapshot is taken at gate installation; later arrivals are deliberately
|
|
528
|
+
* excluded because they are cutoff-after work and remain durably queued.
|
|
529
|
+
*/
|
|
530
|
+
const probeBackendActivity = (current, gateSnapshot) => {
|
|
531
|
+
const probes = {
|
|
532
|
+
paused: () => this.processor?.isPauseRequested?.(current.id) ?? false,
|
|
533
|
+
activeStream: () => this.getAgent(channel, current.baseagent).hasActiveStream(current.id),
|
|
534
|
+
};
|
|
535
|
+
if (this.permissionGateway) {
|
|
536
|
+
probes.pendingPermission = () => this.permissionGateway.getPendingRequests(current.id).length > 0;
|
|
537
|
+
}
|
|
538
|
+
if (this.interactionRouter) {
|
|
539
|
+
probes.pendingInteraction = () => this.interactionRouter.getPending(current.id).length > 0;
|
|
540
|
+
}
|
|
541
|
+
const selfAid = current.selfAID || selfAID || this.getOwningAgent(channel)?.aid;
|
|
542
|
+
if (this.handoffRuntime) {
|
|
543
|
+
probes.activeHandoff = () => {
|
|
544
|
+
if (!selfAid)
|
|
545
|
+
throw new Error('handoff activity owner unavailable');
|
|
546
|
+
return this.handoffRuntime.hasActiveSessionWork(selfAid, current.id);
|
|
547
|
+
};
|
|
548
|
+
probes.openHandoffTarget = () => {
|
|
549
|
+
if (!selfAid)
|
|
550
|
+
throw new Error('handoff activity owner unavailable');
|
|
551
|
+
return this.handoffRuntime.hasOpenTarget(selfAid, current.id);
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
return probeSessionActivity({
|
|
555
|
+
prequeue: gateSnapshot.prequeue,
|
|
556
|
+
active: gateSnapshot.active,
|
|
557
|
+
queued: gateSnapshot.queued,
|
|
558
|
+
dispatching: gateSnapshot.dispatching,
|
|
559
|
+
processing: !!current.processingState,
|
|
560
|
+
activeTurn: !!current.metadata?.turnState?.active,
|
|
561
|
+
}, probes);
|
|
562
|
+
};
|
|
563
|
+
const withIdleTopicBackend = async (sessionId, expectedThreadId, action, fallbackSession) => {
|
|
564
|
+
// Main-session /compact keeps its existing idle guard and provider path.
|
|
565
|
+
// Only topic backend operations require the stronger dequeue barrier.
|
|
566
|
+
if (!expectedThreadId) {
|
|
567
|
+
let current = fallbackSession;
|
|
568
|
+
if (!current) {
|
|
569
|
+
try {
|
|
570
|
+
current = typeof this.sessionManager.getSessionById === 'function'
|
|
571
|
+
? await this.sessionManager.getSessionById(sessionId)
|
|
572
|
+
: undefined;
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
logger.warn(`[CommandHandler] main session snapshot read failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
576
|
+
return { ok: false, reason: 'unknown' };
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (!current || current.threadId)
|
|
580
|
+
return { ok: false, reason: 'missing' };
|
|
581
|
+
// Preserve the legacy main-session /compact exclusion. Topic commands
|
|
582
|
+
// use the stronger barrier below; main sessions retain the historical
|
|
583
|
+
// keyed dequeue lock and provider behavior.
|
|
584
|
+
const releaseLock = typeof this.messageQueue.acquireLock === 'function'
|
|
585
|
+
? this.messageQueue.acquireLock(sessionId)
|
|
586
|
+
: undefined;
|
|
587
|
+
try {
|
|
588
|
+
return { ok: true, value: await action(current) };
|
|
589
|
+
}
|
|
590
|
+
finally {
|
|
591
|
+
releaseLock?.();
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (typeof this.messageQueue.withSessionBarrier !== 'function') {
|
|
595
|
+
return { ok: false, reason: 'unknown' };
|
|
596
|
+
}
|
|
597
|
+
return this.messageQueue.withSessionBarrier(sessionId, async (snapshot) => {
|
|
598
|
+
let current;
|
|
599
|
+
try {
|
|
600
|
+
current = typeof this.sessionManager.getSessionById === 'function'
|
|
601
|
+
? await this.sessionManager.getSessionById(sessionId)
|
|
602
|
+
: expectedThreadId
|
|
603
|
+
? await this.sessionManager.getThreadSession(channel, channelId, expectedThreadId)
|
|
604
|
+
: fallbackSession;
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
logger.warn(`[CommandHandler] session snapshot read failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
608
|
+
return { ok: false, reason: 'unknown' };
|
|
609
|
+
}
|
|
610
|
+
if (!current || (current.threadId || undefined) !== expectedThreadId) {
|
|
611
|
+
return { ok: false, reason: 'missing' };
|
|
612
|
+
}
|
|
613
|
+
const activity = probeBackendActivity(current, snapshot);
|
|
614
|
+
if (activity !== 'idle')
|
|
615
|
+
return { ok: false, reason: activity };
|
|
616
|
+
return { ok: true, value: await action(current, snapshot) };
|
|
617
|
+
});
|
|
618
|
+
};
|
|
502
619
|
// /help 命令不需要会话
|
|
503
620
|
if (normalizedContent === '/help') {
|
|
504
621
|
const canReadModel = canAccessSlashOperation('model.current', 'relation')
|
|
@@ -517,7 +634,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
517
634
|
const appendProcessCommands = (lines) => {
|
|
518
635
|
if (!isDaemonOwner)
|
|
519
636
|
return;
|
|
520
|
-
lines.push('', '🛠️ 进程级运维:', ' /restart - 重启服务', ' /upgrade - 检查版本更新', ' /reload [aid] - 热重载 Agent 配置');
|
|
637
|
+
lines.push('', '🛠️ 进程级运维:', ' /restart - 重启服务', ' /upgrade - 检查版本更新', ' /reload [aid] - 热重载 Agent 配置', ' /fa <提示词> - 单次使用宿主级 fullaccess 执行(别名: /fullaccess)');
|
|
521
638
|
};
|
|
522
639
|
if (!isAdmin && activeChatType === 'group') {
|
|
523
640
|
const lines = [
|
|
@@ -559,6 +676,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
559
676
|
' /s [cli|名称|序号|uuid] - 列出或切换会话(cli 查看未导入的 CLI 会话)',
|
|
560
677
|
' /name <新名称> - 重命名当前会话',
|
|
561
678
|
' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
|
|
679
|
+
...(threadId ? [' /renew - 在当前话题中轮换 backend'] : []),
|
|
562
680
|
' /pause - 在下一次工具调用前暂停当前任务',
|
|
563
681
|
' /resume - 继续已暂停的当前任务',
|
|
564
682
|
' /stop - 中断当前任务',
|
|
@@ -608,6 +726,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
608
726
|
' /fork [名称] - 分支当前会话(从当前对话点创建分支)',
|
|
609
727
|
' /rewind [N] [chat|file|all] - 查看历史/撤销指定轮次(别名: /rw)',
|
|
610
728
|
' /compact - 压缩会话上下文(减少 token 用量)',
|
|
729
|
+
' /renew - 在话题中轮换 backend(下一条消息懒初始化)',
|
|
611
730
|
' /pause - 在下一次工具调用前暂停当前任务',
|
|
612
731
|
' /resume - 继续已暂停的当前任务',
|
|
613
732
|
' /stop - 中断当前任务',
|
|
@@ -639,6 +758,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
639
758
|
...(isDaemonOwner ? [
|
|
640
759
|
' /restart - 重启服务',
|
|
641
760
|
' /reload [aid] - 热重载 Agent 配置',
|
|
761
|
+
' /fa <提示词> - 单次使用宿主级 fullaccess 执行(别名: /fullaccess)',
|
|
642
762
|
] : []),
|
|
643
763
|
...(!isDaemonOwner && isAdmin ? [
|
|
644
764
|
' /reload - 热重载当前 Agent 配置',
|
|
@@ -682,6 +802,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
682
802
|
cmds.push({ command: '/rewind', aliases: ['/rw'], args: '[N] [chat|file|all]', description: '查看历史/撤销指定轮次', category: '会话管理', roles: ['admin', 'owner'] });
|
|
683
803
|
cmds.push({ command: '/compact', description: '压缩会话上下文(减少 token 用量)', category: '会话管理', roles: ['admin', 'owner'] });
|
|
684
804
|
}
|
|
805
|
+
if (threadId && (activeChatType !== 'group' || isAdmin)) {
|
|
806
|
+
cmds.push({ command: '/renew', description: '轮换当前话题 backend', category: '会话管理', roles: activeChatType === 'group' ? ['admin', 'owner'] : ['visitor', 'member', 'admin', 'owner'] });
|
|
807
|
+
}
|
|
685
808
|
// Agent 与模型
|
|
686
809
|
if (isAdmin)
|
|
687
810
|
cmds.push({ command: '/baseagent', aliases: ['/base'], args: '[name]', description: '查看或切换 Agent 后端', category: 'Agent 与模型', roles: ['admin', 'owner'] });
|
|
@@ -707,6 +830,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
707
830
|
cmds.push({ command: '/observable', args: '[true|false]', description: '查看或切换观察者模式', category: '运维', roles: ['owner'] });
|
|
708
831
|
}
|
|
709
832
|
if (isDaemonOwner) {
|
|
833
|
+
cmds.push({ command: '/fa', aliases: ['/fullaccess'], args: '<提示词>', description: '仅让当前提示词对应的 run 使用宿主级 fullaccess', category: '权限管理', roles: ['daemon-owner'] });
|
|
710
834
|
cmds.push({ command: '/restart', description: '重启服务', category: '运维', roles: ['daemon-owner'] });
|
|
711
835
|
cmds.push({ command: '/upgrade', description: '检查版本更新', category: '运维', roles: ['daemon-owner'] });
|
|
712
836
|
cmds.push({ command: '/reload', args: '[aid]', description: '热重载 Agent 配置', category: '运维', roles: ['daemon-owner'] });
|
|
@@ -962,7 +1086,12 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
962
1086
|
}
|
|
963
1087
|
await interruptPausedSessionBeforeReplacement(await getExistingSessionForCommand());
|
|
964
1088
|
const previousDefaultBaseagent = owningAgent.baseagent || this.parseDefaultBaseagent();
|
|
965
|
-
|
|
1089
|
+
try {
|
|
1090
|
+
owningAgent.setActiveBaseagent(args);
|
|
1091
|
+
}
|
|
1092
|
+
catch (error) {
|
|
1093
|
+
return { kind: 'command.error', text: `❌ ${error instanceof Error ? error.message : String(error)}` };
|
|
1094
|
+
}
|
|
966
1095
|
this.eventBus.publish({
|
|
967
1096
|
type: 'agent:baseagent-changed',
|
|
968
1097
|
aid: owningAgent.aid,
|
|
@@ -1655,7 +1784,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
1655
1784
|
}).join('\n');
|
|
1656
1785
|
return { kind: 'command.error', text: `❌ 该 Agent 有 ${busyInfo.count} 个任务执行中,无法 reload。\n\n处理中:\n${processingLines}\n\n等待任务完成后重试,通常 30-60 秒。` };
|
|
1657
1786
|
}
|
|
1658
|
-
const res = await execAgentAction('reload', { aid: targetAid }, userId ?? '', this.eventBus);
|
|
1787
|
+
const res = await execAgentAction('reload', { aid: targetAid }, userId ?? '', this.eventBus, this.agentApplicationService);
|
|
1659
1788
|
if ('error' in res)
|
|
1660
1789
|
return { kind: 'command.error', text: `❌ reload 失败:${res.error}` };
|
|
1661
1790
|
return { kind: 'command.result', text: `✅ Agent ${targetAid} 配置已重载` };
|
|
@@ -2009,41 +2138,225 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2009
2138
|
if (normalizedContent === '/clear') {
|
|
2010
2139
|
return { kind: 'command.error', text: '⚠️ /clear 已移除\n\n请使用 /new [名称] 创建新会话来开始全新上下文。旧会话会保留,可通过 /s 查看或切换。' };
|
|
2011
2140
|
}
|
|
2012
|
-
// /
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
if (
|
|
2016
|
-
return { kind: 'command.error', text: '❌
|
|
2017
|
-
const sessionAgent = this.getAgent(channel, session.baseagent);
|
|
2018
|
-
if (!sessionAgent.capabilities?.compact) {
|
|
2019
|
-
return { kind: 'command.error', text: `❌ 当前 Agent (${sessionAgent.name}) 不支持 /compact` };
|
|
2141
|
+
// /renew:只支持话题会话。它废止当前 backend 绑定;下一条普通消息
|
|
2142
|
+
// 才会在新的 TurnLease 中懒创建并绑定新 backend。
|
|
2143
|
+
if (normalizedContent === '/renew' || /^\/renew\s/.test(normalizedContent)) {
|
|
2144
|
+
if (normalizedContent !== '/renew') {
|
|
2145
|
+
return { kind: 'command.error', text: '❌ /renew 不接受参数,请直接使用 /renew' };
|
|
2020
2146
|
}
|
|
2021
|
-
if (!
|
|
2022
|
-
return { kind: 'command.error', text: '
|
|
2147
|
+
if (!threadId) {
|
|
2148
|
+
return { kind: 'command.error', text: '⚠️ /renew 仅支持话题会话' };
|
|
2149
|
+
}
|
|
2150
|
+
// Card callbacks use synthetic message IDs on several adapters. They are
|
|
2151
|
+
// suitable for UI correlation, but not for the durable inbound claim.
|
|
2152
|
+
if (source === 'card-trigger') {
|
|
2153
|
+
return { kind: 'command.error', text: '⚠️ /renew 仅支持携带原生消息 ID 的普通消息' };
|
|
2154
|
+
}
|
|
2155
|
+
let renewSession;
|
|
2156
|
+
try {
|
|
2157
|
+
// Re-read rather than trusting the authorization lookup above: only a
|
|
2158
|
+
// fresh strict snapshot may be used to choose the barrier key.
|
|
2159
|
+
renewSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
|
|
2160
|
+
}
|
|
2161
|
+
catch (error) {
|
|
2162
|
+
logger.warn(`[CommandHandler] /renew topic lookup failed: thread=${threadId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2163
|
+
return { kind: 'command.error', text: '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试' };
|
|
2164
|
+
}
|
|
2165
|
+
if (!renewSession) {
|
|
2166
|
+
return { kind: 'command.error', text: '❌ 找不到当前话题会话' };
|
|
2167
|
+
}
|
|
2168
|
+
const inboundMessageId = messageId?.trim();
|
|
2169
|
+
if (!inboundMessageId) {
|
|
2170
|
+
return { kind: 'command.error', text: '❌ /renew 缺少稳定的消息 ID,当前渠道不支持安全重试' };
|
|
2171
|
+
}
|
|
2172
|
+
if (typeof this.sessionManager.rotateTopicBackend !== 'function'
|
|
2173
|
+
|| typeof this.messageQueue.withSessionBarrier !== 'function'
|
|
2174
|
+
|| typeof this.messageQueue.claimInbound !== 'function') {
|
|
2175
|
+
return { kind: 'command.error', text: '❌ 当前运行环境不支持安全 backend 轮换' };
|
|
2023
2176
|
}
|
|
2024
|
-
await interruptPausedSessionBeforeReplacement(session);
|
|
2025
|
-
const projectPath = path.isAbsolute(session.projectPath)
|
|
2026
|
-
? session.projectPath
|
|
2027
|
-
: path.resolve(process.cwd(), session.projectPath);
|
|
2028
|
-
const releaseLock = this.messageQueue.acquireLock(session.id);
|
|
2029
2177
|
try {
|
|
2178
|
+
const outcome = await this.messageQueue.withSessionBarrier(renewSession.id, async (snapshot) => {
|
|
2179
|
+
let current;
|
|
2180
|
+
try {
|
|
2181
|
+
current = typeof this.sessionManager.getSessionById === 'function'
|
|
2182
|
+
? await this.sessionManager.getSessionById(renewSession.id)
|
|
2183
|
+
: undefined;
|
|
2184
|
+
}
|
|
2185
|
+
catch (error) {
|
|
2186
|
+
logger.warn(`[CommandHandler] /renew strict session read failed: session=${renewSession.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2187
|
+
return { kind: 'unknown' };
|
|
2188
|
+
}
|
|
2189
|
+
if (!current || current.threadId !== threadId)
|
|
2190
|
+
return { kind: 'missing' };
|
|
2191
|
+
let claim;
|
|
2192
|
+
try {
|
|
2193
|
+
claim = this.messageQueue.claimInbound(current.id, inboundMessageId, selfAID ?? current.selfAID);
|
|
2194
|
+
}
|
|
2195
|
+
catch (error) {
|
|
2196
|
+
logger.error(`[CommandHandler] /renew inbound claim failed: session=${current.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2197
|
+
return { kind: 'claim_failed' };
|
|
2198
|
+
}
|
|
2199
|
+
if (claim === 'duplicate')
|
|
2200
|
+
return { kind: 'duplicate' };
|
|
2201
|
+
const activity = probeBackendActivity(current, snapshot);
|
|
2202
|
+
if (activity !== 'idle')
|
|
2203
|
+
return { kind: activity };
|
|
2204
|
+
const result = await this.sessionManager.rotateTopicBackend({
|
|
2205
|
+
sessionId: current.id,
|
|
2206
|
+
expectedThreadId: threadId,
|
|
2207
|
+
});
|
|
2208
|
+
return {
|
|
2209
|
+
kind: 'rotated',
|
|
2210
|
+
wasBound: result.wasBound,
|
|
2211
|
+
};
|
|
2212
|
+
});
|
|
2213
|
+
if (outcome.kind === 'missing') {
|
|
2214
|
+
return { kind: 'command.error', text: '❌ 当前话题会话已不存在或归属已变化' };
|
|
2215
|
+
}
|
|
2216
|
+
if (outcome.kind === 'claim_failed') {
|
|
2217
|
+
return { kind: 'command.error', text: '❌ 当前渠道无法安全记录该请求,未执行 backend 轮换' };
|
|
2218
|
+
}
|
|
2219
|
+
if (outcome.kind === 'duplicate') {
|
|
2220
|
+
return { kind: 'command.result', text: 'ℹ️ 该请求已接收过,不再重复执行' };
|
|
2221
|
+
}
|
|
2222
|
+
if (outcome.kind === 'busy' || outcome.kind === 'unknown') {
|
|
2223
|
+
return {
|
|
2224
|
+
kind: 'command.error',
|
|
2225
|
+
text: outcome.kind === 'busy'
|
|
2226
|
+
? '⚠️ 当前会话忙碌,无法安全轮换,请稍后再试'
|
|
2227
|
+
: '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试',
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
if (outcome.kind !== 'rotated') {
|
|
2231
|
+
return { kind: 'command.error', text: '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试' };
|
|
2232
|
+
}
|
|
2233
|
+
return {
|
|
2234
|
+
kind: 'command.result',
|
|
2235
|
+
text: `✅ backend 已切换${outcome.wasBound ? '' : '(当前未绑定 backend)'};下一条消息将初始化新 backend(阶段一不携带历史摘要)`,
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
catch (error) {
|
|
2239
|
+
logger.error(`[CommandHandler] /renew failed: session=${renewSession.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2240
|
+
return { kind: 'command.error', text: `❌ backend 轮换失败:${error instanceof Error ? error.message : String(error)}` };
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
// /compact 命令:手动压缩会话上下文
|
|
2244
|
+
if (normalizedContent === '/compact') {
|
|
2245
|
+
const session = await getExistingSessionForCommand();
|
|
2246
|
+
if (!session) {
|
|
2247
|
+
return topicAuthSessionLookupFailed
|
|
2248
|
+
? { kind: 'command.error', text: '⚠️ 当前会话状态未知,无法安全压缩,请稍后再试' }
|
|
2249
|
+
: { kind: 'command.error', text: '❌ 当前没有活跃会话,无需压缩' };
|
|
2250
|
+
}
|
|
2251
|
+
if (!session.threadId)
|
|
2252
|
+
await interruptPausedSessionBeforeReplacement(session);
|
|
2253
|
+
const outcome = await withIdleTopicBackend(session.id, session.threadId || undefined, async (session) => {
|
|
2254
|
+
if (!session.agentSessionId) {
|
|
2255
|
+
return { kind: 'command.error', text: '❌ 当前会话没有历史记录,无需压缩' };
|
|
2256
|
+
}
|
|
2257
|
+
const projectPath = path.isAbsolute(session.projectPath)
|
|
2258
|
+
? session.projectPath
|
|
2259
|
+
: path.resolve(process.cwd(), session.projectPath);
|
|
2260
|
+
const currentSessionAgent = this.getAgent(channel, session.baseagent);
|
|
2261
|
+
if (!currentSessionAgent.capabilities?.compact) {
|
|
2262
|
+
return { kind: 'command.error', text: `❌ 当前 Agent (${currentSessionAgent.name}) 不支持 /compact` };
|
|
2263
|
+
}
|
|
2030
2264
|
if (sendMessage) {
|
|
2031
2265
|
await sendMessage(channelId, '⏳ 正在压缩会话上下文...', this.getReplyContext(session));
|
|
2032
2266
|
}
|
|
2033
|
-
|
|
2034
|
-
|
|
2267
|
+
// Resolve the same relation/agent effective model used for normal turns.
|
|
2268
|
+
// The runner instance's model is only the final fallback; without this
|
|
2269
|
+
// override Claude's /compact command could resume with a stale agent
|
|
2270
|
+
// default (for example glm-5.3) while the turn itself used gpt-5.6-sol.
|
|
2271
|
+
const compactBaseagentRaw = session.baseagent || currentSessionAgent.name;
|
|
2272
|
+
const compactBaseagentNormalized = normalizeBaseagent(compactBaseagentRaw);
|
|
2273
|
+
const compactBaseagent = compactBaseagentNormalized.canonical === 'unknown'
|
|
2274
|
+
? compactBaseagentRaw
|
|
2275
|
+
: compactBaseagentNormalized.canonical;
|
|
2276
|
+
const compactChatType = session.chatType;
|
|
2277
|
+
const compactRelation = resolveSlashRelationTarget.call(this, {
|
|
2278
|
+
session,
|
|
2279
|
+
channel,
|
|
2280
|
+
channelId,
|
|
2281
|
+
userId,
|
|
2282
|
+
selfAID,
|
|
2283
|
+
role: session.identity?.role || identity.role,
|
|
2284
|
+
chatType: compactChatType,
|
|
2285
|
+
});
|
|
2286
|
+
const compactSelector = 'error' in compactRelation
|
|
2287
|
+
? { self: selfAID ?? session.selfAID ?? this.resolveSelfAID(channel) }
|
|
2288
|
+
: compactRelation;
|
|
2289
|
+
let compactConfig = {};
|
|
2290
|
+
try {
|
|
2291
|
+
compactConfig = (resolveEffective(compactSelector, { cache: true }).baseagents || {})[compactBaseagent] || {};
|
|
2292
|
+
}
|
|
2293
|
+
catch {
|
|
2294
|
+
compactConfig = {};
|
|
2295
|
+
}
|
|
2296
|
+
const compactModelDecision = constrainResolvedModelForRole({
|
|
2297
|
+
role: session.identity?.role || identity.role,
|
|
2298
|
+
baseagent: compactBaseagent,
|
|
2299
|
+
model: typeof compactConfig.model === 'string' ? compactConfig.model : undefined,
|
|
2300
|
+
resolveModelId: typeof currentSessionAgent.resolveModelId === 'function'
|
|
2301
|
+
? currentSessionAgent.resolveModelId.bind(currentSessionAgent)
|
|
2302
|
+
: undefined,
|
|
2303
|
+
selfAid: compactSelector.self,
|
|
2304
|
+
});
|
|
2305
|
+
const compactModel = compactModelDecision.model
|
|
2306
|
+
|| (typeof currentSessionAgent.getModel === 'function' ? currentSessionAgent.getModel() : undefined);
|
|
2307
|
+
const compactEffortDecision = resolveRuntimeStringField({
|
|
2308
|
+
selfAid: compactSelector.self,
|
|
2309
|
+
role: session.identity?.role || identity.role,
|
|
2310
|
+
field: `baseagents.${compactBaseagent}.effort`,
|
|
2311
|
+
configuredValue: compactConfig.effort,
|
|
2312
|
+
});
|
|
2313
|
+
const compactConfiguredEffort = compactEffortDecision.effectiveValue;
|
|
2314
|
+
const compactEffort = compactConfiguredEffort && compactConfiguredEffort !== 'auto'
|
|
2315
|
+
? compactConfiguredEffort
|
|
2316
|
+
: (compactConfiguredEffort === 'auto' ? undefined : currentSessionAgent.getEffort?.());
|
|
2317
|
+
// Only send an explicit override when config/role resolution decided a
|
|
2318
|
+
// value. If neither scope provides one, the runner's own getModel/getEffort
|
|
2319
|
+
// fallback is the same effective value and preserving an omitted fourth
|
|
2320
|
+
// argument keeps older runner adapters source-compatible.
|
|
2321
|
+
const hasExplicitCompactModel = typeof compactConfig.model === 'string' && compactConfig.model.trim().length > 0
|
|
2322
|
+
|| compactModelDecision.constrained;
|
|
2323
|
+
const hasExplicitCompactEffort = typeof compactConfig.effort === 'string' && compactConfig.effort.trim().length > 0
|
|
2324
|
+
|| compactEffortDecision.decidedBy === 'role';
|
|
2325
|
+
const compactOverride = (hasExplicitCompactModel && compactModel) || (hasExplicitCompactEffort && compactEffort)
|
|
2326
|
+
? {
|
|
2327
|
+
...(hasExplicitCompactModel && compactModel ? { model: compactModel } : {}),
|
|
2328
|
+
...(hasExplicitCompactEffort && compactEffort ? { effort: compactEffort } : {}),
|
|
2329
|
+
}
|
|
2330
|
+
: undefined;
|
|
2331
|
+
const compacted = compactOverride
|
|
2332
|
+
? await currentSessionAgent.compactSession(session.id, session.agentSessionId, projectPath, compactOverride)
|
|
2333
|
+
: await currentSessionAgent.compactSession(session.id, session.agentSessionId, projectPath);
|
|
2334
|
+
const compactResult = compacted;
|
|
2335
|
+
if (compactResult === true || compactResult?.ok === true) {
|
|
2035
2336
|
return {
|
|
2036
2337
|
kind: 'command.result',
|
|
2037
2338
|
text: '✅ 会话压缩完成',
|
|
2038
2339
|
};
|
|
2039
2340
|
}
|
|
2040
2341
|
else {
|
|
2041
|
-
return {
|
|
2342
|
+
return {
|
|
2343
|
+
kind: 'command.error',
|
|
2344
|
+
text: `❌ 会话压缩失败(${compactResult?.code ?? 'sdk_error'}):${compactResult?.message ?? 'compact failed'}`,
|
|
2345
|
+
};
|
|
2042
2346
|
}
|
|
2347
|
+
}, session);
|
|
2348
|
+
if (!outcome.ok) {
|
|
2349
|
+
if (outcome.reason === 'missing') {
|
|
2350
|
+
return { kind: 'command.error', text: '❌ 当前会话已不存在或归属已变化' };
|
|
2351
|
+
}
|
|
2352
|
+
return {
|
|
2353
|
+
kind: 'command.error',
|
|
2354
|
+
text: outcome.reason === 'busy'
|
|
2355
|
+
? '⚠️ 当前会话忙碌,请稍后再试\n使用 /stop 中断当前任务后重试'
|
|
2356
|
+
: '⚠️ 当前会话状态未知,无法安全压缩,请稍后再试',
|
|
2357
|
+
};
|
|
2043
2358
|
}
|
|
2044
|
-
|
|
2045
|
-
releaseLock();
|
|
2046
|
-
}
|
|
2359
|
+
return outcome.value;
|
|
2047
2360
|
}
|
|
2048
2361
|
// 后续命令可读取现有会话,但不应在这里隐式创建新会话。
|
|
2049
2362
|
// 真正需要新会话的命令应显式调用 createNewSession()。
|
|
@@ -3203,7 +3516,12 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
3203
3516
|
const backupPath = await backupSessionFile(sessionFile);
|
|
3204
3517
|
const fsPromises = await import('fs/promises');
|
|
3205
3518
|
await fsPromises.unlink(sessionFile);
|
|
3206
|
-
|
|
3519
|
+
// A topic backend binding may only be changed by an explicit boundary
|
|
3520
|
+
// rotation/activation. Repair clears runner-local state, but must not
|
|
3521
|
+
// turn a legacy empty callback into an authoritative binding mutation.
|
|
3522
|
+
if (!repairSession.threadId) {
|
|
3523
|
+
await this.sessionManager.updateAgentSessionIdBySessionId(repairSession.id, '');
|
|
3524
|
+
}
|
|
3207
3525
|
repairAgent.updateSessionId(repairSession.id, '');
|
|
3208
3526
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
3209
3527
|
return { kind: 'command.result', text: `✓ 修复完成\n\n检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n修复操作:\n- 已备份损坏文件\n- 已删除损坏文件\n- 已重置异常计数器\n\n备份位置:${backupPath}` };
|
|
@@ -3240,7 +3558,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
3240
3558
|
});
|
|
3241
3559
|
if (triggerAuthDenied)
|
|
3242
3560
|
return triggerAuthDenied;
|
|
3243
|
-
const text = await this.handleTrigger(normalizedContent, channel, channelId, userId ?? '', isAdmin, messageId, chatType, threadId);
|
|
3561
|
+
const text = await this.handleTrigger(normalizedContent, channel, channelId, userId ?? '', isAdmin, messageId, chatType, threadId, isDaemonOwner, authSubject.canApprovePersistentFullAccess);
|
|
3244
3562
|
return { kind: 'command.result', text };
|
|
3245
3563
|
}
|
|
3246
3564
|
return null;
|
|
@@ -317,6 +317,7 @@ const CATALOG = [
|
|
|
317
317
|
{ path: 'eventPhase', type: 'string' },
|
|
318
318
|
{ path: 'callId', type: 'string', optional: true },
|
|
319
319
|
{ path: 'correlationId', type: 'string', optional: true },
|
|
320
|
+
{ path: 'agentName', type: 'string', optional: true },
|
|
320
321
|
{ path: 'agentAid', type: 'string', optional: true },
|
|
321
322
|
{ path: 'permissionMode', type: 'string', optional: true },
|
|
322
323
|
{ path: 'decision', type: 'string' },
|
|
@@ -373,6 +374,8 @@ const CATALOG = [
|
|
|
373
374
|
{ path: 'approved', type: 'boolean' },
|
|
374
375
|
{ path: 'toolName', type: 'string', optional: true },
|
|
375
376
|
{ path: 'reason', type: 'string', optional: true },
|
|
377
|
+
{ path: 'decisionSource', type: 'string', optional: true },
|
|
378
|
+
{ path: 'policyCode', type: 'string', optional: true },
|
|
376
379
|
],
|
|
377
380
|
},
|
|
378
381
|
{
|
|
@@ -385,6 +388,8 @@ const CATALOG = [
|
|
|
385
388
|
{ path: 'requestId', type: 'string' },
|
|
386
389
|
{ path: 'toolName', type: 'string', optional: true },
|
|
387
390
|
{ path: 'reason', type: 'string', optional: true },
|
|
391
|
+
{ path: 'decisionSource', type: 'string', optional: true },
|
|
392
|
+
{ path: 'policyCode', type: 'string', optional: true },
|
|
388
393
|
],
|
|
389
394
|
},
|
|
390
395
|
{
|
package/dist/core/evolagent.js
CHANGED
|
@@ -6,6 +6,7 @@ import { ConfigTarget, read as cfgRead, write as cfgWrite, ensureFile as cfgEnsu
|
|
|
6
6
|
import { withLifecycleForWrite } from '../config/lifecycle.js';
|
|
7
7
|
import { isStaticAgentAdmin, isStaticAgentOwner } from '../config/peer-role-resolver.js';
|
|
8
8
|
import { resolvePrincipal } from '../config/contact-book.js';
|
|
9
|
+
import { isSupportedBaseagent } from '../agents/baseagent.js';
|
|
9
10
|
/**
|
|
10
11
|
* EvolAgent —— 一个 self-agent 的运行时表示。
|
|
11
12
|
*
|
|
@@ -168,6 +169,9 @@ export class EvolAgent {
|
|
|
168
169
|
// ── Baseagent 字段写入 ────────────────────────────
|
|
169
170
|
/** 切换当前活跃 baseagent(写入 config.json)。 */
|
|
170
171
|
setActiveBaseagent(value) {
|
|
172
|
+
if (value !== undefined && !isSupportedBaseagent(value)) {
|
|
173
|
+
throw new Error(`Unsupported baseagent: ${value} (choose: claude/codex/gemini/ecagent)`);
|
|
174
|
+
}
|
|
171
175
|
this.merged.active_baseagent = value;
|
|
172
176
|
this.updateAgentConfig(b => {
|
|
173
177
|
if (value === undefined)
|
|
@@ -63,6 +63,10 @@ export class HandoffDispatcher {
|
|
|
63
63
|
isAgentPaused(selfAid) {
|
|
64
64
|
return this.pausedAgents.has(selfAid);
|
|
65
65
|
}
|
|
66
|
+
/** Whether delivery work for this exact target is currently executing. */
|
|
67
|
+
isRunning(selfAid, targetSessionId) {
|
|
68
|
+
return this.running.has(this.key(selfAid, targetSessionId));
|
|
69
|
+
}
|
|
66
70
|
async drain(selfAid, targetSessionId) {
|
|
67
71
|
const key = this.key(selfAid, targetSessionId);
|
|
68
72
|
if (this.running.has(key)) {
|
|
@@ -150,6 +150,16 @@ export class HandoffRuntime {
|
|
|
150
150
|
.some(instance => instance.state === 'target_sent'
|
|
151
151
|
|| (instance.state === 'return_pending' && instance.return_policy === 'required'));
|
|
152
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Probe only in-flight delivery/return work. Persisted handoffs that are
|
|
155
|
+
* merely waiting in the store may cross a backend generation; the queue or
|
|
156
|
+
* interaction probe handles the active parts separately.
|
|
157
|
+
*/
|
|
158
|
+
hasActiveSessionWork(selfAid, sessionId) {
|
|
159
|
+
return this.dispatcher.isRunning(selfAid, sessionId)
|
|
160
|
+
|| this.store.list(selfAid).some(instance => (instance.origin_session_id === sessionId
|
|
161
|
+
|| instance.target_session_id === sessionId) && instance.state === 'origin_queued' && this.dispatcher.isRunning(selfAid, instance.target_session_id));
|
|
162
|
+
}
|
|
153
163
|
async bindReply(input) {
|
|
154
164
|
const key = `${input.selfAid}\u0000${input.targetSessionId}`;
|
|
155
165
|
return this.mutexes.forKey(key).runExclusive(async () => {
|