evolcore 0.0.22 → 0.0.23
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/AGENTS.md +62 -0
- package/CHANGELOG.md +24 -14
- package/README.md +2 -0
- package/bin/codex-managed-acl.ps1 +155 -0
- package/bin/install-codex-managed-hooks.mjs +373 -95
- package/dist/agents/baseagent-availability.js +48 -0
- package/dist/agents/baseagent.js +1 -1
- package/dist/agents/claude-runner.js +30 -226
- package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +73 -13
- package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
- package/dist/agents/codex/catalog.js +70 -0
- package/dist/agents/codex/diagnostics.js +154 -0
- package/dist/agents/codex-runner.js +407 -139
- package/dist/agents/ecagent-runner.js +10 -31
- package/dist/agents/gemini-runner.js +2 -6
- package/dist/aun/aid/store.js +2 -0
- package/dist/aun/menu-client.js +272 -0
- package/dist/aun/msg/group.js +65 -4
- package/dist/aun/msg/index.js +1 -1
- package/dist/aun/msg/managed-operation.js +51 -4
- package/dist/aun/rpc/client.js +8 -1
- package/dist/channels/aun.js +31 -1
- package/dist/channels/wechat.js +81 -10
- package/dist/cli/aun-commands.js +48 -4
- package/dist/cli/command-log.js +21 -2
- package/dist/cli/daemon-commands.js +142 -45
- package/dist/cli/index.js +20 -0
- package/dist/cli/init.js +2 -10
- package/dist/cli/menu-client.js +95 -0
- package/dist/cli/model.js +23 -8
- package/dist/cli/task-context.js +12 -2
- package/dist/cli/trigger-command.js +9 -1
- package/dist/config/schema-migration.js +14 -2
- package/dist/core/auth/agent-delegation.js +10 -7
- package/dist/core/auth/authorization-audit.js +4 -0
- package/dist/core/auth/operation-catalog.js +4 -0
- package/dist/core/auth/remote-access-store.js +135 -0
- package/dist/core/command/command-handler.js +41 -5
- package/dist/core/command/daemon-diagnostics.js +448 -0
- package/dist/core/command/menu-catalog.js +3 -0
- package/dist/core/command/menu-handler.js +141 -33
- package/dist/core/command/menu-protocol.js +119 -0
- package/dist/core/command/slash-gate.js +3 -3
- package/dist/core/command/slash-handler.js +42 -22
- package/dist/core/message/message-queue.js +20 -15
- package/dist/core/message/response-engine.js +74 -73
- package/dist/core/model/catalog-service.js +308 -0
- package/dist/core/model/model-catalog.js +124 -334
- package/dist/core/model/model-diagnostics.js +4 -5
- package/dist/core/model/model-fallback.js +9 -16
- package/dist/core/model/model-permission.js +1 -1
- package/dist/core/permission/approval-gateway.js +16 -1
- package/dist/core/permission/protected-paths.js +63 -4
- package/dist/core/permission/sandbox-runtime.js +4 -2
- package/dist/core/permission/tool-policy.js +244 -25
- package/dist/core/runtime-lock.js +13 -8
- package/dist/index.js +40 -29
- package/dist/ipc.js +123 -81
- package/dist/trigger/manager.js +3 -1
- package/dist/trigger/parser.js +77 -13
- package/dist/trigger/patch.js +104 -1
- package/dist/utils/ecweb-supervisor.js +11 -2
- package/dist/utils/ecweb-utils.js +8 -26
- package/dist/utils/error-utils.js +7 -0
- package/dist/utils/npm-ops.js +47 -15
- package/kits/docs/evolcore/INDEX.md +1 -0
- package/kits/docs/evolcore/agent-create.md +13 -0
- package/kits/docs/evolcore/group-collaboration.md +7 -2
- package/kits/docs/evolcore/menu-client.md +34 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
- package/kits/migrations/migrate-role-config-v4.mjs +11 -5
- package/kits/migrations/migrate-role-config-v5.mjs +8 -2
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +8 -1
- package/package.json +11 -3
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { hasModelSwitcher, hasPermissionController } from '../../agents/runner-types.js';
|
|
2
|
-
import { getCodexEfforts } from '../../agents/codex-runner.js';
|
|
3
2
|
import { normalizeBaseagent } from '../../agents/baseagent.js';
|
|
4
3
|
import { buildEnvelope, isDeliveryTargetForChannel } from '../message/message-utils.js';
|
|
5
4
|
import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
|
|
@@ -10,13 +9,14 @@ import path from 'path';
|
|
|
10
9
|
import fs from 'fs';
|
|
11
10
|
import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions } from '../../utils/npm-ops.js';
|
|
12
11
|
import { loadDaemonConfig } from '../../config-store.js';
|
|
12
|
+
import { remoteAccessService } from '../auth/remote-access-store.js';
|
|
13
13
|
import { read as cfgRead, resolveEffective, routeFieldPath, write as cfgWrite, } from '../../config/config-manager.js';
|
|
14
14
|
import { execAgentAction } from './agent-control.js';
|
|
15
15
|
import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
|
|
16
16
|
import { authorizationNextStep, formatAuthorizationDenial } from '../auth/authorization-denial.js';
|
|
17
17
|
import { resolvePermissionMode, writeScope } from '../model/config-scope.js';
|
|
18
18
|
import { formatPeerKey } from '../relation/peer-identity.js';
|
|
19
|
-
import { modelMatches } from '../model/model-catalog.js';
|
|
19
|
+
import { modelMatches, getCatalog, assertCatalogSelectable, resolveCatalogModel } from '../model/model-catalog.js';
|
|
20
20
|
import { formatModelCheck, runModelCheck } from '../model/model-diagnostics.js';
|
|
21
21
|
import { constrainResolvedModelForRole, filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
|
|
22
22
|
import { resolveRuntimeStringField } from '../role/runtime-policy.js';
|
|
@@ -28,7 +28,7 @@ import { isSystemControlChannel } from '../system-channels.js';
|
|
|
28
28
|
import { spawnDetachedNode } from '../../utils/cross-platform.js';
|
|
29
29
|
import { inspectDataMigrationRequirement } from '../data-migration.js';
|
|
30
30
|
import { guardIdleCommand, guardKnownCommand, guardRoleCommand, guardThreadCommand, isRecognizedSlashCommand, normalizeSlashContent, probeSessionActivity, } from './slash-gate.js';
|
|
31
|
-
const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
31
|
+
const allEfforts = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
32
32
|
const PERMISSION_MODE_KEYS = PUBLIC_PERMISSION_MODES;
|
|
33
33
|
function defaultSlashChatmode(field) {
|
|
34
34
|
return field === 'private' ? 'interactive' : 'proactive';
|
|
@@ -127,12 +127,8 @@ function resolveSlashRelationTarget(params) {
|
|
|
127
127
|
role: params.role,
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
|
-
function getAvailableEfforts(agent, model) {
|
|
131
|
-
|
|
132
|
-
return allEfforts;
|
|
133
|
-
if (agent.name === 'codex')
|
|
134
|
-
return getCodexEfforts(model);
|
|
135
|
-
return [];
|
|
130
|
+
async function getAvailableEfforts(agent, model) {
|
|
131
|
+
return (await agent.getAvailableEfforts?.(model) ?? []);
|
|
136
132
|
}
|
|
137
133
|
function modelDisplayLabel(agent, model) {
|
|
138
134
|
const full = agent.resolveModelId?.(model);
|
|
@@ -487,7 +483,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
487
483
|
const isProcessLevelSlash = normalizedContent === '/restart'
|
|
488
484
|
|| normalizedContent === '/upgrade'
|
|
489
485
|
|| normalizedContent === '/reload'
|
|
490
|
-
|| normalizedContent.startsWith('/reload ')
|
|
486
|
+
|| normalizedContent.startsWith('/reload ')
|
|
487
|
+
|| normalizedContent === '/pair';
|
|
491
488
|
const roleGuard = guardRoleCommand(normalizedContent, activeChatType, isAdmin || (isDaemonOwner && isProcessLevelSlash));
|
|
492
489
|
if (roleGuard)
|
|
493
490
|
return roleGuard;
|
|
@@ -635,7 +632,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
635
632
|
const appendProcessCommands = (lines) => {
|
|
636
633
|
if (!isDaemonOwner)
|
|
637
634
|
return;
|
|
638
|
-
lines.push('', '🛠️ 进程级运维:', ' /restart - 重启服务', ' /upgrade - 检查版本更新', ' /reload [aid] - 热重载 Agent 配置', ' /fa <提示词> - 单次使用宿主级 fullaccess 执行(别名: /fullaccess)');
|
|
635
|
+
lines.push('', '🛠️ 进程级运维:', ' /restart - 重启服务', ' /upgrade - 检查版本更新', ' /reload [aid] - 热重载 Agent 配置', ' /fa <提示词> - 单次使用宿主级 fullaccess 执行(别名: /fullaccess)', ' /pair - 获取远程控制配对码');
|
|
639
636
|
};
|
|
640
637
|
if (!isAdmin && activeChatType === 'group') {
|
|
641
638
|
const lines = [
|
|
@@ -775,6 +772,23 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
775
772
|
];
|
|
776
773
|
return { kind: 'command.result', text: lines.join('\n') };
|
|
777
774
|
}
|
|
775
|
+
// /pair is also recognized on an Agent channel so it cannot fall through
|
|
776
|
+
// into model input. The daemon control channel has its own fast path; this
|
|
777
|
+
// branch preserves the same daemon-owner boundary for ordinary AUN chats.
|
|
778
|
+
if (normalizedContent === '/pair') {
|
|
779
|
+
if (!isDaemonOwner) {
|
|
780
|
+
return {
|
|
781
|
+
kind: 'command.error',
|
|
782
|
+
text: '❌ /pair 仅限 daemon-owner 使用;请向 daemon 控制 AID 发送 /pair。',
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
const pair = remoteAccessService.getPairingCode(userId);
|
|
786
|
+
const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
|
|
787
|
+
return {
|
|
788
|
+
kind: 'command.result',
|
|
789
|
+
text: `远程控制配对码:${pair.code}(约 ${mins} 分钟内有效)\n可用于 ECWeb 登录或远程 Menu 客户端授权`,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
778
792
|
// /evolhelp 命令:返回 JSON 格式的命令列表(供程序解析)
|
|
779
793
|
if (normalizedContent === '/evolhelp') {
|
|
780
794
|
const cmds = [];
|
|
@@ -835,6 +849,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
835
849
|
cmds.push({ command: '/restart', description: '重启服务', category: '运维', roles: ['daemon-owner'] });
|
|
836
850
|
cmds.push({ command: '/upgrade', description: '检查版本更新', category: '运维', roles: ['daemon-owner'] });
|
|
837
851
|
cmds.push({ command: '/reload', args: '[aid]', description: '热重载 Agent 配置', category: '运维', roles: ['daemon-owner'] });
|
|
852
|
+
cmds.push({ command: '/pair', description: '获取远程控制配对码', category: '运维', roles: ['daemon-owner'] });
|
|
838
853
|
}
|
|
839
854
|
if (!isDaemonOwner && isAdmin) {
|
|
840
855
|
cmds.push({ command: '/reload', description: '热重载当前 Agent 配置', category: '运维', roles: ['admin', 'owner'] });
|
|
@@ -1260,12 +1275,14 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
1260
1275
|
catch { }
|
|
1261
1276
|
const configuredModel = modelConfig.model;
|
|
1262
1277
|
const configuredEffort = modelConfig.effort;
|
|
1263
|
-
const
|
|
1278
|
+
const catalog = await getCatalog(modelSelector.self, fallbackBaseagent);
|
|
1279
|
+
assertCatalogSelectable(catalog);
|
|
1280
|
+
const rawModels = catalog.models.map(m => m.id);
|
|
1264
1281
|
const models = hasModelSwitcher(modelAgent)
|
|
1265
|
-
? filterModelsForRole(identity.role, fallbackBaseagent, rawModels,
|
|
1282
|
+
? filterModelsForRole(identity.role, fallbackBaseagent, rawModels, model => resolveCatalogModel(catalog, model), modelSelector.self)
|
|
1266
1283
|
: rawModels;
|
|
1267
1284
|
const currentModel = hasModelSwitcher(modelAgent) ? (configuredModel || modelAgent.getModel()) : modelAgent.name;
|
|
1268
|
-
const efforts = getAvailableEfforts(modelAgent, currentModel);
|
|
1285
|
+
const efforts = await getAvailableEfforts(modelAgent, currentModel);
|
|
1269
1286
|
const currentEffort = configuredEffort ?? modelAgent.getEffort?.() ?? 'auto';
|
|
1270
1287
|
const canReadModelList = await canReadSlashModelList({
|
|
1271
1288
|
subject: authSubject,
|
|
@@ -1290,7 +1307,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
1290
1307
|
kind: 'command-card',
|
|
1291
1308
|
title: '🤖 切换模型',
|
|
1292
1309
|
buttons: models.map((m) => {
|
|
1293
|
-
const display = modelDisplayLabel(modelAgent, m);
|
|
1310
|
+
const display = modelDisplayLabel(modelAgent, m) + (catalog.models.find(e => e.id === m)?.verification === 'unverified' ? ' [unverified]' : '');
|
|
1294
1311
|
return {
|
|
1295
1312
|
label: modelMatches(modelAgent, m, currentModel) ? `✓ ${display}` : display,
|
|
1296
1313
|
command: `/model ${m}`,
|
|
@@ -1307,6 +1324,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
1307
1324
|
return { kind: 'command.result', text: cardResult };
|
|
1308
1325
|
}
|
|
1309
1326
|
// 降级:文本
|
|
1327
|
+
const catalogHint = `[${catalog.source}/${catalog.freshness}] ${catalog.diagnostics.map(d => d.message).join('; ')}`;
|
|
1310
1328
|
const modelList = models.map((m) => ` ${modelMatches(modelAgent, m, currentModel) ? '✓' : ' '} ${modelDisplayLabel(modelAgent, m)}`).join('\n');
|
|
1311
1329
|
const effortHint = efforts.length > 0
|
|
1312
1330
|
? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
|
|
@@ -1322,8 +1340,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
1322
1340
|
const usage = !args && isAdmin ? '\n\n用法: /model <模型>' : '';
|
|
1323
1341
|
return {
|
|
1324
1342
|
kind: 'command.result',
|
|
1325
|
-
text: `当前模型: ${modelDisplayLabel(modelAgent, currentModel)}${effortHint}\n\n可用模型:\n${modelList}${usage}`,
|
|
1326
|
-
...(args === 'list' ? { structured: { ok: true, model: currentModel, effort: currentEffort, baseagent: fallbackBaseagent, models } } : {}),
|
|
1343
|
+
text: `当前模型: ${modelDisplayLabel(modelAgent, currentModel)}${effortHint}${rawModels.includes(currentModel) ? '' : ' [当前配置,未列入目录]'}\n${catalogHint}\n\n可用模型:\n${modelList}${usage}`,
|
|
1344
|
+
...(args === 'list' ? { structured: { ok: true, model: currentModel, effort: currentEffort, baseagent: fallbackBaseagent, models, revision: catalog.revision, source: catalog.source, freshness: catalog.freshness, diagnostics: catalog.diagnostics } } : {}),
|
|
1327
1345
|
};
|
|
1328
1346
|
}
|
|
1329
1347
|
return { kind: 'command.result', text: `当前模型: ${modelDisplayLabel(modelAgent, currentModel)}${effortHint}` };
|
|
@@ -1384,7 +1402,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
1384
1402
|
catch { }
|
|
1385
1403
|
const models = hasModelSwitcher(modelAgent) ? await modelAgent.listModels() : [];
|
|
1386
1404
|
const allowedModels = hasModelSwitcher(modelAgent)
|
|
1387
|
-
? filterModelsForRole(identity.role, fallbackBaseagent, models, modelAgent.resolveModelId?.bind(modelAgent))
|
|
1405
|
+
? filterModelsForRole(identity.role, fallbackBaseagent, models, modelAgent.resolveModelId?.bind(modelAgent), selfAID ?? this.resolveSelfAID(channel))
|
|
1388
1406
|
: models;
|
|
1389
1407
|
const parts = args.split(/\s+/);
|
|
1390
1408
|
let newModel;
|
|
@@ -1392,7 +1410,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
1392
1410
|
if (parts.length === 1) {
|
|
1393
1411
|
const arg = parts[0];
|
|
1394
1412
|
const currentModel = hasModelSwitcher(modelAgent) ? (configuredModel || modelAgent.getModel()) : modelAgent.name;
|
|
1395
|
-
const efforts = getAvailableEfforts(modelAgent, currentModel);
|
|
1413
|
+
const efforts = await getAvailableEfforts(modelAgent, currentModel);
|
|
1396
1414
|
// effort 相关参数统一转发到 /effort
|
|
1397
1415
|
if (efforts.includes(arg) || arg === 'auto') {
|
|
1398
1416
|
const delegated = await this.handle(`/effort ${arg}`, channel, channelId, undefined, userId, threadId);
|
|
@@ -1425,7 +1443,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
1425
1443
|
if (!models.includes(modelArg)) {
|
|
1426
1444
|
return { kind: 'command.error', text: `❌ 无效的模型ID: ${modelArgRaw}` };
|
|
1427
1445
|
}
|
|
1428
|
-
const targetEfforts = getAvailableEfforts(modelAgent, modelArg);
|
|
1446
|
+
const targetEfforts = await getAvailableEfforts(modelAgent, modelArg);
|
|
1429
1447
|
if (targetEfforts.length === 0) {
|
|
1430
1448
|
return { kind: 'command.error', text: `⚠️ ${modelArg} 不支持推理强度设置` };
|
|
1431
1449
|
}
|
|
@@ -1583,7 +1601,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
1583
1601
|
const configuredModel = modelConfig.model;
|
|
1584
1602
|
const configuredEffort = modelConfig.effort;
|
|
1585
1603
|
const currentModel = hasModelSwitcher(effortAgent) ? (configuredModel || effortAgent.getModel()) : effortAgent.name;
|
|
1586
|
-
const efforts = getAvailableEfforts(effortAgent, currentModel);
|
|
1604
|
+
const efforts = await getAvailableEfforts(effortAgent, currentModel);
|
|
1587
1605
|
const currentEffort = configuredEffort ?? effortAgent.getEffort?.() ?? 'auto';
|
|
1588
1606
|
if (efforts.length === 0) {
|
|
1589
1607
|
return { kind: 'command.error', text: '⚠️ 当前模型不支持推理强度设置' };
|
|
@@ -1645,7 +1663,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
1645
1663
|
const configuredModel = modelConfig.model;
|
|
1646
1664
|
const configuredEffort = modelConfig.effort;
|
|
1647
1665
|
const currentModel = hasModelSwitcher(effortAgent) ? (configuredModel || effortAgent.getModel()) : effortAgent.name;
|
|
1648
|
-
const efforts = getAvailableEfforts(effortAgent, currentModel);
|
|
1666
|
+
const efforts = await getAvailableEfforts(effortAgent, currentModel);
|
|
1649
1667
|
const currentEffort = configuredEffort ?? effortAgent.getEffort?.() ?? 'auto';
|
|
1650
1668
|
if (efforts.length === 0) {
|
|
1651
1669
|
return { kind: 'command.error', text: '⚠️ 当前模型不支持推理强度设置' };
|
|
@@ -2502,6 +2520,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2502
2520
|
if (session && previousAgent) {
|
|
2503
2521
|
// Reset agent backend state so the new
|
|
2504
2522
|
// session starts with a fresh conversation history
|
|
2523
|
+
// Clearing is a pure backend lifecycle operation; model selection is
|
|
2524
|
+
// resolved only when the new session performs its next turn.
|
|
2505
2525
|
await previousAgent.clearSession(session.id, session.agentSessionId || '', session.projectPath);
|
|
2506
2526
|
await previousAgent.closeSession(session.id);
|
|
2507
2527
|
}
|
|
@@ -234,6 +234,7 @@ export class MessageQueue {
|
|
|
234
234
|
}
|
|
235
235
|
const tmp = `${this.dedupPath}.tmp-${process.pid}-${Date.now()}-${this.enqueueSequence}`;
|
|
236
236
|
let fd;
|
|
237
|
+
let renamed = false;
|
|
237
238
|
try {
|
|
238
239
|
fs.mkdirSync(path.dirname(this.dedupPath), { recursive: true });
|
|
239
240
|
const content = Buffer.from(JSON.stringify({
|
|
@@ -253,20 +254,22 @@ export class MessageQueue {
|
|
|
253
254
|
fs.closeSync(fd);
|
|
254
255
|
fd = undefined;
|
|
255
256
|
fs.renameSync(tmp, this.dedupPath);
|
|
256
|
-
|
|
257
|
-
try {
|
|
258
|
-
fs.fsyncSync(persistedFd);
|
|
259
|
-
}
|
|
260
|
-
finally {
|
|
261
|
-
fs.closeSync(persistedFd);
|
|
262
|
-
}
|
|
257
|
+
renamed = true;
|
|
263
258
|
if (process.platform !== 'win32') {
|
|
264
|
-
const directoryFd = fs.openSync(path.dirname(this.dedupPath), 'r');
|
|
265
259
|
try {
|
|
266
|
-
fs.
|
|
260
|
+
const directoryFd = fs.openSync(path.dirname(this.dedupPath), 'r');
|
|
261
|
+
try {
|
|
262
|
+
fs.fsyncSync(directoryFd);
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
fs.closeSync(directoryFd);
|
|
266
|
+
}
|
|
267
267
|
}
|
|
268
|
-
|
|
269
|
-
|
|
268
|
+
catch (error) {
|
|
269
|
+
// The file contents were already fsynced before rename. A platform
|
|
270
|
+
// that cannot fsync directory metadata must not make a committed
|
|
271
|
+
// dedup claim look uncommitted to the caller.
|
|
272
|
+
logger.warn(`[Queue] Durable dedup ledger directory fsync unavailable after commit: ${error instanceof Error ? error.message : String(error)}`);
|
|
270
273
|
}
|
|
271
274
|
}
|
|
272
275
|
return true;
|
|
@@ -279,11 +282,13 @@ export class MessageQueue {
|
|
|
279
282
|
}
|
|
280
283
|
catch { }
|
|
281
284
|
}
|
|
282
|
-
|
|
283
|
-
|
|
285
|
+
if (!renamed) {
|
|
286
|
+
try {
|
|
287
|
+
fs.unlinkSync(tmp);
|
|
288
|
+
}
|
|
289
|
+
catch { }
|
|
284
290
|
}
|
|
285
|
-
|
|
286
|
-
return false;
|
|
291
|
+
return renamed;
|
|
287
292
|
}
|
|
288
293
|
}
|
|
289
294
|
hasEnqueuedMessage(sessionKey, messageId, selfAID) {
|
|
@@ -325,44 +325,71 @@ function withFallbackModel(override, candidate) {
|
|
|
325
325
|
...(candidate.effort ? { effort: candidate.effort } : { effortMode: 'model_default' }),
|
|
326
326
|
};
|
|
327
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Keep the per-task fullaccess fact in the user-message prompt rather than in
|
|
330
|
+
* the long-lived system/developer prompt. This avoids changing a runner's
|
|
331
|
+
* system-prompt cache while still giving the model an explicit, auditable
|
|
332
|
+
* execution-mode signal for the privileged task.
|
|
333
|
+
*/
|
|
334
|
+
const FULL_ACCESS_MESSAGE_PROMPT_CONTEXT = '【当前权限模式提升为fullaccess,可不受限制执行命令,仅当前轮 task 有效】';
|
|
335
|
+
/**
|
|
336
|
+
* Put the one-task permission notice between the rendered message envelope and
|
|
337
|
+
* the user's literal input. The normal message fragment ends its envelope in
|
|
338
|
+
* `›`; matching the dated normal-message form avoids placing the notice inside
|
|
339
|
+
* an owner-hint envelope when one is present before the real inbound message.
|
|
340
|
+
*/
|
|
341
|
+
function insertMessagePromptContext(prompt, context, userContent) {
|
|
342
|
+
if (!context)
|
|
343
|
+
return prompt;
|
|
344
|
+
const headers = [...prompt.matchAll(/‹\d{4}-\d{2}-\d{2} [^\n]*· from:[^\n]*›/gu)];
|
|
345
|
+
const renderedHeader = headers[0];
|
|
346
|
+
const headerEnd = renderedHeader
|
|
347
|
+
? (renderedHeader.index ?? 0) + renderedHeader[0].length
|
|
348
|
+
: -1;
|
|
349
|
+
if (headerEnd >= 0) {
|
|
350
|
+
const remainder = prompt.slice(headerEnd).replace(/^\n+/u, '');
|
|
351
|
+
return `${prompt.slice(0, headerEnd)}\n${context}${remainder ? `\n${remainder}` : ''}`;
|
|
352
|
+
}
|
|
353
|
+
if (userContent?.trim()) {
|
|
354
|
+
const contentStart = prompt.indexOf(userContent);
|
|
355
|
+
if (contentStart >= 0) {
|
|
356
|
+
return `${prompt.slice(0, contentStart)}${context}\n${prompt.slice(contentStart)}`;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Internal recovery prompts have no message envelope; keep their context at
|
|
360
|
+
// the front so the runner still sees it for the same task.
|
|
361
|
+
return `${context}\n\n${prompt}`;
|
|
362
|
+
}
|
|
328
363
|
function getStreamErrorText(result) {
|
|
329
|
-
|
|
364
|
+
const parts = [
|
|
330
365
|
result.errors?.join('\n'),
|
|
331
|
-
result.lastReplyText,
|
|
332
|
-
result.fullText,
|
|
333
366
|
result.subtype,
|
|
334
367
|
result.terminalReason,
|
|
335
368
|
]
|
|
336
|
-
.filter((part) => typeof part === 'string' && part.trim().length > 0)
|
|
337
|
-
|
|
369
|
+
.filter((part) => typeof part === 'string' && part.trim().length > 0);
|
|
370
|
+
// Some older runners put the terminal diagnostic in `result`, but never
|
|
371
|
+
// treat streamed assistant text as an error merely because the turn failed.
|
|
372
|
+
const fullText = result.fullText.trim();
|
|
373
|
+
const fullTextLooksLikeDiagnostic = fullText.length > 0 && (isRetryableError(new Error(fullText))
|
|
374
|
+
|| isContextTooLongText(fullText));
|
|
375
|
+
if (result.isError && !result.hasReceivedText && fullText
|
|
376
|
+
&& (parts.length === 0 || fullTextLooksLikeDiagnostic)) {
|
|
377
|
+
parts.unshift(fullText);
|
|
378
|
+
}
|
|
379
|
+
return parts.join('\n');
|
|
338
380
|
}
|
|
339
381
|
function getStreamErrorMessage(result, includeEmoji = false) {
|
|
340
382
|
const raw = getStreamErrorText(result) || '任务执行失败';
|
|
341
383
|
const mapped = getErrorMessage(new Error(raw), result.terminalReason, includeEmoji);
|
|
342
384
|
const generic = includeEmoji ? '❌ 处理消息时出错,请稍后重试' : '处理消息时出错,请稍后重试';
|
|
343
|
-
|
|
385
|
+
// Unknown runner-specific terminal reasons do not have a friendly mapping.
|
|
386
|
+
// Preserve their diagnostic text instead of collapsing it to the generic
|
|
387
|
+
// "处理消息时出错" message.
|
|
388
|
+
if (raw && mapped === generic) {
|
|
344
389
|
return includeEmoji ? `❌ ${raw}` : raw;
|
|
345
390
|
}
|
|
346
391
|
return mapped;
|
|
347
392
|
}
|
|
348
|
-
function getRuntimeErrorMessage(raw, includeEmoji = false) {
|
|
349
|
-
const fallback = raw || '任务执行失败';
|
|
350
|
-
const mapped = getErrorMessage(new Error(fallback), undefined, includeEmoji);
|
|
351
|
-
const generic = includeEmoji ? '❌ 处理消息时出错,请稍后重试' : '处理消息时出错,请稍后重试';
|
|
352
|
-
if (mapped === generic)
|
|
353
|
-
return includeEmoji ? `❌ ${fallback}` : fallback;
|
|
354
|
-
return mapped;
|
|
355
|
-
}
|
|
356
|
-
function normalizeComparableText(text) {
|
|
357
|
-
return text.replace(/\s+/g, ' ').trim();
|
|
358
|
-
}
|
|
359
|
-
function isPendingTextSameAsStreamError(pendingText, errorText) {
|
|
360
|
-
const pending = normalizeComparableText(pendingText);
|
|
361
|
-
const error = normalizeComparableText(errorText);
|
|
362
|
-
if (!pending || !error || pending.length < 20)
|
|
363
|
-
return false;
|
|
364
|
-
return pending === error || pending.includes(error) || error.includes(pending);
|
|
365
|
-
}
|
|
366
393
|
function streamHitContextLimit(result) {
|
|
367
394
|
return result.terminalReason === 'prompt_too_long' ||
|
|
368
395
|
isContextTooLongText(result.lastReplyText) ||
|
|
@@ -446,12 +473,6 @@ function getContextTooLongHint(agent) {
|
|
|
446
473
|
}
|
|
447
474
|
return '上下文过长,请精简提问,或使用 /new 新建会话后继续';
|
|
448
475
|
}
|
|
449
|
-
function getContextCompactFailedHint(agent) {
|
|
450
|
-
if (canCompactAgent(agent)) {
|
|
451
|
-
return '上下文过长,自动压缩失败,请手动输入 /compact 重试';
|
|
452
|
-
}
|
|
453
|
-
return '上下文过长,请精简提问,或使用 /new 新建会话后继续';
|
|
454
|
-
}
|
|
455
476
|
function canCompactAgent(agent) {
|
|
456
477
|
return hasCompact(agent) && agent.capabilities?.compact !== false;
|
|
457
478
|
}
|
|
@@ -1042,7 +1063,7 @@ export class ResponseEngine {
|
|
|
1042
1063
|
return true;
|
|
1043
1064
|
}
|
|
1044
1065
|
async retryAfterContextRecovery(prompt, opts) {
|
|
1045
|
-
const { streamKey, renderer, agent, session, absoluteProjectPath, effectiveSystemPrompt, modelOverride, runtimeEnv, resetTimer, shouldSuppress, proactive, turnLease, beforeRunQuery, } = opts;
|
|
1066
|
+
const { streamKey, renderer, agent, session, absoluteProjectPath, effectiveSystemPrompt, modelOverride, runtimeEnv, resetTimer, shouldSuppress, proactive, turnLease, beforeRunQuery, messagePromptContext, } = opts;
|
|
1046
1067
|
if (!session.agentSessionId || !canCompactAgent(agent)) {
|
|
1047
1068
|
throw new Error('CONTEXT_COMPACT_FAILED');
|
|
1048
1069
|
}
|
|
@@ -1081,7 +1102,7 @@ export class ResponseEngine {
|
|
|
1081
1102
|
await this.emitOperationalNotice(renderer, '✅ 压缩完成,继续处理...', 'info', 'compact-retry');
|
|
1082
1103
|
await renderer.flush();
|
|
1083
1104
|
beforeRunQuery?.();
|
|
1084
|
-
const retryStream = await agent.runQuery(session.id, prompt, absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, recoveryModelOverride, runtimeEnv);
|
|
1105
|
+
const retryStream = await agent.runQuery(session.id, insertMessagePromptContext(prompt, messagePromptContext), absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, recoveryModelOverride, runtimeEnv);
|
|
1085
1106
|
if (await this.cancelStaleRunnerStart(agent, session.id, streamKey, turnLease, turnLease.taskId)) {
|
|
1086
1107
|
throw new Error('RUNNER_START_SUPERSEDED');
|
|
1087
1108
|
}
|
|
@@ -1365,11 +1386,11 @@ export class ResponseEngine {
|
|
|
1365
1386
|
'/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
|
|
1366
1387
|
'/stop', '/pause', '/resume', '/clear', '/compact', '/renew', '/del', '/perm', '/file', '/check',
|
|
1367
1388
|
'/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode',
|
|
1368
|
-
'/aid', '/upgrade', '/evolagent',
|
|
1389
|
+
'/aid', '/upgrade', '/evolagent', '/pair ',
|
|
1369
1390
|
];
|
|
1370
1391
|
/** 判断消息内容是否为已知命令 */
|
|
1371
1392
|
isKnownCommand(content) {
|
|
1372
|
-
return content === '/s' ||
|
|
1393
|
+
return content === '/s' || content === '/pair' ||
|
|
1373
1394
|
ResponseEngine.COMMAND_PREFIXES.some(cmd => content.startsWith(cmd));
|
|
1374
1395
|
}
|
|
1375
1396
|
/**
|
|
@@ -2689,6 +2710,7 @@ export class ResponseEngine {
|
|
|
2689
2710
|
: body;
|
|
2690
2711
|
// 先用裸文本兜底;vars 构造完成后用消息渲染层重算(见下方 effectivePrompt 重赋值)。
|
|
2691
2712
|
let effectivePrompt = wrapPrompt(message.content);
|
|
2713
|
+
let fullAccessMessagePromptContext;
|
|
2692
2714
|
let renderResult;
|
|
2693
2715
|
let effectiveSystemPrompt;
|
|
2694
2716
|
let modelOverride;
|
|
@@ -2784,6 +2806,9 @@ export class ResponseEngine {
|
|
|
2784
2806
|
// records all observe the same normalized value.
|
|
2785
2807
|
permissionContext.permissionMode = effectivePermissionMode;
|
|
2786
2808
|
permissionContext.executionPermission = fullAccessAuthorization;
|
|
2809
|
+
fullAccessMessagePromptContext = fullAccessAuthorization
|
|
2810
|
+
? FULL_ACCESS_MESSAGE_PROMPT_CONTEXT
|
|
2811
|
+
: undefined;
|
|
2787
2812
|
// 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
|
|
2788
2813
|
// 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
|
|
2789
2814
|
// 多对端并发各自独立解析、各自传参,无共享状态可被污染。
|
|
@@ -2878,7 +2903,9 @@ export class ResponseEngine {
|
|
|
2878
2903
|
agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
|
|
2879
2904
|
primaryModel = modelOverride?.model || agentModel;
|
|
2880
2905
|
if (!hasExplicitTriggerModel && primaryModel) {
|
|
2881
|
-
const fallbackResolution = eligibleFallbackModels({
|
|
2906
|
+
const fallbackResolution = await eligibleFallbackModels({
|
|
2907
|
+
selfAid: selfAid || undefined,
|
|
2908
|
+
catalog: agent.getModelCatalog?.bind(agent),
|
|
2882
2909
|
baseagent: normalizedBaseagent.canonical,
|
|
2883
2910
|
currentModel: primaryModel,
|
|
2884
2911
|
resolveModelId: typeof agent.resolveModelId === 'function'
|
|
@@ -3218,6 +3245,12 @@ export class ResponseEngine {
|
|
|
3218
3245
|
if (v2HandoffIds.length > 0 && renderResult?.body.trim())
|
|
3219
3246
|
handoffPromptRendered = true;
|
|
3220
3247
|
}
|
|
3248
|
+
// Keep this notice in the per-turn message prompt. Injecting it into
|
|
3249
|
+
// the system prompt would alter long-lived runner prompt caches and
|
|
3250
|
+
// could make a one-turn grant look persistent.
|
|
3251
|
+
if (fullAccessMessagePromptContext && effectivePrompt.trim()) {
|
|
3252
|
+
effectivePrompt = insertMessagePromptContext(effectivePrompt, fullAccessMessagePromptContext, message.content);
|
|
3253
|
+
}
|
|
3221
3254
|
// 空消息防护:在 agent 调用之前检查 prompt 是否为空
|
|
3222
3255
|
// 防止空消息(或纯空格消息)浪费 API 调用
|
|
3223
3256
|
if (!effectivePrompt.trim()) {
|
|
@@ -3527,6 +3560,7 @@ export class ResponseEngine {
|
|
|
3527
3560
|
proactiveSelfAid: selfAid,
|
|
3528
3561
|
permissionMode: effectivePermissionMode,
|
|
3529
3562
|
turnLease,
|
|
3563
|
+
messagePromptContext: fullAccessMessagePromptContext,
|
|
3530
3564
|
beforeRunQuery: fullAccessAuthorization
|
|
3531
3565
|
? () => this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent)
|
|
3532
3566
|
: undefined,
|
|
@@ -3573,6 +3607,7 @@ export class ResponseEngine {
|
|
|
3573
3607
|
proactiveSelfAid: selfAid,
|
|
3574
3608
|
permissionMode: effectivePermissionMode,
|
|
3575
3609
|
turnLease,
|
|
3610
|
+
messagePromptContext: fullAccessMessagePromptContext,
|
|
3576
3611
|
beforeRunQuery: fullAccessAuthorization
|
|
3577
3612
|
? () => this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent)
|
|
3578
3613
|
: undefined,
|
|
@@ -5254,8 +5289,10 @@ export class ResponseEngine {
|
|
|
5254
5289
|
// 运行时错误(Codex: turn.failed / item error)
|
|
5255
5290
|
if (event.type === 'error') {
|
|
5256
5291
|
logger.warn(`[ResponseEngine] error event: ${event.errorType}: ${event.error}`);
|
|
5257
|
-
//
|
|
5258
|
-
|
|
5292
|
+
// Keep runtime diagnostics separate from assistant prose. Appending
|
|
5293
|
+
// an error to lastReplyText makes the terminal flush treat the error
|
|
5294
|
+
// as user-visible model output and can also make it look like a
|
|
5295
|
+
// duplicate of a pending text event.
|
|
5259
5296
|
completeResult = {
|
|
5260
5297
|
...completeResult,
|
|
5261
5298
|
isError: true,
|
|
@@ -5265,14 +5302,6 @@ export class ResponseEngine {
|
|
|
5265
5302
|
lastReplyText,
|
|
5266
5303
|
fullText: '',
|
|
5267
5304
|
};
|
|
5268
|
-
// 上下文过长的错误不在此处输出 notice,留给外层 isPromptTooLong 触发 auto-compact
|
|
5269
|
-
const isContextError = isContextTooLongText(event.error || '');
|
|
5270
|
-
const isRetryableRuntimeError = isRetryableError(new Error(event.error || ''));
|
|
5271
|
-
const isRecoverableModelError = classifyError(new Error(event.error || '')) === ErrorType.MODEL_UNAVAILABLE;
|
|
5272
|
-
if (!isContextError && !isRetryableRuntimeError && !isRecoverableModelError && !hasErrorResult && !shouldSuppress()) {
|
|
5273
|
-
hasErrorResult = true;
|
|
5274
|
-
renderer.addNotice(getRuntimeErrorMessage(event.error || '任务执行失败', false), 'warn', 'runtime-error', true);
|
|
5275
|
-
}
|
|
5276
5305
|
}
|
|
5277
5306
|
// 完成事件
|
|
5278
5307
|
// SDK 可能产生多个 complete 事件(如 subagent 或 auto-compact 二次查询),
|
|
@@ -5291,34 +5320,6 @@ export class ResponseEngine {
|
|
|
5291
5320
|
madeProgress = true;
|
|
5292
5321
|
// thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
|
|
5293
5322
|
// 由那里按 LLM 输出的每个 text item 单独写一条,此处不再写。
|
|
5294
|
-
// 失败且无前置错误输出:显示 errors 摘要
|
|
5295
|
-
// 但用户主动中断(新消息打断 或 /stop 命令)时不显示错误提示
|
|
5296
|
-
// 上下文过长的错误留给外层 isPromptTooLong 触发 auto-compact,不在此处输出
|
|
5297
|
-
const interruptReason = this.interruptedSessions.get(session.id);
|
|
5298
|
-
const isUserInterrupt = isNonErrorInterruptReason(interruptReason)
|
|
5299
|
-
|| isExpectedTerminalInterrupt(event.terminalReason);
|
|
5300
|
-
const isContextTooLong = event.terminalReason === 'prompt_too_long'
|
|
5301
|
-
|| isContextTooLongText(event.errors?.join(' ') || '')
|
|
5302
|
-
|| isContextTooLongText(lastReplyText);
|
|
5303
|
-
const completeErrorText = getStreamErrorText(completeResult);
|
|
5304
|
-
const isRetryableCompleteError = event.isError && !isContextTooLong && completeErrorText
|
|
5305
|
-
? isRetryableError(new Error(completeErrorText))
|
|
5306
|
-
: false;
|
|
5307
|
-
const isRecoverableModelError = event.isError && !isContextTooLong && completeErrorText
|
|
5308
|
-
? classifyError(new Error(completeErrorText)) === ErrorType.MODEL_UNAVAILABLE
|
|
5309
|
-
: false;
|
|
5310
|
-
if (event.isError && completeErrorText && isPendingTextSameAsStreamError(renderer.getRemainingText(), completeErrorText)) {
|
|
5311
|
-
renderer.discardPendingText();
|
|
5312
|
-
}
|
|
5313
|
-
if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt && !isContextTooLong && !isRetryableCompleteError && !isRecoverableModelError) {
|
|
5314
|
-
// 使用 terminalReason 提供更友好的错误提示(不带 emoji,由 formatter 统一加)
|
|
5315
|
-
const userFriendlyMessage = event.terminalReason === 'prompt_too_long'
|
|
5316
|
-
? getContextTooLongHint(agent)
|
|
5317
|
-
: event.terminalReason === 'context_compact_failed'
|
|
5318
|
-
? getContextCompactFailedHint(agent)
|
|
5319
|
-
: getStreamErrorMessage(completeResult, false);
|
|
5320
|
-
renderer.addNotice(userFriendlyMessage, 'warn', 'task-error', true);
|
|
5321
|
-
}
|
|
5322
5323
|
// 中间 complete:flush 掉已有 activities(不带 isFinal),让中间结果及时显示
|
|
5323
5324
|
// 最终文本留给流结束后的统一 flush(true)
|
|
5324
5325
|
if (renderer.hasContent()) {
|