evolcore 0.0.12 → 0.0.14
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 +37 -0
- package/bin/codex-managed-hook.mjs +4 -1
- package/bin/ec-safe-output.js +26 -7
- package/bin/install-codex-managed-hooks.mjs +201 -0
- package/dist/agents/claude-runner.js +105 -11
- package/dist/agents/codex-app-server-client.js +176 -7
- package/dist/agents/codex-runner.js +248 -84
- package/dist/agents/ecagent-runner.js +21 -2
- package/dist/agents/gemini-runner.js +9 -4
- package/dist/aun/aid/control-aid.js +36 -13
- package/dist/aun/aid/store.js +13 -7
- package/dist/aun/group-fs-download.js +56 -0
- package/dist/aun/group-identity.js +1 -2
- package/dist/aun/msg/group.js +11 -33
- package/dist/aun/msg/index.js +1 -2
- package/dist/aun/msg/managed-operation.js +176 -55
- package/dist/aun/msg/upload.js +16 -1
- package/dist/aun/outbox.js +31 -2
- package/dist/aun/rpc/client.js +66 -0
- package/dist/aun/rpc/index.js +1 -2
- package/dist/aun/storage/{upload.js → client.js} +11 -2
- package/dist/aun/storage/index.js +1 -2
- package/dist/channels/aun.js +514 -150
- package/dist/channels/daemon.js +2 -0
- package/dist/cli/agent.js +9 -96
- package/dist/cli/aun-commands.js +1 -1
- package/dist/cli/cli-argv.js +10 -0
- package/dist/cli/config.js +9 -17
- package/dist/cli/ctl-command.js +1 -1
- package/dist/cli/daemon-commands.js +39 -24
- package/dist/cli/fs-command.js +65 -19
- package/dist/cli/init.js +77 -8
- package/dist/cli/queue-command.js +42 -0
- package/dist/cli/restart-monitor.js +3 -14
- package/dist/cli/task-context.js +172 -0
- package/dist/cli/trigger-command.js +7 -0
- package/dist/config/access-policy.js +1 -2
- package/dist/config/builtin-roles.js +7 -0
- package/dist/config/config-manager.js +27 -3
- package/dist/config/contact-book-store.js +7 -2
- package/dist/config/peer-role-resolver.js +6 -2
- package/dist/config/role-migration-startup.js +53 -0
- package/dist/config/role-store.js +1 -1
- package/dist/core/auth/agent-delegation.js +1 -1
- package/dist/core/auth/auth-gateway.js +30 -1
- package/dist/core/auth/authorization-audit.js +47 -0
- package/dist/core/auth/operation-authorizer.js +1 -0
- package/dist/core/auth/operation-catalog.js +10 -1
- package/dist/core/bootstrap-messages.js +9 -2
- package/dist/core/bootstrap-service.js +8 -1
- package/dist/core/command/command-handler.js +201 -51
- package/dist/core/command/menu-catalog.js +4 -0
- package/dist/core/command/menu-handler.js +85 -18
- package/dist/core/command/menu-protocol.js +23 -2
- package/dist/core/command/slash-handler.js +58 -10
- package/dist/core/event-catalog.js +8 -0
- package/dist/core/handoff/runtime.js +12 -1
- package/dist/core/handoff/store.js +5 -1
- package/dist/core/interaction-router.js +10 -0
- package/dist/core/message/im-renderer.js +34 -1
- package/dist/core/message/message-bridge.js +68 -13
- package/dist/core/message/message-log.js +2 -0
- package/dist/core/message/message-queue.js +267 -39
- package/dist/core/message/message-utils.js +81 -4
- package/dist/core/message/response-engine.js +242 -48
- package/dist/core/message/stream-debouncer.js +10 -2
- package/dist/core/permission/approval-gateway.js +1 -0
- package/dist/core/permission/ec-command-parser.js +649 -18
- package/dist/core/permission/index.js +1 -1
- package/dist/core/permission/mode.js +15 -0
- package/dist/core/permission/readonly-shell-query.js +148 -8
- package/dist/core/permission/sandbox-runtime.js +21 -0
- package/dist/core/permission/tool-policy.js +266 -56
- package/dist/core/protected-paths.js +107 -2
- package/dist/core/runtime-lock.js +101 -0
- package/dist/core/session/session-manager.js +21 -2
- package/dist/core/session/session-mapper.js +20 -1
- package/dist/eck/detect.js +18 -1
- package/dist/eck/group-rules-sync.js +15 -44
- package/dist/index.js +191 -89
- package/dist/ipc.js +16 -3
- package/dist/paths.js +3 -0
- package/dist/response-system/engines/v1/proactive-flow.js +92 -8
- package/dist/response-system/modes/single-session/index.js +3 -0
- package/dist/trigger/feedback.js +64 -11
- package/dist/trigger/history.js +42 -7
- package/dist/trigger/manager.js +21 -1
- package/dist/trigger/parser.js +14 -2
- package/dist/trigger/scheduler.js +1 -0
- package/dist/trigger/validation.js +67 -6
- package/dist/utils/ecweb-supervisor.js +332 -0
- package/dist/utils/error-utils.js +17 -0
- package/dist/utils/logger.js +37 -4
- package/kits/docs/evolcore/trigger.md +17 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/templates/roles/admin.json +7 -0
- package/kits/templates/roles/member.json +8 -1
- package/kits/templates/roles/visitor.json +2 -0
- package/package.json +1 -1
- package/dist/aun/msg/payload-type.js +0 -27
- package/dist/aun/rpc/caller.js +0 -42
- package/dist/aun/rpc/connection.js +0 -25
- package/dist/aun/storage/manage.js +0 -10
- package/dist/config/access-policy-domain.js +0 -18
- package/dist/config/config-batch-get.js +0 -11
- package/dist/config/owner-policy.js +0 -4
- package/dist/config/role-config-v4-startup.js +0 -32
- package/dist/config/role-config-v5-startup.js +0 -27
- package/dist/core/auth/trigger-authorization.js +0 -15
- package/dist/core/interaction-registration.js +0 -10
- package/dist/core/permission/execution-sandbox.js +0 -16
- package/dist/core/relation/peer-key.js +0 -1
- package/dist/core/session/session-key.js +0 -24
- package/dist/eck/baseagent-caps.js +0 -18
- package/dist/eck/rules-loader.js +0 -28
|
@@ -6,7 +6,7 @@ import { hasModelSwitcher } from '../../agents/runner-types.js';
|
|
|
6
6
|
import { getCodexEfforts } from '../../agents/codex-runner.js';
|
|
7
7
|
import { execCodexCliSync, resolveCodexCliPath } from '../../utils/codex-cli.js';
|
|
8
8
|
import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
|
|
9
|
-
import { buildEnvelope } from '../message/message-utils.js';
|
|
9
|
+
import { buildEnvelope, isDeliveryTarget, isDeliveryTargetForChannel, replyContextFromSession } from '../message/message-utils.js';
|
|
10
10
|
import path from 'path';
|
|
11
11
|
import fs from 'fs';
|
|
12
12
|
import os from 'os';
|
|
@@ -28,7 +28,7 @@ import { normalizeCliArgv, parseCliIntent, parseLegacyCliCommand, validateCliArg
|
|
|
28
28
|
import { auditCommandAuthorization, hashArgv } from '../auth/authorization-audit.js';
|
|
29
29
|
import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
|
|
30
30
|
import { evaluateRoleOperationCapability } from '../auth/operation-authorizer.js';
|
|
31
|
-
import { splitConfigBatchGetArgv } from '../../
|
|
31
|
+
import { splitConfigBatchGetArgv } from '../../cli/cli-argv.js';
|
|
32
32
|
import { chatmodeFieldForPeer, resolveChatModeForField } from '../message/peer-mode.js';
|
|
33
33
|
import { PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
|
|
34
34
|
import { resolveRuntimePermissionMode, resolveRuntimeStringField, validateRuntimeStringFieldOverride, } from '../role/runtime-policy.js';
|
|
@@ -36,6 +36,7 @@ import { isManagementRole } from '../../config/builtin-roles.js';
|
|
|
36
36
|
import { SYSTEM_CONTROL_CHANNEL } from '../system-channels.js';
|
|
37
37
|
import { logger } from '../../utils/logger.js';
|
|
38
38
|
import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
|
|
39
|
+
import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
|
|
39
40
|
import { evolMenuResponseTransportMetadata, menuFailure, menuCommandForName, menuSuccess, normalizeMenuError, validateConfigWriteScope, validateMenuRequest, } from './menu-protocol.js';
|
|
40
41
|
import { roleMenuAction, roleMenuOperation, roleMenuOptions, roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
|
|
41
42
|
import { handleConnectMenu, connectMenuOperation } from './connect-menu.js';
|
|
@@ -1013,8 +1014,13 @@ function buildRelationIntentArgs(params) {
|
|
|
1013
1014
|
return out;
|
|
1014
1015
|
}
|
|
1015
1016
|
function buildMenuAuthSubject(owner, params) {
|
|
1016
|
-
if (params.subject)
|
|
1017
|
-
return
|
|
1017
|
+
if (params.subject) {
|
|
1018
|
+
return {
|
|
1019
|
+
...params.subject,
|
|
1020
|
+
...(params.requestId && !params.subject.requestId ? { requestId: params.requestId } : {}),
|
|
1021
|
+
...(params.session?.id && !params.subject.sessionId ? { sessionId: params.session.id } : {}),
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1018
1024
|
const agent = owner.getOwningAgent?.(params.channel);
|
|
1019
1025
|
const selfAid = agent?.aid || params.session?.selfAID;
|
|
1020
1026
|
const channelType = owner.resolveChannelType?.(params.channel) || params.session?.channelType;
|
|
@@ -1025,6 +1031,8 @@ function buildMenuAuthSubject(owner, params) {
|
|
|
1025
1031
|
return buildAuthSubject({
|
|
1026
1032
|
selfAid,
|
|
1027
1033
|
actorId: params.userId,
|
|
1034
|
+
requestId: params.requestId,
|
|
1035
|
+
sessionId: params.session?.id,
|
|
1028
1036
|
channel: params.channel,
|
|
1029
1037
|
channelType: channelType || params.channel.split('#')[0],
|
|
1030
1038
|
channelId: params.channelId,
|
|
@@ -1114,7 +1122,7 @@ function buildRoleMenuContext(owner, channel, subject) {
|
|
|
1114
1122
|
channel: contactAdapter.channelName,
|
|
1115
1123
|
channelId: applicantAid,
|
|
1116
1124
|
agentName: owningAgent?.name ?? '<unknown>',
|
|
1117
|
-
replyContext: { metadata: evolMenuResponseTransportMetadata() },
|
|
1125
|
+
replyContext: { delivery: { chatType: 'private' }, metadata: evolMenuResponseTransportMetadata() },
|
|
1118
1126
|
}), {
|
|
1119
1127
|
kind: 'custom',
|
|
1120
1128
|
channelType: 'aun',
|
|
@@ -2710,6 +2718,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
2710
2718
|
const subject = buildMenuAuthSubject(this, {
|
|
2711
2719
|
identity: authIdentity,
|
|
2712
2720
|
session: authSession,
|
|
2721
|
+
requestId,
|
|
2713
2722
|
explicitChatType,
|
|
2714
2723
|
channel,
|
|
2715
2724
|
channelId,
|
|
@@ -2927,6 +2936,10 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
2927
2936
|
if (triggerThread !== undefined && triggerThread !== 'per_run' && triggerThread !== 'by_run' && triggerThread !== 'by_trigger') {
|
|
2928
2937
|
return { error: `无效 triggerThread: ${triggerThread}`, code: 'INVALID_ARGS' };
|
|
2929
2938
|
}
|
|
2939
|
+
const targetChatType = args.targetChatType;
|
|
2940
|
+
if (targetChatType !== undefined && targetChatType !== 'private' && targetChatType !== 'group') {
|
|
2941
|
+
return { error: `无效 targetChatType: ${targetChatType}`, code: 'INVALID_ARGS' };
|
|
2942
|
+
}
|
|
2930
2943
|
const parsed = {
|
|
2931
2944
|
scheduleType: args.scheduleType,
|
|
2932
2945
|
scheduleValue: String(args.scheduleValue ?? ''),
|
|
@@ -2937,6 +2950,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
2937
2950
|
name: args.name,
|
|
2938
2951
|
targetChannel: args.targetChannel,
|
|
2939
2952
|
targetChannelId: args.targetChannelId,
|
|
2953
|
+
targetChatType,
|
|
2940
2954
|
targetThreadId: args.targetThreadId,
|
|
2941
2955
|
agentId: args.agentId,
|
|
2942
2956
|
model: args.model,
|
|
@@ -3313,13 +3327,56 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
3313
3327
|
}
|
|
3314
3328
|
if (action === 'restart') {
|
|
3315
3329
|
const suppressRealRestart = shouldSuppressRealRestart();
|
|
3330
|
+
let restartReplyContext;
|
|
3331
|
+
try {
|
|
3332
|
+
restartReplyContext = session
|
|
3333
|
+
? replyContextFromSession(session, transportReplyContext)
|
|
3334
|
+
: transportReplyContext;
|
|
3335
|
+
}
|
|
3336
|
+
catch (error) {
|
|
3337
|
+
const code = error?.code;
|
|
3338
|
+
if (channel.split('#', 1)[0].toLowerCase() !== 'aun'
|
|
3339
|
+
|| (code !== 'AUN_OUTBOUND_ROUTE_REQUIRED' && code !== 'AUN_OUTBOUND_ROUTE_MISMATCH')) {
|
|
3340
|
+
throw error;
|
|
3341
|
+
}
|
|
3342
|
+
return {
|
|
3343
|
+
error: '当前 AUN 会话缺少有效的持久化出站路由,请从可信入站上下文重试',
|
|
3344
|
+
code,
|
|
3345
|
+
};
|
|
3346
|
+
}
|
|
3347
|
+
const restartDelivery = restartReplyContext?.delivery
|
|
3348
|
+
?? (explicitChatType === 'group'
|
|
3349
|
+
? { chatType: 'group', groupId: channelId }
|
|
3350
|
+
: explicitChatType === 'private'
|
|
3351
|
+
? { chatType: 'private' }
|
|
3352
|
+
: undefined);
|
|
3353
|
+
if (channel.split('#', 1)[0].toLowerCase() === 'aun'
|
|
3354
|
+
&& !isDeliveryTargetForChannel(restartDelivery, channelId)) {
|
|
3355
|
+
return {
|
|
3356
|
+
error: '无法确定有效的重启通知出站路由,请从可信会话上下文重试',
|
|
3357
|
+
code: isDeliveryTarget(restartDelivery)
|
|
3358
|
+
? 'AUN_OUTBOUND_ROUTE_MISMATCH'
|
|
3359
|
+
: 'AUN_OUTBOUND_ROUTE_REQUIRED',
|
|
3360
|
+
};
|
|
3361
|
+
}
|
|
3316
3362
|
if (!suppressRealRestart) {
|
|
3317
|
-
const restartInfo = {
|
|
3363
|
+
const restartInfo = {
|
|
3364
|
+
channel,
|
|
3365
|
+
channelId,
|
|
3366
|
+
timestamp: Date.now(),
|
|
3367
|
+
...(restartDelivery ? { delivery: restartDelivery } : {}),
|
|
3368
|
+
};
|
|
3318
3369
|
const controlDir = daemonControlDir();
|
|
3319
3370
|
fs.mkdirSync(controlDir, { recursive: true });
|
|
3320
3371
|
fs.writeFileSync(path.join(controlDir, 'restart-pending.json'), JSON.stringify(restartInfo));
|
|
3321
|
-
|
|
3372
|
+
// Use the canonical CLI restart path so the restart itself performs
|
|
3373
|
+
// the version check and automatic upgrades before replacing the daemon.
|
|
3374
|
+
// Do not inherit managed-session credentials: otherwise cmdRestart can
|
|
3375
|
+
// delegate back to this daemon and recursively enqueue another restart.
|
|
3376
|
+
spawnDetachedNode([path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart'], {
|
|
3322
3377
|
EVOLCORE_HOME: resolvePaths().root,
|
|
3378
|
+
EVOLCORE_SESSION_ID: '',
|
|
3379
|
+
[AGENT_DELEGATION_TOKEN_ENV]: '',
|
|
3323
3380
|
});
|
|
3324
3381
|
}
|
|
3325
3382
|
else {
|
|
@@ -3386,6 +3443,17 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
3386
3443
|
? (session?.metadata?.groupId || channelId)
|
|
3387
3444
|
: userId;
|
|
3388
3445
|
const cliPeerKey = cliChannelType && cliPeerKeyId ? formatPeerKey(cliChannelType, cliPeerKeyId) : undefined;
|
|
3446
|
+
const subject = buildMenuAuthSubject(this, {
|
|
3447
|
+
identity,
|
|
3448
|
+
session,
|
|
3449
|
+
requestId,
|
|
3450
|
+
explicitChatType,
|
|
3451
|
+
channel,
|
|
3452
|
+
channelId,
|
|
3453
|
+
userId,
|
|
3454
|
+
fromControlChannel: fromControlChannel ?? false,
|
|
3455
|
+
subject: authSubject,
|
|
3456
|
+
});
|
|
3389
3457
|
if (Array.isArray(args?.argv)) {
|
|
3390
3458
|
argv = withDefaultRelationContext(argv, { self: cliSelfAid, peer: cliPeerKey });
|
|
3391
3459
|
}
|
|
@@ -3404,6 +3472,8 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
3404
3472
|
await auditCommandAuthorization({
|
|
3405
3473
|
ts: Date.now(), source: 'menu.cli', operation: parsed.intent.operation,
|
|
3406
3474
|
scope: parsed.intent.scope, dangerous: true, actorId: userId,
|
|
3475
|
+
requestId, sessionId: subject.sessionId, agentAid: subject.selfAid,
|
|
3476
|
+
permissionMode: subject.permissionMode,
|
|
3407
3477
|
channel, channelId, role: identity.role, decision: 'deny', code: 'NOT_ALLOWED',
|
|
3408
3478
|
reason: 'Unrecognized CLI command', taskId: requestId, argvHash: hashArgv(intentArgv),
|
|
3409
3479
|
name: 'cli', action: 'exec', args: { argv: [...requestedArgv] },
|
|
@@ -3412,16 +3482,6 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
3412
3482
|
}
|
|
3413
3483
|
parsedCommands.push(parsed);
|
|
3414
3484
|
}
|
|
3415
|
-
const subject = buildMenuAuthSubject(this, {
|
|
3416
|
-
identity,
|
|
3417
|
-
session,
|
|
3418
|
-
explicitChatType,
|
|
3419
|
-
channel,
|
|
3420
|
-
channelId,
|
|
3421
|
-
userId,
|
|
3422
|
-
fromControlChannel: fromControlChannel ?? false,
|
|
3423
|
-
subject: authSubject,
|
|
3424
|
-
});
|
|
3425
3485
|
const selfAid = subject.selfAid;
|
|
3426
3486
|
const peerKey = subject.peerKey;
|
|
3427
3487
|
const isDaemonOwner = subject.isDaemonOwner;
|
|
@@ -3446,6 +3506,10 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
3446
3506
|
allowExplicitRelationTarget: true,
|
|
3447
3507
|
auditAllowed: false,
|
|
3448
3508
|
auditMetadata: {
|
|
3509
|
+
requestId,
|
|
3510
|
+
sessionId: subject.sessionId,
|
|
3511
|
+
agentAid: subject.selfAid,
|
|
3512
|
+
permissionMode: subject.permissionMode,
|
|
3449
3513
|
taskId: requestId,
|
|
3450
3514
|
argvHash: hashArgv(authArgv),
|
|
3451
3515
|
name: 'cli',
|
|
@@ -3466,6 +3530,8 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
3466
3530
|
ts: Date.now(), source: 'menu.cli', operation: parsed.intent.operation,
|
|
3467
3531
|
scope: parsed.intent.scope, dangerous: parsed.intent.dangerous ?? false,
|
|
3468
3532
|
actorId: userId, selfAid, peerKey,
|
|
3533
|
+
requestId, sessionId: subject.sessionId, agentAid: selfAid,
|
|
3534
|
+
permissionMode: subject.permissionMode,
|
|
3469
3535
|
channel, channelId, role: identity.role, isDaemonOwner,
|
|
3470
3536
|
fromControlChannel: fromControlChannel ?? false, decision: 'allow',
|
|
3471
3537
|
matchedRule: decision.command?.matchedRule, taskId: requestId, argvHash: hashArgv(argv),
|
|
@@ -3509,7 +3575,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
|
|
|
3509
3575
|
return { error: '通道不存在', code: 'EXEC_FAILED' };
|
|
3510
3576
|
if (!adapter.capabilities?.file)
|
|
3511
3577
|
return { error: '通道不支持文件发送', code: 'NOT_SUPPORTED' };
|
|
3512
|
-
const sessionReplyContext = session ? this.getReplyContext(session) : undefined;
|
|
3578
|
+
const sessionReplyContext = session ? replyContextFromSession(session, this.getReplyContext(session)) : undefined;
|
|
3513
3579
|
const replyCtx = transportReplyContext
|
|
3514
3580
|
? {
|
|
3515
3581
|
...(sessionReplyContext ?? {}),
|
|
@@ -3593,6 +3659,7 @@ async function execMenuForSystemControl(payload, context) {
|
|
|
3593
3659
|
const trustedSubject = buildAuthSubject({
|
|
3594
3660
|
selfAid: targetAid,
|
|
3595
3661
|
actorId: context.actorAid,
|
|
3662
|
+
requestId: id !== undefined && id !== null && String(id) ? String(id) : undefined,
|
|
3596
3663
|
channel: targetChannel,
|
|
3597
3664
|
channelType: 'aun',
|
|
3598
3665
|
channelId: context.actorAid || SYSTEM_CONTROL_CHANNEL,
|
|
@@ -269,6 +269,9 @@ export class MenuRequestDeduper {
|
|
|
269
269
|
ttlMs;
|
|
270
270
|
maxEntries;
|
|
271
271
|
entries = new Map();
|
|
272
|
+
/** Retain failed request identities briefly so a subsequent identical
|
|
273
|
+
* request is observable as a retry instead of an unrelated first attempt. */
|
|
274
|
+
failures = new Map();
|
|
272
275
|
constructor(ttlMs = 30_000, maxEntries = 2_048) {
|
|
273
276
|
this.ttlMs = ttlMs;
|
|
274
277
|
this.maxEntries = maxEntries;
|
|
@@ -279,7 +282,13 @@ export class MenuRequestDeduper {
|
|
|
279
282
|
if (existing) {
|
|
280
283
|
if (existing.fingerprint !== fingerprint)
|
|
281
284
|
return { conflict: true };
|
|
282
|
-
return { value: await existing.promise, replayed: true };
|
|
285
|
+
return { value: await existing.promise, replayed: true, retry: false };
|
|
286
|
+
}
|
|
287
|
+
const previousFailure = this.failures.get(key);
|
|
288
|
+
if (previousFailure) {
|
|
289
|
+
if (previousFailure.fingerprint !== fingerprint)
|
|
290
|
+
return { conflict: true };
|
|
291
|
+
this.failures.delete(key);
|
|
283
292
|
}
|
|
284
293
|
const promise = operation();
|
|
285
294
|
const entry = { fingerprint, promise, expiresAt: Date.now() + this.ttlMs };
|
|
@@ -288,10 +297,12 @@ export class MenuRequestDeduper {
|
|
|
288
297
|
try {
|
|
289
298
|
const value = await promise;
|
|
290
299
|
entry.expiresAt = Date.now() + this.ttlMs;
|
|
291
|
-
return { value, replayed: false };
|
|
300
|
+
return { value, replayed: false, retry: !!previousFailure };
|
|
292
301
|
}
|
|
293
302
|
catch (error) {
|
|
294
303
|
this.entries.delete(key);
|
|
304
|
+
this.failures.set(key, { fingerprint, expiresAt: Date.now() + this.ttlMs });
|
|
305
|
+
this.enforceLimit();
|
|
295
306
|
throw error;
|
|
296
307
|
}
|
|
297
308
|
}
|
|
@@ -301,6 +312,10 @@ export class MenuRequestDeduper {
|
|
|
301
312
|
if (entry.expiresAt <= now)
|
|
302
313
|
this.entries.delete(key);
|
|
303
314
|
}
|
|
315
|
+
for (const [key, entry] of this.failures) {
|
|
316
|
+
if (entry.expiresAt <= now)
|
|
317
|
+
this.failures.delete(key);
|
|
318
|
+
}
|
|
304
319
|
}
|
|
305
320
|
enforceLimit() {
|
|
306
321
|
while (this.entries.size > this.maxEntries) {
|
|
@@ -309,6 +324,12 @@ export class MenuRequestDeduper {
|
|
|
309
324
|
break;
|
|
310
325
|
this.entries.delete(oldest);
|
|
311
326
|
}
|
|
327
|
+
while (this.failures.size > this.maxEntries) {
|
|
328
|
+
const oldest = this.failures.keys().next().value;
|
|
329
|
+
if (!oldest)
|
|
330
|
+
break;
|
|
331
|
+
this.failures.delete(oldest);
|
|
332
|
+
}
|
|
312
333
|
}
|
|
313
334
|
}
|
|
314
335
|
export class MenuDiagnosticLimiter {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { hasModelSwitcher, hasPermissionController } from '../../agents/runner-types.js';
|
|
2
2
|
import { getCodexEfforts } from '../../agents/codex-runner.js';
|
|
3
|
-
import { buildEnvelope } from '../message/message-utils.js';
|
|
3
|
+
import { buildEnvelope, isDeliveryTargetForChannel } from '../message/message-utils.js';
|
|
4
4
|
import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
|
|
5
5
|
import { logger } from '../../utils/logger.js';
|
|
6
6
|
import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
|
|
@@ -2492,17 +2492,50 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2492
2492
|
// 执行重启逻辑(共用于卡片回调和文本确认)
|
|
2493
2493
|
const executeRestart = async () => {
|
|
2494
2494
|
const suppressRealRestart = shouldSuppressRealRestart();
|
|
2495
|
-
let replyContext;
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2495
|
+
let restartReplyContext = replyContext;
|
|
2496
|
+
try {
|
|
2497
|
+
if (!restartReplyContext?.delivery && threadId) {
|
|
2498
|
+
const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
|
|
2499
|
+
if (threadSession)
|
|
2500
|
+
restartReplyContext = this.getReplyContext(threadSession);
|
|
2501
|
+
}
|
|
2502
|
+
else if (!restartReplyContext?.delivery && activeSession) {
|
|
2503
|
+
restartReplyContext = this.getReplyContext(activeSession);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
catch (error) {
|
|
2507
|
+
const code = error?.code;
|
|
2508
|
+
if (channel.split('#', 1)[0].toLowerCase() !== 'aun'
|
|
2509
|
+
|| (code !== 'AUN_OUTBOUND_ROUTE_REQUIRED' && code !== 'AUN_OUTBOUND_ROUTE_MISMATCH')) {
|
|
2510
|
+
throw error;
|
|
2511
|
+
}
|
|
2512
|
+
logger.warn(`[System] Refusing restart from invalid persisted AUN session route: channel=${channel} channelId=${channelId} code=${code}`);
|
|
2513
|
+
return false;
|
|
2514
|
+
}
|
|
2515
|
+
// The authenticated inbound transport context takes precedence over a
|
|
2516
|
+
// potentially stale session snapshot. When no session exists, the
|
|
2517
|
+
// explicit command chat type is the only acceptable local fallback.
|
|
2518
|
+
if (restartReplyContext?.delivery === undefined) {
|
|
2519
|
+
restartReplyContext = {
|
|
2520
|
+
...(restartReplyContext ?? {}),
|
|
2521
|
+
...(chatType === 'group'
|
|
2522
|
+
? { delivery: { chatType: 'group', groupId: channelId } }
|
|
2523
|
+
: chatType === 'private'
|
|
2524
|
+
? { delivery: { chatType: 'private' } }
|
|
2525
|
+
: {}),
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2528
|
+
if (channel.split('#', 1)[0].toLowerCase() === 'aun'
|
|
2529
|
+
&& !isDeliveryTargetForChannel(restartReplyContext?.delivery, channelId)) {
|
|
2530
|
+
logger.warn(`[System] Refusing restart without a valid explicit delivery route: channel=${channel} channelId=${channelId}`);
|
|
2531
|
+
return false;
|
|
2500
2532
|
}
|
|
2501
2533
|
const restartInfo = {
|
|
2502
2534
|
channel,
|
|
2503
2535
|
channelId,
|
|
2504
2536
|
timestamp: Date.now(),
|
|
2505
|
-
...(
|
|
2537
|
+
...(restartReplyContext?.delivery ? { delivery: restartReplyContext.delivery } : {}),
|
|
2538
|
+
...(restartReplyContext?.replyToMessageId ? { rootId: restartReplyContext.replyToMessageId } : {}),
|
|
2506
2539
|
};
|
|
2507
2540
|
if (!suppressRealRestart) {
|
|
2508
2541
|
const controlDir = daemonControlDir();
|
|
@@ -2527,7 +2560,11 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2527
2560
|
channelId,
|
|
2528
2561
|
agentName: 'system',
|
|
2529
2562
|
chatmode: 'interactive',
|
|
2530
|
-
replyContext
|
|
2563
|
+
replyContext: restartReplyContext ?? (chatType === 'group'
|
|
2564
|
+
? { delivery: { chatType: 'group', groupId: channelId } }
|
|
2565
|
+
: chatType === 'private'
|
|
2566
|
+
? { delivery: { chatType: 'private' } }
|
|
2567
|
+
: undefined),
|
|
2531
2568
|
});
|
|
2532
2569
|
await adapter.send(envelope, { kind: 'command.result', text: '🔄 服务正在重启,请稍候...(约 5 秒后恢复)' });
|
|
2533
2570
|
// 等待消息发送完成后再延迟 kill
|
|
@@ -2570,7 +2607,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2570
2607
|
return { kind: 'command.result', text: sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。' };
|
|
2571
2608
|
}
|
|
2572
2609
|
}
|
|
2573
|
-
await executeRestart();
|
|
2610
|
+
const restarted = await executeRestart();
|
|
2611
|
+
if (!restarted) {
|
|
2612
|
+
return { kind: 'command.error', text: '❌ 无法确定重启通知的出站路由,请从可信会话上下文重试' };
|
|
2613
|
+
}
|
|
2574
2614
|
// executeRestart 内部已经发送了反馈消息,这里返回 null 避免重复发送
|
|
2575
2615
|
return null;
|
|
2576
2616
|
}
|
|
@@ -2728,7 +2768,15 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2728
2768
|
}
|
|
2729
2769
|
// 发送文件
|
|
2730
2770
|
try {
|
|
2731
|
-
const replyCtx =
|
|
2771
|
+
const replyCtx = isCrossChannel
|
|
2772
|
+
? { delivery: { chatType: 'private' } }
|
|
2773
|
+
: sendSession
|
|
2774
|
+
? this.getReplyContext(sendSession)
|
|
2775
|
+
: chatType === 'group'
|
|
2776
|
+
? { delivery: { chatType: 'group', groupId: channelId } }
|
|
2777
|
+
: chatType === 'private'
|
|
2778
|
+
? { delivery: { chatType: 'private' } }
|
|
2779
|
+
: undefined;
|
|
2732
2780
|
await targetAdapter.send(buildEnvelope({ channel: targetAdapter.channelName, channelId: targetChannelId, replyContext: replyCtx }), { kind: 'result.file', filePath: realPath });
|
|
2733
2781
|
const sizeStr = stat.size < 1024 ? `${stat.size} B`
|
|
2734
2782
|
: stat.size < 1024 * 1024 ? `${(stat.size / 1024).toFixed(1)} KB`
|
|
@@ -313,6 +313,10 @@ const CATALOG = [
|
|
|
313
313
|
{ path: 'sessionId', type: 'string' },
|
|
314
314
|
{ path: 'toolName', type: 'string' },
|
|
315
315
|
{ path: 'input', type: 'object' },
|
|
316
|
+
{ path: 'callId', type: 'string', optional: true },
|
|
317
|
+
{ path: 'correlationId', type: 'string', optional: true },
|
|
318
|
+
{ path: 'agentAid', type: 'string', optional: true },
|
|
319
|
+
{ path: 'permissionMode', type: 'string', optional: true },
|
|
316
320
|
{ path: 'timestamp', type: 'number', optional: true },
|
|
317
321
|
],
|
|
318
322
|
},
|
|
@@ -326,6 +330,10 @@ const CATALOG = [
|
|
|
326
330
|
{ path: 'toolName', type: 'string' },
|
|
327
331
|
{ path: 'isError', type: 'boolean', optional: true },
|
|
328
332
|
{ path: 'agentName', type: 'string', optional: true },
|
|
333
|
+
{ path: 'callId', type: 'string', optional: true },
|
|
334
|
+
{ path: 'correlationId', type: 'string', optional: true },
|
|
335
|
+
{ path: 'agentAid', type: 'string', optional: true },
|
|
336
|
+
{ path: 'permissionMode', type: 'string', optional: true },
|
|
329
337
|
{ path: 'timestamp', type: 'number', optional: true },
|
|
330
338
|
],
|
|
331
339
|
},
|
|
@@ -6,6 +6,7 @@ import { HandoffStore } from './store.js';
|
|
|
6
6
|
import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
|
|
7
7
|
import { recordCausationSpan } from '../causation/audit.js';
|
|
8
8
|
import { logger } from '../../utils/logger.js';
|
|
9
|
+
import { replyContextFromSession } from '../message/message-utils.js';
|
|
9
10
|
const HANDOFF_STALE_WARN_MS = 72 * 60 * 60 * 1000;
|
|
10
11
|
const HANDOFF_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
11
12
|
const HANDOFF_LARGE_AGGREGATE_COUNT = 10;
|
|
@@ -79,7 +80,12 @@ export class HandoffRuntime {
|
|
|
79
80
|
if (!origin || origin.selfAID !== input.selfAid)
|
|
80
81
|
throw new Error('origin session not found');
|
|
81
82
|
const targetChannelKey = formatChannelKey({ type: 'aun', selfAID: input.selfAid, name: 'main' });
|
|
82
|
-
|
|
83
|
+
if (!input.targetChatType) {
|
|
84
|
+
throw Object.assign(new Error('AUN handoff requires an explicit target chat type'), {
|
|
85
|
+
code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const targetChatType = input.targetChatType;
|
|
83
89
|
const target = await this.sessionManager.getOrCreateSession(targetChannelKey, input.to, origin.projectPath, input.thread, {
|
|
84
90
|
channelKey: targetChannelKey,
|
|
85
91
|
peerId: input.to,
|
|
@@ -112,6 +118,9 @@ export class HandoffRuntime {
|
|
|
112
118
|
targetSessionId: target.id,
|
|
113
119
|
payload: input.payload,
|
|
114
120
|
encrypt: input.encrypt,
|
|
121
|
+
delivery: targetChatType === 'group'
|
|
122
|
+
? { chatType: 'group', groupId: input.to }
|
|
123
|
+
: { chatType: 'private' },
|
|
115
124
|
returnPolicy: input.explicitReturnPolicy ?? 'required',
|
|
116
125
|
originAuthorization: input.originAuthorization,
|
|
117
126
|
causation: handoffCausation,
|
|
@@ -209,6 +218,8 @@ export class HandoffRuntime {
|
|
|
209
218
|
messageId,
|
|
210
219
|
timestamp: Date.now(),
|
|
211
220
|
source: 'handoff',
|
|
221
|
+
groupId: origin.chatType === 'group' ? (origin.metadata?.groupId || origin.channelId) : undefined,
|
|
222
|
+
replyContext: replyContextFromSession(origin),
|
|
212
223
|
boundSessionId: origin.id,
|
|
213
224
|
causation: instance.return_causation
|
|
214
225
|
? deriveCausation(instance.return_causation)
|
|
@@ -115,7 +115,11 @@ export class HandoffStore {
|
|
|
115
115
|
origin_session_id: input.originSessionId,
|
|
116
116
|
origin_message_id: input.originMessageId,
|
|
117
117
|
target_session_id: input.targetSessionId,
|
|
118
|
-
request: {
|
|
118
|
+
request: {
|
|
119
|
+
payload: clonePayload(input.payload),
|
|
120
|
+
encrypt: input.encrypt,
|
|
121
|
+
...(input.delivery ? { delivery: input.delivery } : {}),
|
|
122
|
+
},
|
|
119
123
|
...(input.originAuthorization
|
|
120
124
|
? { origin_authorization: { ...input.originAuthorization } }
|
|
121
125
|
: {}),
|
|
@@ -228,6 +228,16 @@ export class InteractionRouter {
|
|
|
228
228
|
return undefined;
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
|
+
/** Bind an adapter response to its runtime-owned channel identity domain. */
|
|
232
|
+
export function registerAdapterInteractions(adapter, router) {
|
|
233
|
+
const channelKey = adapter.channelKey;
|
|
234
|
+
adapter.onInteraction?.(async (response) => {
|
|
235
|
+
return router.handleAsync({ ...response, channelKey });
|
|
236
|
+
});
|
|
237
|
+
adapter.onInteractionInvalidated?.((interactionId, reason) => {
|
|
238
|
+
return router.cancelFromChannel(interactionId, reason);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
231
241
|
/** 把 CommandCard 渲染为文本提示,channel 不支持卡片时使用 */
|
|
232
242
|
export function renderCommandCardAsText(card) {
|
|
233
243
|
const lines = [card.title];
|
|
@@ -59,7 +59,21 @@ export class IMRenderer {
|
|
|
59
59
|
/** 推入 AgentEvent,按 chatmode 投影 */
|
|
60
60
|
emit(event) {
|
|
61
61
|
try {
|
|
62
|
-
|
|
62
|
+
const correlatedEvent = event.type === 'tool_use' || event.type === 'tool_result'
|
|
63
|
+
? { ...event, correlationId: event.correlationId ?? event.callId }
|
|
64
|
+
: event;
|
|
65
|
+
logger.event({
|
|
66
|
+
source: 'runner',
|
|
67
|
+
taskId: this.opts.envelope.taskId,
|
|
68
|
+
sessionId: this.opts.envelope.sessionId,
|
|
69
|
+
channelId: this.opts.envelope.channelId,
|
|
70
|
+
agentAid: this.opts.agentAid,
|
|
71
|
+
agentName: this.opts.envelope.agentName,
|
|
72
|
+
...(correlatedEvent.type === 'tool_use' || correlatedEvent.type === 'tool_result'
|
|
73
|
+
? { correlationId: correlatedEvent.correlationId, toolName: correlatedEvent.name }
|
|
74
|
+
: {}),
|
|
75
|
+
event: correlatedEvent,
|
|
76
|
+
});
|
|
63
77
|
}
|
|
64
78
|
catch {
|
|
65
79
|
// logger.event 失败不影响业务
|
|
@@ -244,6 +258,25 @@ export class IMRenderer {
|
|
|
244
258
|
durationMs: opts.durationMs,
|
|
245
259
|
});
|
|
246
260
|
}
|
|
261
|
+
/** 发送不进入 activity/thought 历史的非终态 warning status。 */
|
|
262
|
+
async sendWarningStatus(message, subtype, details) {
|
|
263
|
+
const payload = {
|
|
264
|
+
kind: 'status.progress',
|
|
265
|
+
metadata: {
|
|
266
|
+
activityType: 'progress',
|
|
267
|
+
message,
|
|
268
|
+
severity: 'warn',
|
|
269
|
+
...(subtype !== undefined && { subtype }),
|
|
270
|
+
...(details?.missingItems !== undefined && { missingItems: details.missingItems }),
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
try {
|
|
274
|
+
await this.opts.send(payload);
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
logger.warn(`[IMRenderer] warning status send failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
247
280
|
/** 添加任务生命周期边界事件,复用 activity.batch 投影路径。 */
|
|
248
281
|
addLifecycle(phase, metadata, text) {
|
|
249
282
|
const item = {
|