evolcore 0.0.17 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/bin/codex-managed-hook.mjs +16 -7
- package/bin/install-codex-managed-hooks.mjs +4 -2
- package/dist/agents/claude-runner.js +113 -24
- package/dist/agents/codex-app-server-client.js +6 -1
- package/dist/agents/codex-runner.js +21 -6
- package/dist/agents/ecagent-runner.js +39 -10
- package/dist/agents/gemini-runner.js +90 -19
- package/dist/aun/aid/store.js +36 -0
- package/dist/aun/msg/p2p.js +20 -8
- package/dist/channels/aun.js +159 -21
- package/dist/cli/agent-command.js +67 -6
- package/dist/cli/agent.js +26 -0
- package/dist/cli/command-log.js +23 -4
- package/dist/cli/daemon-commands.js +53 -12
- package/dist/cli/init.js +21 -5
- package/dist/cli/restart-monitor.js +13 -6
- package/dist/cli/watch-logs.js +2 -2
- package/dist/config/builtin-roles.js +5 -1
- package/dist/config/role-ranks.js +4 -0
- package/dist/core/audit/event-key.js +29 -0
- package/dist/core/audit/log-integrity.js +13 -3
- package/dist/core/auth/auth-gateway.js +14 -18
- package/dist/core/auth/authorization-audit.js +110 -3
- package/dist/core/auth/authorization-denial.js +17 -0
- package/dist/core/auth/operation-authorizer.js +143 -18
- package/dist/core/auth/operation-catalog.js +21 -5
- package/dist/core/bootstrap-messages.js +11 -6
- package/dist/core/bootstrap-service.js +26 -4
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +25 -16
- package/dist/core/command/command-handler.js +50 -4
- package/dist/core/command/group-menu.js +1 -1
- package/dist/core/command/menu-catalog.js +32 -7
- package/dist/core/command/menu-handler.js +59 -23
- package/dist/core/command/menu-protocol.js +196 -0
- package/dist/core/command/slash-gate.js +14 -5
- package/dist/core/command/slash-handler.js +81 -99
- package/dist/core/event-catalog.js +18 -0
- package/dist/core/message/message-bridge.js +72 -9
- package/dist/core/message/pause-controller.js +53 -0
- package/dist/core/message/response-engine.js +97 -11
- package/dist/core/permission/sandbox-runtime.js +79 -13
- package/dist/core/permission/tool-policy.js +1 -1
- package/dist/index.js +357 -48
- package/dist/ipc.js +75 -4
- package/dist/utils/atomic-write.js +45 -11
- package/dist/utils/logger.js +27 -0
- package/dist/utils/windows-autostart.js +740 -83
- package/ecagent/dist/harness/agent-harness.d.ts +1 -1
- package/ecagent/dist/harness/agent-harness.js +6 -4
- package/kits/docs/evolcore/config.md +1 -1
- package/kits/docs/evolcore/group-rules.md +2 -1
- package/kits/docs/identity/ROLE_DETAIL.md +3 -1
- package/kits/eck_manifest.json +25 -16
- package/kits/rules/01-overview.md +5 -5
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/04-relation.md +4 -4
- package/kits/rules/05-venue.md +5 -5
- package/kits/templates/bootstrap-welcome.md +3 -1
- package/kits/templates/system-fragments/bootstrap.md +17 -9
- package/package.json +1 -1
|
@@ -7,18 +7,18 @@ import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
|
|
|
7
7
|
import crypto from 'crypto';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import fs from 'fs';
|
|
10
|
-
import os from 'os';
|
|
11
10
|
import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions } from '../../utils/npm-ops.js';
|
|
12
11
|
import { loadDaemonConfig } from '../../config-store.js';
|
|
13
12
|
import { read as cfgRead, resolveEffective, routeFieldPath, write as cfgWrite, } from '../../config/config-manager.js';
|
|
14
13
|
import { execAgentAction } from './agent-control.js';
|
|
15
14
|
import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
|
|
15
|
+
import { authorizationNextStep, formatAuthorizationDenial } from '../auth/authorization-denial.js';
|
|
16
16
|
import { resolvePermissionMode, writeScope } from '../model/config-scope.js';
|
|
17
17
|
import { formatPeerKey } from '../relation/peer-identity.js';
|
|
18
18
|
import { modelMatches } from '../model/model-catalog.js';
|
|
19
19
|
import { formatModelCheck, runModelCheck } from '../model/model-diagnostics.js';
|
|
20
20
|
import { filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
|
|
21
|
-
import { displaySessionTitle
|
|
21
|
+
import { displaySessionTitle } from '../session/session-title.js';
|
|
22
22
|
import { chatmodeFieldForPeer, resolveChatModeForField } from '../message/peer-mode.js';
|
|
23
23
|
import { normalizePermissionMode as normalizePermissionModeContract, PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
|
|
24
24
|
import { isManagementRole } from '../../config/builtin-roles.js';
|
|
@@ -157,7 +157,7 @@ async function authorizeSlashIntent(params) {
|
|
|
157
157
|
const channelType = channel.split('#')[0];
|
|
158
158
|
const peerKey = channelType && peerKeyId ? formatPeerKey(channelType, peerKeyId) : undefined;
|
|
159
159
|
const subject = params.subject
|
|
160
|
-
? { ...params.subject,
|
|
160
|
+
? { ...params.subject, identity }
|
|
161
161
|
: buildAuthSubject({
|
|
162
162
|
selfAid,
|
|
163
163
|
actorId: userId,
|
|
@@ -175,7 +175,13 @@ async function authorizeSlashIntent(params) {
|
|
|
175
175
|
}
|
|
176
176
|
const decision = await authorizeOperation({ source: 'slash', intent, subject });
|
|
177
177
|
if (!decision.allow) {
|
|
178
|
-
|
|
178
|
+
const reasonCode = decision.command?.reasonCode;
|
|
179
|
+
return {
|
|
180
|
+
kind: 'command.error',
|
|
181
|
+
text: formatAuthorizationDenial(decision.reason, reasonCode),
|
|
182
|
+
reason: decision.code,
|
|
183
|
+
...(reasonCode ? { reasonCode, nextStep: authorizationNextStep(reasonCode) } : {}),
|
|
184
|
+
};
|
|
179
185
|
}
|
|
180
186
|
return null;
|
|
181
187
|
}
|
|
@@ -471,6 +477,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
471
477
|
activeAgent: getActiveAgentIfAvailable(),
|
|
472
478
|
sessionManager: this.sessionManager,
|
|
473
479
|
messageQueue: this.messageQueue,
|
|
480
|
+
isSessionPaused: sessionId => this.processor?.isPauseRequested?.(sessionId) ?? false,
|
|
474
481
|
getAgentForSession: session => this.getAgent(channel, session.baseagent),
|
|
475
482
|
});
|
|
476
483
|
if (idleGuard)
|
|
@@ -481,6 +488,17 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
481
488
|
const isCmd = isRecognizedSlashCommand(normalizedContent);
|
|
482
489
|
if (!isCmd)
|
|
483
490
|
return undefined;
|
|
491
|
+
const interruptPausedSessionBeforeReplacement = async (commandSession) => {
|
|
492
|
+
if (commandSession && (this.processor?.isPauseRequested?.(commandSession.id) ?? false)) {
|
|
493
|
+
this.eventBus.publish({
|
|
494
|
+
type: 'task:interrupted',
|
|
495
|
+
sessionId: commandSession.id,
|
|
496
|
+
reason: 'stop',
|
|
497
|
+
agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
|
|
498
|
+
});
|
|
499
|
+
await this.processor.interruptSession(commandSession.id, 'stop');
|
|
500
|
+
}
|
|
501
|
+
};
|
|
484
502
|
// /help 命令不需要会话
|
|
485
503
|
if (normalizedContent === '/help') {
|
|
486
504
|
const canReadModel = canAccessSlashOperation('model.current', 'relation')
|
|
@@ -541,6 +559,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
541
559
|
' /s [cli|名称|序号|uuid] - 列出或切换会话(cli 查看未导入的 CLI 会话)',
|
|
542
560
|
' /name <新名称> - 重命名当前会话',
|
|
543
561
|
' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
|
|
562
|
+
' /pause - 在下一次工具调用前暂停当前任务',
|
|
563
|
+
' /resume - 继续已暂停的当前任务',
|
|
564
|
+
' /stop - 中断当前任务',
|
|
544
565
|
' /status - 显示会话状态',
|
|
545
566
|
' /check - 检查 EvolAgent 实例健康',
|
|
546
567
|
...((canReadModel || canUseModel || canSetEffort) ? [
|
|
@@ -587,6 +608,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
587
608
|
' /fork [名称] - 分支当前会话(从当前对话点创建分支)',
|
|
588
609
|
' /rewind [N] [chat|file|all] - 查看历史/撤销指定轮次(别名: /rw)',
|
|
589
610
|
' /compact - 压缩会话上下文(减少 token 用量)',
|
|
611
|
+
' /pause - 在下一次工具调用前暂停当前任务',
|
|
612
|
+
' /resume - 继续已暂停的当前任务',
|
|
613
|
+
' /stop - 中断当前任务',
|
|
590
614
|
'',
|
|
591
615
|
'🤖 Agent 与模型:',
|
|
592
616
|
' /baseagent [name] - 查看或切换 Agent 后端(别名: /base)',
|
|
@@ -605,6 +629,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
605
629
|
'',
|
|
606
630
|
'🛠️ 运维:',
|
|
607
631
|
' /status - 显示会话状态',
|
|
632
|
+
' /pause - 在下一次工具调用前暂停当前任务',
|
|
633
|
+
' /resume - 继续已暂停的当前任务',
|
|
608
634
|
' /stop - 中断当前任务',
|
|
609
635
|
' /check - 检查 EvolAgent 实例健康',
|
|
610
636
|
...(isOwner ? [
|
|
@@ -670,6 +696,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
670
696
|
cmds.push({ command: '/perm', args: 'allow|always|deny', description: '审批权限请求', category: '权限管理', roles: [identity.role] });
|
|
671
697
|
// 运维
|
|
672
698
|
cmds.push({ command: '/status', description: '显示会话状态', category: '运维', roles: ['visitor', 'member', 'admin', 'owner'] });
|
|
699
|
+
cmds.push({ command: '/pause', description: '在下一次工具调用前暂停当前任务', category: '运维', roles: ['admin', 'owner'] });
|
|
700
|
+
cmds.push({ command: '/resume', description: '继续已暂停的当前任务', category: '运维', roles: ['admin', 'owner'] });
|
|
673
701
|
cmds.push({ command: '/stop', description: '中断当前任务', category: '运维', roles: ['admin', 'owner'] });
|
|
674
702
|
cmds.push({ command: '/check', description: '检查 EvolAgent 实例健康', category: '运维', roles: ['visitor', 'member', 'admin', 'owner'] });
|
|
675
703
|
if (isAdmin) {
|
|
@@ -851,92 +879,36 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
851
879
|
return { kind: 'command.result', text: fb.result ?? '✓ 已回答' };
|
|
852
880
|
return { kind: 'command.error', text: '❌ 当前没有待回答的问题' };
|
|
853
881
|
}
|
|
854
|
-
// /resume
|
|
855
|
-
if (normalizedContent
|
|
882
|
+
// /pause and /resume control the current session's next-tool gate.
|
|
883
|
+
if (normalizedContent.startsWith('/pause ')) {
|
|
884
|
+
return { kind: 'command.error', text: '用法: /pause' };
|
|
885
|
+
}
|
|
886
|
+
if (normalizedContent === '/pause') {
|
|
887
|
+
const pauseSession = await getExistingSessionForCommand();
|
|
888
|
+
if (!pauseSession)
|
|
889
|
+
return { kind: 'command.result', text: '当前没有活跃会话' };
|
|
890
|
+
const pauseAgent = this.getAgent(channel, pauseSession.baseagent);
|
|
891
|
+
const hasActiveTask = pauseAgent.hasActiveStream(pauseSession.id)
|
|
892
|
+
|| this.messageQueue.isProcessing(pauseSession.id);
|
|
893
|
+
if (!hasActiveTask)
|
|
894
|
+
return { kind: 'command.result', text: '当前没有正在处理的任务' };
|
|
895
|
+
const requested = this.processor.pauseSession(pauseSession.id);
|
|
896
|
+
return {
|
|
897
|
+
kind: 'command.result',
|
|
898
|
+
text: requested
|
|
899
|
+
? '✓ 已请求暂停,任务将在下一次工具调用前暂停'
|
|
900
|
+
: '当前任务已请求暂停或正在暂停',
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
if (normalizedContent.startsWith('/resume ')) {
|
|
904
|
+
return { kind: 'command.error', text: '用法: /resume' };
|
|
905
|
+
}
|
|
906
|
+
if (normalizedContent === '/resume') {
|
|
856
907
|
const resumeSession = await getExistingSessionForCommand();
|
|
857
908
|
if (!resumeSession)
|
|
858
909
|
return { kind: 'command.result', text: '当前没有活跃会话' };
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
const homeDir = os.homedir();
|
|
862
|
-
const encodedPath = encodePath(resumeSession.projectPath);
|
|
863
|
-
const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
|
|
864
|
-
if (!fs.existsSync(projectDir)) {
|
|
865
|
-
return { kind: 'command.error', text: '❌ 未找到 Claude 会话记录目录' };
|
|
866
|
-
}
|
|
867
|
-
const jsonlFiles = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
|
|
868
|
-
if (jsonlFiles.length === 0) {
|
|
869
|
-
return { kind: 'command.error', text: '❌ 当前项目没有 Claude 会话记录' };
|
|
870
|
-
}
|
|
871
|
-
const sessions = [];
|
|
872
|
-
for (const file of jsonlFiles) {
|
|
873
|
-
const filePath = path.join(projectDir, file);
|
|
874
|
-
const sessionId = file.replace('.jsonl', '');
|
|
875
|
-
let lastTimestamp = '';
|
|
876
|
-
let firstUserMessage = '';
|
|
877
|
-
let model = '';
|
|
878
|
-
let branch = '';
|
|
879
|
-
let turns = 0;
|
|
880
|
-
try {
|
|
881
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
882
|
-
const lines = content.split('\n').filter(l => l.trim());
|
|
883
|
-
for (const line of lines) {
|
|
884
|
-
const event = JSON.parse(line);
|
|
885
|
-
if (event.timestamp && event.timestamp > lastTimestamp) {
|
|
886
|
-
lastTimestamp = event.timestamp;
|
|
887
|
-
}
|
|
888
|
-
if (event.gitBranch && !branch) {
|
|
889
|
-
branch = event.gitBranch;
|
|
890
|
-
}
|
|
891
|
-
if (event.type === 'user' && event.message?.role === 'user') {
|
|
892
|
-
const msgContent = event.message.content;
|
|
893
|
-
const isToolResult = Array.isArray(msgContent) && msgContent.every((c) => c.type === 'tool_result');
|
|
894
|
-
if (!isToolResult) {
|
|
895
|
-
turns++;
|
|
896
|
-
if (!firstUserMessage) {
|
|
897
|
-
let candidate = '';
|
|
898
|
-
if (typeof msgContent === 'string') {
|
|
899
|
-
candidate = msgContent;
|
|
900
|
-
}
|
|
901
|
-
else if (Array.isArray(msgContent)) {
|
|
902
|
-
const textBlock = msgContent.find((c) => c.type === 'text');
|
|
903
|
-
if (textBlock?.text) {
|
|
904
|
-
candidate = textBlock.text;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
// 跳过 Claude Code 注入的脚手架 prompt,取第一条真人消息
|
|
908
|
-
if (candidate && !isSyntheticCliPrompt(candidate, 'claude')) {
|
|
909
|
-
firstUserMessage = candidate.slice(0, 100);
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
if (event.type === 'assistant' && event.message?.model && !model) {
|
|
915
|
-
model = event.message.model;
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
catch {
|
|
920
|
-
continue;
|
|
921
|
-
}
|
|
922
|
-
if (!lastTimestamp)
|
|
923
|
-
continue;
|
|
924
|
-
sessions.push({
|
|
925
|
-
sessionId,
|
|
926
|
-
lastMessageTime: lastTimestamp,
|
|
927
|
-
firstUserMessage: firstUserMessage || '(无消息)',
|
|
928
|
-
model: model || 'unknown',
|
|
929
|
-
turns,
|
|
930
|
-
branch: branch || 'unknown',
|
|
931
|
-
});
|
|
932
|
-
}
|
|
933
|
-
sessions.sort((a, b) => b.lastMessageTime.localeCompare(a.lastMessageTime));
|
|
934
|
-
return { kind: 'command.result', text: JSON.stringify(sessions, null, 2) };
|
|
935
|
-
}
|
|
936
|
-
catch (error) {
|
|
937
|
-
logger.error('[CommandHandler] /resume failed:', error);
|
|
938
|
-
return { kind: 'command.error', text: `❌ 读取会话记录失败: ${error instanceof Error ? error.message : '未知错误'}` };
|
|
939
|
-
}
|
|
910
|
+
const resumed = this.processor.resumeSession(resumeSession.id);
|
|
911
|
+
return { kind: 'command.result', text: resumed ? '✓ 已继续当前任务' : '当前任务未处于暂停状态' };
|
|
940
912
|
}
|
|
941
913
|
// /baseagent 命令:查看或切换 Agent 后端
|
|
942
914
|
if (normalizedContent === '/baseagent' || normalizedContent.startsWith('/baseagent ')) {
|
|
@@ -988,6 +960,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
988
960
|
if (!owningAgent) {
|
|
989
961
|
return { kind: 'command.error', text: '❌ 当前 channel 无绑定 agent,无法设置 active_baseagent' };
|
|
990
962
|
}
|
|
963
|
+
await interruptPausedSessionBeforeReplacement(await getExistingSessionForCommand());
|
|
991
964
|
const previousDefaultBaseagent = owningAgent.baseagent || this.parseDefaultBaseagent();
|
|
992
965
|
owningAgent.setActiveBaseagent(args);
|
|
993
966
|
this.eventBus.publish({
|
|
@@ -1646,11 +1619,6 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
1646
1619
|
if (normalizedContent === '/reload' || normalizedContent.startsWith('/reload ')) {
|
|
1647
1620
|
const aidArg = normalizedContent.slice('/reload'.length).trim() || undefined;
|
|
1648
1621
|
const selfAid = this.agentRegistry?.resolveByChannel(channel)?.aid;
|
|
1649
|
-
// agent channel 的 owner/admin 不能跨 agent reload;先返回领域内错误,避免落到
|
|
1650
|
-
// 底层 daemon-owner 约束的英文 reason。
|
|
1651
|
-
if (!isDaemonOwner && aidArg && aidArg !== selfAid) {
|
|
1652
|
-
return { kind: 'command.error', text: '❌ 无权限:跨 agent reload 仅限 daemon owner 使用' };
|
|
1653
|
-
}
|
|
1654
1622
|
const targetAid = aidArg ?? selfAid;
|
|
1655
1623
|
if (!targetAid) {
|
|
1656
1624
|
return { kind: 'command.error', text: '❌ 无法确定目标 agent,请指定 aid:/reload <aid>' };
|
|
@@ -1664,7 +1632,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
1664
1632
|
args: { ...(aidArg ? { aid: aidArg } : {}), ...(selfAid ? { self: selfAid } : {}) },
|
|
1665
1633
|
dangerous: true,
|
|
1666
1634
|
},
|
|
1667
|
-
identity
|
|
1635
|
+
identity,
|
|
1668
1636
|
session: activeSession,
|
|
1669
1637
|
channel,
|
|
1670
1638
|
channelId,
|
|
@@ -2010,12 +1978,16 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2010
1978
|
return { kind: 'command.result', text: `✅ @ 处理模式已切换: ${currentMode ?? '未设置'} → ${arg}` };
|
|
2011
1979
|
}
|
|
2012
1980
|
// /stop 命令:中断当前任务
|
|
1981
|
+
if (normalizedContent.startsWith('/stop ')) {
|
|
1982
|
+
return { kind: 'command.error', text: '用法: /stop' };
|
|
1983
|
+
}
|
|
2013
1984
|
if (normalizedContent === '/stop') {
|
|
2014
1985
|
const stopSession = await getExistingSessionForCommand();
|
|
2015
1986
|
if (!stopSession)
|
|
2016
1987
|
return { kind: 'command.result', text: '当前没有正在处理的任务' };
|
|
2017
1988
|
const stopAgent = this.getAgent(channel, stopSession.baseagent);
|
|
2018
1989
|
const sessionKey = stopSession.id;
|
|
1990
|
+
this.processor?.clearPauseSession?.(sessionKey);
|
|
2019
1991
|
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
2020
1992
|
const hasActive = stopAgent.hasActiveStream(sessionKey);
|
|
2021
1993
|
const isProcessing = this.messageQueue.isProcessing(sessionKey);
|
|
@@ -2049,6 +2021,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2049
2021
|
if (!session.agentSessionId) {
|
|
2050
2022
|
return { kind: 'command.error', text: '❌ 当前会话没有历史记录,无需压缩' };
|
|
2051
2023
|
}
|
|
2024
|
+
await interruptPausedSessionBeforeReplacement(session);
|
|
2052
2025
|
const projectPath = path.isAbsolute(session.projectPath)
|
|
2053
2026
|
? session.projectPath
|
|
2054
2027
|
: path.resolve(process.cwd(), session.projectPath);
|
|
@@ -2168,6 +2141,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2168
2141
|
return { kind: 'command.error', text: `❌ 会话名称 "${sessionName}" 已存在,请使用其他名称` };
|
|
2169
2142
|
}
|
|
2170
2143
|
}
|
|
2144
|
+
await interruptPausedSessionBeforeReplacement(session || activeSession);
|
|
2171
2145
|
const projectPath = this.getEffectiveDefaultPath(channel);
|
|
2172
2146
|
if (sendMessage && session) {
|
|
2173
2147
|
await sendMessage(channelId, `⏳ 正在创建新会话${sessionName ? `: ${sessionName}` : ''}...`, this.getReplyContext(session));
|
|
@@ -2291,7 +2265,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2291
2265
|
groups.set(type, []);
|
|
2292
2266
|
groups.get(type).push({ name, status });
|
|
2293
2267
|
}
|
|
2294
|
-
if (!isAdmin) {
|
|
2268
|
+
if (!isAdmin && !isDaemonOwner) {
|
|
2295
2269
|
// visitor/member: 仅显示实例通道摘要
|
|
2296
2270
|
const total = [...groups.values()].flat().length;
|
|
2297
2271
|
const healthy = [...groups.values()].flat().filter(i => i.status.includes('✓')).length;
|
|
@@ -2443,9 +2417,6 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2443
2417
|
if (normalizedContent === '/restart') {
|
|
2444
2418
|
// 进程级操作:必须是 daemon owner(daemon.json.owners),与 menu 协议 /system restart 一致。
|
|
2445
2419
|
// agent-channel 的 owner/admin 角色不足以重启整个 daemon。
|
|
2446
|
-
if (!isDaemonOwner) {
|
|
2447
|
-
return { kind: 'command.error', text: '❌ 无权限:服务重启仅限 daemon owner 使用' };
|
|
2448
|
-
}
|
|
2449
2420
|
const restartSelfAid = this.agentRegistry?.resolveByChannel(channel)?.aid;
|
|
2450
2421
|
const authDenied = await authorizeIntent({
|
|
2451
2422
|
intent: {
|
|
@@ -2455,7 +2426,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2455
2426
|
args: {},
|
|
2456
2427
|
dangerous: true,
|
|
2457
2428
|
},
|
|
2458
|
-
identity
|
|
2429
|
+
identity,
|
|
2459
2430
|
session: activeSession,
|
|
2460
2431
|
channel,
|
|
2461
2432
|
channelId,
|
|
@@ -2624,7 +2595,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2624
2595
|
args: {},
|
|
2625
2596
|
dangerous: true,
|
|
2626
2597
|
},
|
|
2627
|
-
identity
|
|
2598
|
+
identity,
|
|
2628
2599
|
session: activeSession,
|
|
2629
2600
|
channel,
|
|
2630
2601
|
channelId,
|
|
@@ -3008,6 +2979,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
3008
2979
|
: await this.sessionManager.listImportableCliSessions(projectPath, currentBaseagent);
|
|
3009
2980
|
const cliSession = cliSessions.find((c) => c.uuid.startsWith(sessionName));
|
|
3010
2981
|
if (cliSession) {
|
|
2982
|
+
// Importing a CLI session replaces the active session. Cancel any
|
|
2983
|
+
// pending tool-boundary pause only after the target has been
|
|
2984
|
+
// validated, so an invalid UUID cannot interrupt the current task.
|
|
2985
|
+
await interruptPausedSessionBeforeReplacement(session || activeSession);
|
|
3011
2986
|
const imported = await this.sessionManager.importCliSession(channel, channelId, projectPath, cliSession.uuid, currentBaseagent, selfAID ?? session?.selfAID);
|
|
3012
2987
|
this.eventBus.publish({ type: 'session:imported', sessionId: imported.id, agentSessionId: cliSession.uuid, projectPath });
|
|
3013
2988
|
const projectName = this.getProjectName(projectPath);
|
|
@@ -3038,6 +3013,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
3038
3013
|
if (!session.threadId && targetSession.threadId) {
|
|
3039
3014
|
return { kind: 'command.error', text: `❌ 无法从主会话切换到话题会话\n话题会话仅在对应话题内可用` };
|
|
3040
3015
|
}
|
|
3016
|
+
await interruptPausedSessionBeforeReplacement(session);
|
|
3017
|
+
this.processor?.clearPauseSession?.(session.id);
|
|
3018
|
+
this.processor?.clearPauseSession?.(targetSession.id);
|
|
3041
3019
|
const switched = await this.sessionManager.switchToSession(channel, channelId, targetSession.id);
|
|
3042
3020
|
if (!switched) {
|
|
3043
3021
|
return { kind: 'command.error', text: `❌ 切换会话失败` };
|
|
@@ -3125,6 +3103,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
3125
3103
|
}
|
|
3126
3104
|
this.eventBus.publish({ type: 'session:deleted', sessionId: targetSession.id });
|
|
3127
3105
|
const targetAgent = this.getAgent(channel, targetSession.baseagent);
|
|
3106
|
+
this.processor?.clearPauseSession?.(targetSession.id);
|
|
3128
3107
|
await targetAgent.closeSession(targetSession.id);
|
|
3129
3108
|
return { kind: 'command.result', text: `✓ 已删除会话: ${displaySessionTitle(targetSession.name, sessionName)}\n会话文件已保留,可通过 CLI 访问` };
|
|
3130
3109
|
}
|
|
@@ -3141,6 +3120,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
3141
3120
|
if (!forkAgent.capabilities?.fork) {
|
|
3142
3121
|
return { kind: 'command.error', text: `❌ 当前 Agent (${forkAgent.name}) 不支持 /fork\n\n可使用 /new 创建新会话替代` };
|
|
3143
3122
|
}
|
|
3123
|
+
await interruptPausedSessionBeforeReplacement(session);
|
|
3144
3124
|
try {
|
|
3145
3125
|
const forkedSessionId = await forkAgent.forkSession(session.agentSessionId, session.projectPath, forkName);
|
|
3146
3126
|
const newSession = await this.sessionManager.createForkedSession(session, forkedSessionId, forkName);
|
|
@@ -3195,6 +3175,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
3195
3175
|
if (!['chat', 'file', 'all'].includes(mode)) {
|
|
3196
3176
|
return { kind: 'command.error', text: `❌ 无效模式 "${mode}",可选:chat | file | all` };
|
|
3197
3177
|
}
|
|
3178
|
+
await interruptPausedSessionBeforeReplacement(session);
|
|
3198
3179
|
return { kind: 'command.result', text: await this.handleRewind(session, rewindAgent, turnNum, mode) };
|
|
3199
3180
|
}
|
|
3200
3181
|
// /repair 命令:检查并修复会话文件
|
|
@@ -3204,6 +3185,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
3204
3185
|
return { kind: 'command.result', text: '当前没有活跃会话' };
|
|
3205
3186
|
const repairAgent = this.getAgent(channel, repairSession.baseagent);
|
|
3206
3187
|
const { checkSessionFile, backupSessionFile } = await import('../session/session-file-health.js');
|
|
3188
|
+
await interruptPausedSessionBeforeReplacement(repairSession);
|
|
3207
3189
|
try {
|
|
3208
3190
|
if (!repairSession.agentSessionId) {
|
|
3209
3191
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
@@ -313,10 +313,16 @@ const CATALOG = [
|
|
|
313
313
|
{ path: 'sessionId', type: 'string' },
|
|
314
314
|
{ path: 'toolName', type: 'string' },
|
|
315
315
|
{ path: 'input', type: 'object' },
|
|
316
|
+
{ path: 'eventKey', type: 'string' },
|
|
317
|
+
{ path: 'eventPhase', type: 'string' },
|
|
316
318
|
{ path: 'callId', type: 'string', optional: true },
|
|
317
319
|
{ path: 'correlationId', type: 'string', optional: true },
|
|
318
320
|
{ path: 'agentAid', type: 'string', optional: true },
|
|
319
321
|
{ path: 'permissionMode', type: 'string', optional: true },
|
|
322
|
+
{ path: 'decision', type: 'string' },
|
|
323
|
+
{ path: 'decisionSource', type: 'string' },
|
|
324
|
+
{ path: 'executed', type: 'boolean' },
|
|
325
|
+
{ path: 'executionState', type: 'string' },
|
|
320
326
|
{ path: 'timestamp', type: 'number', optional: true },
|
|
321
327
|
],
|
|
322
328
|
},
|
|
@@ -328,6 +334,8 @@ const CATALOG = [
|
|
|
328
334
|
fields: [
|
|
329
335
|
{ path: 'sessionId', type: 'string' },
|
|
330
336
|
{ path: 'toolName', type: 'string' },
|
|
337
|
+
{ path: 'eventKey', type: 'string' },
|
|
338
|
+
{ path: 'eventPhase', type: 'string' },
|
|
331
339
|
{ path: 'isError', type: 'boolean', optional: true },
|
|
332
340
|
{ path: 'errorCode', type: 'string', optional: true },
|
|
333
341
|
{ path: 'agentName', type: 'string', optional: true },
|
|
@@ -335,6 +343,10 @@ const CATALOG = [
|
|
|
335
343
|
{ path: 'correlationId', type: 'string', optional: true },
|
|
336
344
|
{ path: 'agentAid', type: 'string', optional: true },
|
|
337
345
|
{ path: 'permissionMode', type: 'string', optional: true },
|
|
346
|
+
{ path: 'decision', type: 'string' },
|
|
347
|
+
{ path: 'decisionSource', type: 'string' },
|
|
348
|
+
{ path: 'executed', type: 'boolean' },
|
|
349
|
+
{ path: 'executionState', type: 'string' },
|
|
338
350
|
{ path: 'timestamp', type: 'number', optional: true },
|
|
339
351
|
],
|
|
340
352
|
},
|
|
@@ -846,3 +858,9 @@ export function getEventCatalog(opts = {}) {
|
|
|
846
858
|
const namespaces = [...new Set(events.map(entry => entry.namespace))].sort();
|
|
847
859
|
return { namespaces, events };
|
|
848
860
|
}
|
|
861
|
+
/** Whether an event Trigger pattern can subscribe to daemon-internal events. */
|
|
862
|
+
export function eventPatternIncludesInternal(pattern) {
|
|
863
|
+
return CATALOG.some(entry => entry.internal === true && (pattern === '*'
|
|
864
|
+
|| pattern === entry.type
|
|
865
|
+
|| (pattern.endsWith(':*') && entry.namespace === pattern.slice(0, -2))));
|
|
866
|
+
}
|
|
@@ -17,7 +17,7 @@ import { handlePendingQQBotContactBindMessage, registerPendingQQBotContactBind }
|
|
|
17
17
|
import { handlePendingWecomContactBindMessage, registerPendingWecomContactBind } from '../../channels/wecom.js';
|
|
18
18
|
import { handlePendingWechatContactBindMessage, registerPendingWechatContactBind } from '../../channels/wechat.js';
|
|
19
19
|
import { authorizeAccess, buildAuthSubject } from '../auth/auth-gateway.js';
|
|
20
|
-
import { MenuDiagnosticLimiter, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, menuSuccess, normalizeMenuError, parseMenuControl, validateMenuRequest, evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata, isAunMenuTokenRequired, menuCommandForName, } from '../command/menu-protocol.js';
|
|
20
|
+
import { MenuDiagnosticLimiter, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, menuSuccess, normalizeMenuError, parseMenuControl, validateMenuRequest, evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata, isAunMenuTokenRequired, menuCommandForName, normalizeMenuResponseTiming, withMenuProcessingTime, logMenuRequestCompleted, logMenuRequestReceived, } from '../command/menu-protocol.js';
|
|
21
21
|
import { resolveExplicitMenuExecutionContext, validateMenuCatalogArgs, validateMenuValueBatchArgs } from '../command/menu-catalog.js';
|
|
22
22
|
import { MenuTokenStore } from '../command/menu-token-store.js';
|
|
23
23
|
import { SessionRenewService } from '../session/session-renew.js';
|
|
@@ -691,9 +691,16 @@ export class MessageBridge {
|
|
|
691
691
|
// that timestamp through session resolution and queue processing.
|
|
692
692
|
// The fallback is only for legacy adapters that do not provide it.
|
|
693
693
|
const receivedAt = msg.receivedAt ?? Date.now();
|
|
694
|
+
const menuTiming = normalizeMenuResponseTiming(msg);
|
|
694
695
|
const channelKey = adapter?.channelKey || channelName;
|
|
695
696
|
const owningAgent = this.agentRegistry?.resolveByChannel(channelKey)
|
|
696
697
|
?? this.agentRegistry?.resolveByChannel(channelName);
|
|
698
|
+
// Agent-owned business controls bypass ResponseEngine entirely, so
|
|
699
|
+
// expose them only after bootstrap has explicitly reached active.
|
|
700
|
+
// Missing lifecycle fails closed; control channels without an owning
|
|
701
|
+
// EvolAgent keep their existing command behavior.
|
|
702
|
+
const allowsNormalCommandRouting = !owningAgent
|
|
703
|
+
|| owningAgent.config?.lifecycle === 'active';
|
|
697
704
|
const parsedChannelKey = tryParseChannelKey(channelKey);
|
|
698
705
|
const chatType = msg.chatType || 'private';
|
|
699
706
|
const delivery = chatType === 'group'
|
|
@@ -772,14 +779,21 @@ export class MessageBridge {
|
|
|
772
779
|
logger.info(`[MessageBridge] Inbound admission denied before business routing: self=${selfAid} actor=${actorId ?? '<none>'} channel=${channelKey} reason=${admissionDecision.reason}`);
|
|
773
780
|
return;
|
|
774
781
|
}
|
|
775
|
-
const menuControl = resolvedChannelType === 'aun'
|
|
782
|
+
const menuControl = allowsNormalCommandRouting && resolvedChannelType === 'aun'
|
|
776
783
|
? parseMenuControl(content)
|
|
777
784
|
: { isMenu: false };
|
|
778
785
|
let menuResponseWarning;
|
|
779
786
|
if (menuControl.isMenu) {
|
|
787
|
+
// AUN captures this mark at its adapter boundary. Legacy/test
|
|
788
|
+
// adapters fall back to the bridge entry mark captured above.
|
|
789
|
+
Object.assign(msg, menuTiming);
|
|
780
790
|
this.logMenuInbound(channelName, msg, menuControl);
|
|
781
791
|
if (!hasValidMenuId(menuControl)) {
|
|
782
792
|
this.logMenuDiagnostic('missing-id', channelName, msg, menuControl);
|
|
793
|
+
logMenuRequestCompleted(menuControl.request, undefined, this.menuFlowContext(channelName, msg), {
|
|
794
|
+
delivery: 'dropped',
|
|
795
|
+
reason: 'missing-id',
|
|
796
|
+
});
|
|
783
797
|
return;
|
|
784
798
|
}
|
|
785
799
|
const versionError = evaluateEvolMenuVersionGate({
|
|
@@ -916,7 +930,9 @@ export class MessageBridge {
|
|
|
916
930
|
// 2. 命令快速路径(去除引用前缀后检查,兼容话题中引用上文的情况)
|
|
917
931
|
const contentForCmd = content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
|
|
918
932
|
const cmdContent = contentForCmd || content;
|
|
919
|
-
const isCmd =
|
|
933
|
+
const isCmd = allowsNormalCommandRouting
|
|
934
|
+
&& msg.source !== 'handoff'
|
|
935
|
+
&& this.cmdHandler.isCommand(cmdContent);
|
|
920
936
|
if (isCmd) {
|
|
921
937
|
logger.debug(`[MessageBridge] Command detected: "${cmdContent}", routing to handler`);
|
|
922
938
|
// 命令也要记录入方向 jsonl(不创建 session,直接用 chatDirPath 计算路径)
|
|
@@ -953,11 +969,13 @@ export class MessageBridge {
|
|
|
953
969
|
logger.debug(`[MessageBridge] Failed to log inbound command: ${e}`);
|
|
954
970
|
}
|
|
955
971
|
}
|
|
956
|
-
if (
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
972
|
+
if (allowsNormalCommandRouting
|
|
973
|
+
&& msg.source !== 'handoff'
|
|
974
|
+
&& await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => {
|
|
975
|
+
const taskId = `cmd-${msg.messageId || Date.now()}`;
|
|
976
|
+
logger.channelOut({ channel: channelName, channelId: msg.channelId, taskId, correlationId: taskId, sessionId: msg.replyContext?.sessionId, agentAid: msg.selfAID, payload: { kind: 'command.result', text } });
|
|
977
|
+
return sendReply(msg.channelId, text, msg.replyContext);
|
|
978
|
+
}, msg.peerId, msg.threadId, msg.chatType, msg.source, msg.replyContext, msg.messageId, msg.selfAID, authSubject))
|
|
961
979
|
return;
|
|
962
980
|
// 3. session 解析(使用 Channel 层填充的 chatType)
|
|
963
981
|
if (!(await this.canCreateThreadSession(channelName, msg, chatType))) {
|
|
@@ -1408,6 +1426,9 @@ export class MessageBridge {
|
|
|
1408
1426
|
}
|
|
1409
1427
|
}
|
|
1410
1428
|
async sendMenuResponse(adapter, channel, msg, response) {
|
|
1429
|
+
const request = this.menuRequestForLog(msg, response);
|
|
1430
|
+
const flowContext = this.menuFlowContext(channel, msg);
|
|
1431
|
+
const timedResponse = withMenuProcessingTime(response, msg);
|
|
1411
1432
|
if (!adapter?.send) {
|
|
1412
1433
|
this.logMenuDiagnostic('transport-unavailable', channel, msg, {
|
|
1413
1434
|
isMenu: true,
|
|
@@ -1417,6 +1438,10 @@ export class MessageBridge {
|
|
|
1417
1438
|
id: response.id,
|
|
1418
1439
|
name: response.name,
|
|
1419
1440
|
}, 'TEMPORARILY_UNAVAILABLE');
|
|
1441
|
+
logMenuRequestCompleted(request, timedResponse, flowContext, {
|
|
1442
|
+
delivery: 'dropped',
|
|
1443
|
+
reason: 'transport-unavailable',
|
|
1444
|
+
});
|
|
1420
1445
|
return;
|
|
1421
1446
|
}
|
|
1422
1447
|
const agentName = this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>';
|
|
@@ -1429,6 +1454,10 @@ export class MessageBridge {
|
|
|
1429
1454
|
};
|
|
1430
1455
|
if (msg.channelType === 'aun' && !replyContext.delivery) {
|
|
1431
1456
|
logger.warn(`[MessageBridge] dropping AUN menu response without trusted delivery route: channel=${channel} channelId=${msg.channelId}`);
|
|
1457
|
+
logMenuRequestCompleted(request, timedResponse, flowContext, {
|
|
1458
|
+
delivery: 'dropped',
|
|
1459
|
+
reason: 'missing-delivery-route',
|
|
1460
|
+
});
|
|
1432
1461
|
return;
|
|
1433
1462
|
}
|
|
1434
1463
|
const envelope = buildEnvelope({
|
|
@@ -1438,7 +1467,18 @@ export class MessageBridge {
|
|
|
1438
1467
|
agentName,
|
|
1439
1468
|
replyContext,
|
|
1440
1469
|
});
|
|
1441
|
-
|
|
1470
|
+
try {
|
|
1471
|
+
await adapter.send(envelope, { kind: 'custom', channelType: channel, payload: timedResponse });
|
|
1472
|
+
logMenuRequestCompleted(request, timedResponse, flowContext, { delivery: 'sent' });
|
|
1473
|
+
}
|
|
1474
|
+
catch (error) {
|
|
1475
|
+
logMenuRequestCompleted(request, timedResponse, flowContext, {
|
|
1476
|
+
delivery: 'failed',
|
|
1477
|
+
reason: 'transport-error',
|
|
1478
|
+
transportError: error,
|
|
1479
|
+
});
|
|
1480
|
+
throw error;
|
|
1481
|
+
}
|
|
1442
1482
|
}
|
|
1443
1483
|
menuTokenOwnerAid(channel, msg) {
|
|
1444
1484
|
return msg.selfAID || this.agentRegistry?.resolveByChannel(channel)?.aid;
|
|
@@ -1466,6 +1506,7 @@ export class MessageBridge {
|
|
|
1466
1506
|
return normalized.length > 0 && normalized.length <= 256 ? normalized : undefined;
|
|
1467
1507
|
}
|
|
1468
1508
|
logMenuInbound(channel, msg, parsed) {
|
|
1509
|
+
logMenuRequestReceived(parsed.request, this.menuFlowContext(channel, msg));
|
|
1469
1510
|
logger.channelIn({
|
|
1470
1511
|
channel,
|
|
1471
1512
|
channelId: this.shortHash(msg.channelId),
|
|
@@ -1482,6 +1523,21 @@ export class MessageBridge {
|
|
|
1482
1523
|
},
|
|
1483
1524
|
});
|
|
1484
1525
|
}
|
|
1526
|
+
menuRequestForLog(msg, response) {
|
|
1527
|
+
try {
|
|
1528
|
+
const parsed = JSON.parse(msg.content);
|
|
1529
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
1530
|
+
return parsed;
|
|
1531
|
+
}
|
|
1532
|
+
catch {
|
|
1533
|
+
// Fall through to a minimal correlation record.
|
|
1534
|
+
}
|
|
1535
|
+
return {
|
|
1536
|
+
type: 'menu.unknown',
|
|
1537
|
+
id: response.id,
|
|
1538
|
+
...(response.name ? { name: response.name } : {}),
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1485
1541
|
logMenuDiagnostic(category, channel, msg, parsed, code) {
|
|
1486
1542
|
const scope = msg.chatType === 'group' ? (msg.groupId || msg.channelId) : msg.peerId;
|
|
1487
1543
|
const key = `${category}:${channel}:${this.shortHash(scope)}`;
|
|
@@ -1611,6 +1667,13 @@ export class MessageBridge {
|
|
|
1611
1667
|
* 撤回消息:先查 debounce 窗口,再查 message queue,最后查正在执行的任务。
|
|
1612
1668
|
* @returns true 如果找到并取消/中断
|
|
1613
1669
|
*/
|
|
1670
|
+
menuFlowContext(channel, msg) {
|
|
1671
|
+
return {
|
|
1672
|
+
source: 'agent-aun',
|
|
1673
|
+
selfAid: msg.selfAID ?? this.agentRegistry?.resolveByChannel(channel)?.aid,
|
|
1674
|
+
messageId: msg.messageId,
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1614
1677
|
cancel(messageId, selfAID) {
|
|
1615
1678
|
// 阶段 1: debounce 窗口(尚未入队)
|
|
1616
1679
|
for (const d of this.debouncers.values()) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Per-session gate used only at the next tool boundary. */
|
|
2
|
+
export class PauseController {
|
|
3
|
+
state = 'running';
|
|
4
|
+
waiters = new Set();
|
|
5
|
+
requestPause() {
|
|
6
|
+
if (this.state !== 'running')
|
|
7
|
+
return false;
|
|
8
|
+
this.state = 'pause_requested';
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
isPauseRequested() { return this.state === 'pause_requested'; }
|
|
12
|
+
isPaused() { return this.state === 'paused'; }
|
|
13
|
+
waitAtToolBoundary(signal) {
|
|
14
|
+
if (signal?.aborted)
|
|
15
|
+
return Promise.resolve('cancelled');
|
|
16
|
+
if (this.state === 'running')
|
|
17
|
+
return Promise.resolve('none');
|
|
18
|
+
this.state = 'paused';
|
|
19
|
+
return new Promise(resolve => {
|
|
20
|
+
const waiter = { resolve: () => { }, abort: undefined };
|
|
21
|
+
const finish = (result) => {
|
|
22
|
+
this.waiters.delete(waiter);
|
|
23
|
+
if (waiter.abort)
|
|
24
|
+
signal?.removeEventListener('abort', waiter.abort);
|
|
25
|
+
resolve(result);
|
|
26
|
+
};
|
|
27
|
+
waiter.resolve = finish;
|
|
28
|
+
if (signal) {
|
|
29
|
+
waiter.abort = () => finish('cancelled');
|
|
30
|
+
signal.addEventListener('abort', waiter.abort, { once: true });
|
|
31
|
+
}
|
|
32
|
+
this.waiters.add(waiter);
|
|
33
|
+
if (this.state === 'running')
|
|
34
|
+
finish('released');
|
|
35
|
+
else if (signal?.aborted)
|
|
36
|
+
finish('cancelled');
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
resume() {
|
|
40
|
+
if (this.state === 'running')
|
|
41
|
+
return false;
|
|
42
|
+
this.state = 'running';
|
|
43
|
+
for (const waiter of [...this.waiters])
|
|
44
|
+
waiter.resolve('released');
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
cancel() {
|
|
48
|
+
this.state = 'running';
|
|
49
|
+
for (const waiter of [...this.waiters])
|
|
50
|
+
waiter.resolve('cancelled');
|
|
51
|
+
}
|
|
52
|
+
clear() { this.cancel(); }
|
|
53
|
+
}
|