evolcore 0.0.21 → 0.0.22
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 +43 -0
- package/bin/codex-managed-hook.mjs +3 -0
- package/bin/install-codex-managed-hooks.mjs +3 -1
- package/dist/agents/claude-runner.js +14 -0
- package/dist/agents/codex-app-server-client.js +31 -5
- package/dist/agents/codex-runner.js +926 -121
- package/dist/aun/outbox.js +7 -0
- package/dist/channels/aun.js +209 -35
- package/dist/cli/daemon-commands.js +29 -8
- package/dist/cli/task-context.js +4 -0
- package/dist/cli/trigger-command.js +13 -4
- package/dist/config/config-field-policy.js +3 -0
- package/dist/config/config-manager.js +32 -5
- package/dist/config/contact-book-store.js +25 -3
- package/dist/core/auth/agent-delegation.js +12 -0
- package/dist/core/auth/auth-gateway.js +8 -0
- package/dist/core/auth/authorization-audit.js +66 -6
- package/dist/core/bootstrap-messages.js +8 -0
- package/dist/core/bootstrap-service.js +93 -25
- package/dist/core/command/command-handler.js +21 -0
- package/dist/core/command/menu-handler.js +9 -0
- package/dist/core/command/menu-protocol.js +1 -1
- package/dist/core/command/slash-handler.js +41 -18
- package/dist/core/data-migration.js +11 -1
- package/dist/core/event-catalog.js +32 -0
- package/dist/core/handoff/runtime.js +23 -3
- package/dist/core/message/im-renderer.js +7 -3
- package/dist/core/message/message-bridge.js +60 -2
- package/dist/core/message/message-log.js +33 -0
- package/dist/core/message/message-queue.js +21 -0
- package/dist/core/message/response-engine.js +172 -41
- package/dist/core/permission/ec-command-parser.js +272 -70
- package/dist/core/permission/protected-paths.js +11 -10
- package/dist/core/permission/tool-error-code.js +12 -0
- package/dist/core/permission/tool-policy.js +46 -5
- package/dist/core/session/session-manager.js +30 -0
- package/dist/core/session/session-renew.js +18 -1
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/index.js +64 -5
- package/dist/ipc.js +97 -17
- package/dist/paths.js +18 -0
- package/dist/response-system/engines/v1/proactive-flow.js +7 -2
- package/dist/stats/price-resolver.js +4 -0
- package/dist/trigger/feedback.js +14 -2
- package/dist/trigger/parser.js +10 -1
- package/dist/trigger/scheduler.js +20 -3
- package/dist/utils/logger.js +9 -4
- package/dist/utils/tool-summary.js +59 -0
- package/dist/utils/windows-shell-trust.js +201 -0
- package/kits/docs/evolcore/INDEX.md +2 -2
- package/kits/docs/evolcore/agent-create.md +146 -0
- package/kits/docs/evolcore/agent.md +6 -0
- package/kits/docs/evolcore/group-collaboration.md +251 -0
- package/kits/docs/evolcore/group-rules.md +1 -19
- package/kits/docs/evolcore/group.md +3 -1
- package/kits/docs/evolcore/trigger.md +6 -3
- package/kits/docs/prompt-loading-architecture.md +6 -0
- package/kits/eck_message_manifest.json +6 -6
- package/kits/schemas/_meta.json +3 -2
- package/kits/schemas/agent-config.schema.12.json +427 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/system-fragments/bootstrap.md +2 -1
- package/kits/templates/system-fragments/commands.md +2 -2
- package/package.json +2 -2
|
@@ -26,6 +26,7 @@ import { normalizePermissionMode as normalizePermissionModeContract, PUBLIC_PERM
|
|
|
26
26
|
import { isManagementRole } from '../../config/builtin-roles.js';
|
|
27
27
|
import { isSystemControlChannel } from '../system-channels.js';
|
|
28
28
|
import { spawnDetachedNode } from '../../utils/cross-platform.js';
|
|
29
|
+
import { inspectDataMigrationRequirement } from '../data-migration.js';
|
|
29
30
|
import { guardIdleCommand, guardKnownCommand, guardRoleCommand, guardThreadCommand, isRecognizedSlashCommand, normalizeSlashContent, probeSessionActivity, } from './slash-gate.js';
|
|
30
31
|
const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
31
32
|
const PERMISSION_MODE_KEYS = PUBLIC_PERMISSION_MODES;
|
|
@@ -2142,15 +2143,15 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2142
2143
|
// 才会在新的 TurnLease 中懒创建并绑定新 backend。
|
|
2143
2144
|
if (normalizedContent === '/renew' || /^\/renew\s/.test(normalizedContent)) {
|
|
2144
2145
|
if (normalizedContent !== '/renew') {
|
|
2145
|
-
return { kind: '
|
|
2146
|
+
return { kind: 'system.error', text: '❌ /renew 不接受参数,请直接使用 /renew', subtype: 'session.renew.invalid_arguments', recoverable: true };
|
|
2146
2147
|
}
|
|
2147
2148
|
if (!threadId) {
|
|
2148
|
-
return { kind: '
|
|
2149
|
+
return { kind: 'system.error', text: '⚠️ /renew 仅支持话题会话', subtype: 'session.renew.unsupported', recoverable: true };
|
|
2149
2150
|
}
|
|
2150
2151
|
// Card callbacks use synthetic message IDs on several adapters. They are
|
|
2151
2152
|
// suitable for UI correlation, but not for the durable inbound claim.
|
|
2152
2153
|
if (source === 'card-trigger') {
|
|
2153
|
-
return { kind: '
|
|
2154
|
+
return { kind: 'system.error', text: '⚠️ /renew 仅支持携带原生消息 ID 的普通消息', subtype: 'session.renew.invalid_source', recoverable: true };
|
|
2154
2155
|
}
|
|
2155
2156
|
let renewSession;
|
|
2156
2157
|
try {
|
|
@@ -2160,19 +2161,19 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2160
2161
|
}
|
|
2161
2162
|
catch (error) {
|
|
2162
2163
|
logger.warn(`[CommandHandler] /renew topic lookup failed: thread=${threadId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2163
|
-
return { kind: '
|
|
2164
|
+
return { kind: 'system.error', text: '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试', subtype: 'session.renew.state_unknown', recoverable: true };
|
|
2164
2165
|
}
|
|
2165
2166
|
if (!renewSession) {
|
|
2166
|
-
return { kind: '
|
|
2167
|
+
return { kind: 'system.error', text: '❌ 找不到当前话题会话', subtype: 'session.renew.session_missing', recoverable: true };
|
|
2167
2168
|
}
|
|
2168
2169
|
const inboundMessageId = messageId?.trim();
|
|
2169
2170
|
if (!inboundMessageId) {
|
|
2170
|
-
return { kind: '
|
|
2171
|
+
return { kind: 'system.error', text: '❌ /renew 缺少稳定的消息 ID,当前渠道不支持安全重试', subtype: 'session.renew.message_id_missing', recoverable: true };
|
|
2171
2172
|
}
|
|
2172
2173
|
if (typeof this.sessionManager.rotateTopicBackend !== 'function'
|
|
2173
2174
|
|| typeof this.messageQueue.withSessionBarrier !== 'function'
|
|
2174
2175
|
|| typeof this.messageQueue.claimInbound !== 'function') {
|
|
2175
|
-
return { kind: '
|
|
2176
|
+
return { kind: 'system.error', text: '❌ 当前运行环境不支持安全 backend 轮换', subtype: 'session.renew.unavailable', recoverable: true };
|
|
2176
2177
|
}
|
|
2177
2178
|
try {
|
|
2178
2179
|
const outcome = await this.messageQueue.withSessionBarrier(renewSession.id, async (snapshot) => {
|
|
@@ -2211,33 +2212,36 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2211
2212
|
};
|
|
2212
2213
|
});
|
|
2213
2214
|
if (outcome.kind === 'missing') {
|
|
2214
|
-
return { kind: '
|
|
2215
|
+
return { kind: 'system.error', text: '❌ 当前话题会话已不存在或归属已变化', subtype: 'session.renew.session_changed', recoverable: true };
|
|
2215
2216
|
}
|
|
2216
2217
|
if (outcome.kind === 'claim_failed') {
|
|
2217
|
-
return { kind: '
|
|
2218
|
+
return { kind: 'system.error', text: '❌ 当前渠道无法安全记录该请求,未执行 backend 轮换', subtype: 'session.renew.claim_failed', recoverable: true };
|
|
2218
2219
|
}
|
|
2219
2220
|
if (outcome.kind === 'duplicate') {
|
|
2220
|
-
return { kind: '
|
|
2221
|
+
return { kind: 'system.notice', text: 'ℹ️ 该请求已接收过,不再重复执行', subtype: 'session.renew.duplicate' };
|
|
2221
2222
|
}
|
|
2222
2223
|
if (outcome.kind === 'busy' || outcome.kind === 'unknown') {
|
|
2223
2224
|
return {
|
|
2224
|
-
kind: '
|
|
2225
|
+
kind: 'system.error',
|
|
2225
2226
|
text: outcome.kind === 'busy'
|
|
2226
2227
|
? '⚠️ 当前会话忙碌,无法安全轮换,请稍后再试'
|
|
2227
2228
|
: '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试',
|
|
2229
|
+
subtype: outcome.kind === 'busy' ? 'session.renew.busy' : 'session.renew.state_unknown',
|
|
2230
|
+
recoverable: true,
|
|
2228
2231
|
};
|
|
2229
2232
|
}
|
|
2230
2233
|
if (outcome.kind !== 'rotated') {
|
|
2231
|
-
return { kind: '
|
|
2234
|
+
return { kind: 'system.error', text: '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试', subtype: 'session.renew.state_unknown', recoverable: true };
|
|
2232
2235
|
}
|
|
2233
2236
|
return {
|
|
2234
|
-
kind: '
|
|
2237
|
+
kind: 'system.notice',
|
|
2235
2238
|
text: `✅ backend 已切换${outcome.wasBound ? '' : '(当前未绑定 backend)'};下一条消息将初始化新 backend(阶段一不携带历史摘要)`,
|
|
2239
|
+
subtype: 'session.renewed',
|
|
2236
2240
|
};
|
|
2237
2241
|
}
|
|
2238
2242
|
catch (error) {
|
|
2239
2243
|
logger.error(`[CommandHandler] /renew failed: session=${renewSession.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2240
|
-
return { kind: '
|
|
2244
|
+
return { kind: 'system.error', text: `❌ backend 轮换失败:${error instanceof Error ? error.message : String(error)}`, subtype: 'session.renew.failed', recoverable: true };
|
|
2241
2245
|
}
|
|
2242
2246
|
}
|
|
2243
2247
|
// /compact 命令:手动压缩会话上下文
|
|
@@ -2451,13 +2455,17 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2451
2455
|
if (sessionName) {
|
|
2452
2456
|
const existing = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
|
|
2453
2457
|
if (existing) {
|
|
2454
|
-
return { kind: '
|
|
2458
|
+
return { kind: 'system.error', text: `❌ 会话名称 "${sessionName}" 已存在,请使用其他名称`, subtype: 'session.new.name_conflict', recoverable: true };
|
|
2455
2459
|
}
|
|
2456
2460
|
}
|
|
2457
2461
|
await interruptPausedSessionBeforeReplacement(session || activeSession);
|
|
2458
2462
|
const projectPath = this.getEffectiveDefaultPath(channel);
|
|
2459
2463
|
if (sendMessage && session) {
|
|
2460
|
-
await sendMessage(channelId, `⏳ 正在创建新会话${sessionName ? `: ${sessionName}` : ''}...`, this.getReplyContext(session)
|
|
2464
|
+
await sendMessage(channelId, `⏳ 正在创建新会话${sessionName ? `: ${sessionName}` : ''}...`, this.getReplyContext(session), {
|
|
2465
|
+
kind: 'system.notice',
|
|
2466
|
+
text: `⏳ 正在创建新会话${sessionName ? `: ${sessionName}` : ''}...`,
|
|
2467
|
+
subtype: 'session.creating',
|
|
2468
|
+
});
|
|
2461
2469
|
}
|
|
2462
2470
|
const newSessionBaseagent = this.agentRegistry?.resolveByChannel(channel)?.baseagent || this.parseDefaultBaseagent();
|
|
2463
2471
|
const previousMetadata = session?.metadata || activeSession?.metadata || {};
|
|
@@ -2524,7 +2532,11 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2524
2532
|
?? newRunner.getEffort?.()
|
|
2525
2533
|
?? newAgent?.effort;
|
|
2526
2534
|
const backendBits = [newBaseagent, backendModel, backendEffort].filter(Boolean).join(' · ');
|
|
2527
|
-
return {
|
|
2535
|
+
return {
|
|
2536
|
+
kind: 'system.notice',
|
|
2537
|
+
text: `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 项目: ${this.getProjectName(projectPath)}\n 后端: ${backendBits}\n 之前的对话历史已保留,可通过 /s 查看${handoffDiscardWarning ? `\n${handoffDiscardWarning}` : ''}`,
|
|
2538
|
+
subtype: 'session.created',
|
|
2539
|
+
};
|
|
2528
2540
|
}
|
|
2529
2541
|
// /check 命令:检查 EvolAgent 实例健康(visitor/member 可用,详情仅 admin)
|
|
2530
2542
|
if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
|
|
@@ -2773,7 +2785,6 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2773
2785
|
const count = this.messageCache.getCount(s.id);
|
|
2774
2786
|
return `${s.projectPath} 有 ${count} 条新消息`;
|
|
2775
2787
|
});
|
|
2776
|
-
// 执行重启逻辑(共用于卡片回调和文本确认)
|
|
2777
2788
|
const executeRestart = async () => {
|
|
2778
2789
|
const suppressRealRestart = shouldSuppressRealRestart();
|
|
2779
2790
|
let restartReplyContext = replyContext;
|
|
@@ -2814,6 +2825,11 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2814
2825
|
logger.warn(`[System] Refusing restart without a valid explicit delivery route: channel=${channel} channelId=${channelId}`);
|
|
2815
2826
|
return false;
|
|
2816
2827
|
}
|
|
2828
|
+
const migrationRequirement = inspectDataMigrationRequirement(resolvePaths().root);
|
|
2829
|
+
if (migrationRequirement.required) {
|
|
2830
|
+
logger.warn(`[System] Refusing restart while data migration is pending: operations=${migrationRequirement.operationCount}`);
|
|
2831
|
+
return { code: 'DATA_MIGRATION_REQUIRED', operationCount: migrationRequirement.operationCount };
|
|
2832
|
+
}
|
|
2817
2833
|
const restartInfo = {
|
|
2818
2834
|
channel,
|
|
2819
2835
|
channelId,
|
|
@@ -2892,6 +2908,13 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2892
2908
|
}
|
|
2893
2909
|
}
|
|
2894
2910
|
const restarted = await executeRestart();
|
|
2911
|
+
if (typeof restarted === 'object' && restarted.code === 'DATA_MIGRATION_REQUIRED') {
|
|
2912
|
+
return {
|
|
2913
|
+
kind: 'command.error',
|
|
2914
|
+
text: `❌ 待处理的用户数据迁移(${restarted.operationCount} 项)阻止重启。请先运行 ec data migrate --dry-run,然后运行 ec data migrate --apply。`,
|
|
2915
|
+
reason: restarted.code,
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2895
2918
|
if (!restarted) {
|
|
2896
2919
|
return { kind: 'command.error', text: '❌ 无法确定重启通知的出站路由,请从可信会话上下文重试' };
|
|
2897
2920
|
}
|
|
@@ -768,7 +768,17 @@ export function planDataMigration(root) {
|
|
|
768
768
|
}
|
|
769
769
|
catch { }
|
|
770
770
|
const contactAudit = path.join(root, 'data', 'contact-book-audit.jsonl');
|
|
771
|
-
|
|
771
|
+
// The legacy audit file may be provisioned as an empty placeholder (for
|
|
772
|
+
// example by a protected-path sandbox). An empty source has nothing to
|
|
773
|
+
// partition and must not block daemon startup.
|
|
774
|
+
let hasContactAuditRecords = false;
|
|
775
|
+
try {
|
|
776
|
+
const stat = fs.statSync(contactAudit);
|
|
777
|
+
hasContactAuditRecords = stat.isFile()
|
|
778
|
+
&& fs.readFileSync(contactAudit, 'utf8').trim().length > 0;
|
|
779
|
+
}
|
|
780
|
+
catch { }
|
|
781
|
+
if (hasContactAuditRecords) {
|
|
772
782
|
const operationId = nextId('contact-audit');
|
|
773
783
|
operations.push({ id: operationId, kind: 'contact-audit-partition', source: contactAudit, staging: path.join(stagingDir, operationId), status: 'planned' });
|
|
774
784
|
}
|
|
@@ -246,6 +246,8 @@ const CATALOG = [
|
|
|
246
246
|
description: 'agent 任务开始',
|
|
247
247
|
fields: [
|
|
248
248
|
{ path: 'sessionId', type: 'string' },
|
|
249
|
+
{ path: 'taskId', type: 'string', optional: true },
|
|
250
|
+
{ path: 'generation', type: 'number', optional: true },
|
|
249
251
|
{ path: 'agentName', type: 'string', optional: true },
|
|
250
252
|
{ path: 'encrypt', type: 'boolean', optional: true },
|
|
251
253
|
{ path: 'chatmode', type: 'string', optional: true },
|
|
@@ -269,6 +271,8 @@ const CATALOG = [
|
|
|
269
271
|
description: 'agent 任务完成',
|
|
270
272
|
fields: [
|
|
271
273
|
{ path: 'sessionId', type: 'string' },
|
|
274
|
+
{ path: 'taskId', type: 'string', optional: true },
|
|
275
|
+
{ path: 'generation', type: 'number', optional: true },
|
|
272
276
|
{ path: 'channel', type: 'string' },
|
|
273
277
|
{ path: 'channelName', type: 'string', optional: true },
|
|
274
278
|
{ path: 'channelId', type: 'string' },
|
|
@@ -287,6 +291,8 @@ const CATALOG = [
|
|
|
287
291
|
description: 'agent 任务失败',
|
|
288
292
|
fields: [
|
|
289
293
|
{ path: 'sessionId', type: 'string' },
|
|
294
|
+
{ path: 'taskId', type: 'string', optional: true },
|
|
295
|
+
{ path: 'generation', type: 'number', optional: true },
|
|
290
296
|
{ path: 'error', type: 'string' },
|
|
291
297
|
{ path: 'errorType', type: 'string' },
|
|
292
298
|
{ path: 'terminalReason', type: 'string', optional: true },
|
|
@@ -300,6 +306,8 @@ const CATALOG = [
|
|
|
300
306
|
description: 'agent 任务被中断',
|
|
301
307
|
fields: [
|
|
302
308
|
{ path: 'sessionId', type: 'string' },
|
|
309
|
+
{ path: 'taskId', type: 'string', optional: true },
|
|
310
|
+
{ path: 'generation', type: 'number', optional: true },
|
|
303
311
|
{ path: 'reason', type: 'string', optional: true },
|
|
304
312
|
{ path: 'agentName', type: 'string', optional: true },
|
|
305
313
|
],
|
|
@@ -339,13 +347,17 @@ const CATALOG = [
|
|
|
339
347
|
{ path: 'eventPhase', type: 'string' },
|
|
340
348
|
{ path: 'isError', type: 'boolean', optional: true },
|
|
341
349
|
{ path: 'errorCode', type: 'string', optional: true },
|
|
350
|
+
{ path: 'classificationMissing', type: 'boolean', optional: true },
|
|
342
351
|
{ path: 'agentName', type: 'string', optional: true },
|
|
343
352
|
{ path: 'callId', type: 'string', optional: true },
|
|
344
353
|
{ path: 'correlationId', type: 'string', optional: true },
|
|
354
|
+
{ path: 'requestId', type: 'string', optional: true },
|
|
345
355
|
{ path: 'agentAid', type: 'string', optional: true },
|
|
346
356
|
{ path: 'permissionMode', type: 'string', optional: true },
|
|
347
357
|
{ path: 'decision', type: 'string' },
|
|
348
358
|
{ path: 'decisionSource', type: 'string' },
|
|
359
|
+
{ path: 'policyCode', type: 'string', optional: true },
|
|
360
|
+
{ path: 'reason', type: 'string', optional: true },
|
|
349
361
|
{ path: 'executed', type: 'boolean' },
|
|
350
362
|
{ path: 'executionState', type: 'string' },
|
|
351
363
|
{ path: 'timestamp', type: 'number', optional: true },
|
|
@@ -399,6 +411,21 @@ const CATALOG = [
|
|
|
399
411
|
description: 'runner 开始压缩',
|
|
400
412
|
fields: [{ path: 'sessionId', type: 'string' }],
|
|
401
413
|
},
|
|
414
|
+
{
|
|
415
|
+
type: 'runner:proactive-reminder',
|
|
416
|
+
namespace: 'runner',
|
|
417
|
+
name: 'proactive-reminder',
|
|
418
|
+
description: '主动模式提醒已生成并请求注入模型上下文',
|
|
419
|
+
fields: [
|
|
420
|
+
{ path: 'sessionId', type: 'string' },
|
|
421
|
+
{ path: 'kind', type: 'string' },
|
|
422
|
+
{ path: 'text', type: 'string' },
|
|
423
|
+
{ path: 'queueLength', type: 'number', optional: true },
|
|
424
|
+
{ path: 'toolCount', type: 'number', optional: true },
|
|
425
|
+
{ path: 'injection', type: 'string' },
|
|
426
|
+
{ path: 'timestamp', type: 'number', optional: true },
|
|
427
|
+
],
|
|
428
|
+
},
|
|
402
429
|
{
|
|
403
430
|
type: 'runner:compact-complete',
|
|
404
431
|
namespace: 'runner',
|
|
@@ -690,6 +717,11 @@ const CATALOG = [
|
|
|
690
717
|
{ path: 'runId', type: 'string' },
|
|
691
718
|
{ path: 'originTriggerId', type: 'string' },
|
|
692
719
|
{ path: 'reason', type: 'string' },
|
|
720
|
+
{ path: 'reasonCode', type: 'string', optional: true },
|
|
721
|
+
{ path: 'decisionSource', type: 'string', optional: true },
|
|
722
|
+
{ path: 'executionState', type: 'string', optional: true },
|
|
723
|
+
{ path: 'generation', type: 'number', optional: true },
|
|
724
|
+
{ path: 'currentGeneration', type: 'number', optional: true },
|
|
693
725
|
{ path: 'targetChannel', type: 'string' },
|
|
694
726
|
{ path: 'targetChannelId', type: 'string' },
|
|
695
727
|
{ path: 'fireTime', type: 'number', optional: true },
|
|
@@ -266,8 +266,8 @@ export class HandoffRuntime {
|
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
|
-
buildPromptItem(message) {
|
|
270
|
-
return this.buildPromptItems(message)[0] ?? null;
|
|
269
|
+
buildPromptItem(message, fallbackPeerRole) {
|
|
270
|
+
return this.buildPromptItems(message, fallbackPeerRole)[0] ?? null;
|
|
271
271
|
}
|
|
272
272
|
isDiscardedDelivery(message) {
|
|
273
273
|
const delivery = message.handoffDelivery;
|
|
@@ -277,7 +277,7 @@ export class HandoffRuntime {
|
|
|
277
277
|
const ids = delivery.handoffIds?.length ? delivery.handoffIds : [delivery.handoffId];
|
|
278
278
|
return ids.length > 0 && ids.every(handoffId => this.store.get(selfAid, handoffId)?.state === 'discarded');
|
|
279
279
|
}
|
|
280
|
-
buildPromptItems(message) {
|
|
280
|
+
buildPromptItems(message, fallbackPeerRole) {
|
|
281
281
|
const delivery = message.handoffDelivery;
|
|
282
282
|
const selfAid = message.selfAID;
|
|
283
283
|
if (!delivery || !selfAid)
|
|
@@ -310,8 +310,18 @@ export class HandoffRuntime {
|
|
|
310
310
|
peerId: message.peerId,
|
|
311
311
|
peerName: message.peerName,
|
|
312
312
|
peerType: message.peerType,
|
|
313
|
+
peerRole: message.batchRole ?? message.resolvedIdentity?.role ?? fallbackPeerRole,
|
|
314
|
+
sameDevice: message.sameDevice,
|
|
315
|
+
sameNetwork: message.sameNetwork,
|
|
316
|
+
sameEgressIp: message.sameEgressIp,
|
|
317
|
+
encrypted: message.encrypted,
|
|
313
318
|
content: message.content,
|
|
314
319
|
timestamp: message.timestamp,
|
|
320
|
+
receivedAt: message.receivedAt,
|
|
321
|
+
gatewaySeq: message.gatewaySeq,
|
|
322
|
+
images: message.images,
|
|
323
|
+
mentions: message.mentions,
|
|
324
|
+
mentionAids: message.mentionAids,
|
|
315
325
|
causation: message.causation,
|
|
316
326
|
handoff: {
|
|
317
327
|
kind: 'response_to_origin',
|
|
@@ -348,8 +358,18 @@ export class HandoffRuntime {
|
|
|
348
358
|
peerId: message.peerId,
|
|
349
359
|
peerName: message.peerName,
|
|
350
360
|
peerType: message.peerType,
|
|
361
|
+
peerRole: message.batchRole ?? message.resolvedIdentity?.role ?? fallbackPeerRole,
|
|
362
|
+
sameDevice: message.sameDevice,
|
|
363
|
+
sameNetwork: message.sameNetwork,
|
|
364
|
+
sameEgressIp: message.sameEgressIp,
|
|
365
|
+
encrypted: message.encrypted,
|
|
351
366
|
content: message.content,
|
|
352
367
|
timestamp: message.timestamp,
|
|
368
|
+
receivedAt: message.receivedAt,
|
|
369
|
+
gatewaySeq: message.gatewaySeq,
|
|
370
|
+
images: message.images,
|
|
371
|
+
mentions: message.mentions,
|
|
372
|
+
mentionAids: message.mentionAids,
|
|
353
373
|
causation: message.causation,
|
|
354
374
|
handoff: {
|
|
355
375
|
kind: 'request_to_target',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { logger } from '../../utils/logger.js';
|
|
2
|
-
import { summarizeToolInput } from '../../utils/tool-summary.js';
|
|
2
|
+
import { summarizeToolInput, toolInputForDisplay } from '../../utils/tool-summary.js';
|
|
3
3
|
import { getErrorMessage, isContextTooLongText } from '../../utils/error-utils.js';
|
|
4
4
|
import { DEFAULT_FLUSH_DELAY_MS } from '../../types.js';
|
|
5
5
|
export class IMRenderer {
|
|
@@ -34,6 +34,9 @@ export class IMRenderer {
|
|
|
34
34
|
const correlatedEvent = event.type === 'tool_use' || event.type === 'tool_result'
|
|
35
35
|
? { ...event, correlationId: event.correlationId ?? event.callId }
|
|
36
36
|
: event;
|
|
37
|
+
const permissionMode = typeof this.opts.permissionMode === 'function'
|
|
38
|
+
? this.opts.permissionMode()
|
|
39
|
+
: this.opts.permissionMode;
|
|
37
40
|
logger.event({
|
|
38
41
|
source: 'runner',
|
|
39
42
|
taskId: this.opts.envelope.taskId,
|
|
@@ -41,6 +44,7 @@ export class IMRenderer {
|
|
|
41
44
|
channelId: this.opts.envelope.channelId,
|
|
42
45
|
agentAid: this.opts.agentAid,
|
|
43
46
|
agentName: this.opts.envelope.agentName,
|
|
47
|
+
...(permissionMode ? { permissionMode } : {}),
|
|
44
48
|
...(correlatedEvent.type === 'tool_use' || correlatedEvent.type === 'tool_result'
|
|
45
49
|
? { correlationId: correlatedEvent.correlationId, toolName: correlatedEvent.name }
|
|
46
50
|
: {}),
|
|
@@ -184,7 +188,7 @@ export class IMRenderer {
|
|
|
184
188
|
kind: 'tool_call',
|
|
185
189
|
call_id: callId || this.synthCallId(),
|
|
186
190
|
name,
|
|
187
|
-
arguments: input,
|
|
191
|
+
arguments: toolInputForDisplay(name, input),
|
|
188
192
|
text: descText,
|
|
189
193
|
});
|
|
190
194
|
this.messageTimestamps.push(Date.now());
|
|
@@ -568,7 +572,7 @@ export class IMRenderer {
|
|
|
568
572
|
text: desc,
|
|
569
573
|
};
|
|
570
574
|
if (event.input !== undefined)
|
|
571
|
-
item.arguments = event.input;
|
|
575
|
+
item.arguments = toolInputForDisplay(event.name, event.input);
|
|
572
576
|
return item;
|
|
573
577
|
}
|
|
574
578
|
case 'tool_result':
|
|
@@ -101,6 +101,8 @@ export class MessageBridge {
|
|
|
101
101
|
processOwnersProvider;
|
|
102
102
|
debouncers = new Map();
|
|
103
103
|
defaultDebounce;
|
|
104
|
+
/** Serializes session selection/renewal with inbound log admission per route. */
|
|
105
|
+
admissionLocks = new Map();
|
|
104
106
|
agentRegistry;
|
|
105
107
|
bootstrapService;
|
|
106
108
|
menuDeduper = new MenuRequestDeduper();
|
|
@@ -724,6 +726,7 @@ export class MessageBridge {
|
|
|
724
726
|
register(channelName, onMessage, sendReply, adapter, channelType) {
|
|
725
727
|
const effectiveChannelType = channelType || channelName;
|
|
726
728
|
onMessage(async (msg) => {
|
|
729
|
+
let releaseAdmission;
|
|
727
730
|
let releaseSessionIngress;
|
|
728
731
|
const releaseIngress = () => {
|
|
729
732
|
releaseSessionIngress?.();
|
|
@@ -754,6 +757,12 @@ export class MessageBridge {
|
|
|
754
757
|
const conversationId = chatType === 'group' ? (msg.groupId || msg.channelId) : msg.peerId;
|
|
755
758
|
const selfAid = msg.selfAID || owningAgent?.aid || parsedChannelKey?.selfAID;
|
|
756
759
|
const resolvedChannelType = msg.channelType || parsedChannelKey?.type || effectiveChannelType;
|
|
760
|
+
const admissionRoute = [
|
|
761
|
+
resolvedChannelType,
|
|
762
|
+
selfAid || '',
|
|
763
|
+
msg.channelId,
|
|
764
|
+
msg.threadId || '',
|
|
765
|
+
].join('#');
|
|
757
766
|
const trustedHandoffEcho = msg.source === 'handoff'
|
|
758
767
|
&& resolvedChannelType === 'aun'
|
|
759
768
|
&& !!selfAid
|
|
@@ -1034,6 +1043,12 @@ export class MessageBridge {
|
|
|
1034
1043
|
&& !executionPermissionOverride
|
|
1035
1044
|
&& !parseFullAccessCommand(cmdContent).matched
|
|
1036
1045
|
&& this.cmdHandler.isCommand(cmdContent);
|
|
1046
|
+
if (isCmd && /^\/new(?:\s|$)/i.test(cmdContent)) {
|
|
1047
|
+
// /new changes the active main-session boundary. Serialize it with
|
|
1048
|
+
// ordinary session admission so an in-flight renew cannot append or
|
|
1049
|
+
// enqueue against a superseded session after /new commits.
|
|
1050
|
+
releaseAdmission = await this.acquireAdmission(admissionRoute);
|
|
1051
|
+
}
|
|
1037
1052
|
if (isCmd) {
|
|
1038
1053
|
logger.debug(`[MessageBridge] Command detected: "${cmdContent}", routing to handler`);
|
|
1039
1054
|
// 命令也要记录入方向 jsonl(不创建 session,直接用 chatDirPath 计算路径)
|
|
@@ -1115,6 +1130,11 @@ export class MessageBridge {
|
|
|
1115
1130
|
// 通道找不到归属时退回到 globalConfig(一般是测试场景)
|
|
1116
1131
|
const effectiveProjectPath = owningAgent?.projectPath
|
|
1117
1132
|
?? this.defaultProjectPath;
|
|
1133
|
+
// Session renewal and inbound history append form one admission
|
|
1134
|
+
// transaction. Without this per-route single-flight, a second message
|
|
1135
|
+
// can observe the just-renewed empty session before the first message
|
|
1136
|
+
// has been appended and create another renewed session.
|
|
1137
|
+
releaseAdmission = await this.acquireAdmission(admissionRoute);
|
|
1118
1138
|
const hadMainSession = msg.threadId ? true : this.sessionManager.hasMainSession(channelName, msg.channelId, msg.channelType || effectiveChannelType, msg.selfAID || selfAid);
|
|
1119
1139
|
let session = await this.sessionManager.getOrCreateSession(channelName, msg.channelId, effectiveProjectPath, msg.threadId, Object.keys(metadata).length ? metadata : undefined, this.extractTopicName(msg), msg.peerId, chatType, owningAgent?.baseagent, msg.selfAID, msg.channelType || effectiveChannelType, msg.peerType, identity);
|
|
1120
1140
|
if (session.threadId && typeof this.messageQueue.reserveSessionIngress === 'function') {
|
|
@@ -1244,6 +1264,12 @@ export class MessageBridge {
|
|
|
1244
1264
|
}
|
|
1245
1265
|
if (!handoffCandidate)
|
|
1246
1266
|
appendMessageLog(inboundEntry.chatDir, inboundEntry.entry);
|
|
1267
|
+
// The authoritative session choice is now paired with its first
|
|
1268
|
+
// conversation record. Release before waiting for queue completion so
|
|
1269
|
+
// later group messages can still exercise the normal interrupt/FIFO
|
|
1270
|
+
// policy instead of being serialized behind the whole model turn.
|
|
1271
|
+
releaseAdmission?.();
|
|
1272
|
+
releaseAdmission = undefined;
|
|
1247
1273
|
// 6. ACK + debounce/enqueue
|
|
1248
1274
|
// ACK 在到达时立即做(每条独立 ACK),不等合并
|
|
1249
1275
|
// Interrupt 模式(单聊)→ 入队前 debounce 合并
|
|
@@ -1305,10 +1331,28 @@ export class MessageBridge {
|
|
|
1305
1331
|
logger.error(`[MessageBridge] Error in onMessage handler for ${channelName}:`, error);
|
|
1306
1332
|
}
|
|
1307
1333
|
finally {
|
|
1334
|
+
releaseAdmission?.();
|
|
1308
1335
|
releaseIngress();
|
|
1309
1336
|
}
|
|
1310
1337
|
});
|
|
1311
1338
|
}
|
|
1339
|
+
async acquireAdmission(key) {
|
|
1340
|
+
const previous = this.admissionLocks.get(key) ?? Promise.resolve();
|
|
1341
|
+
let release;
|
|
1342
|
+
const current = new Promise(resolve => { release = resolve; });
|
|
1343
|
+
const tail = previous.then(() => current);
|
|
1344
|
+
this.admissionLocks.set(key, tail);
|
|
1345
|
+
await previous;
|
|
1346
|
+
let released = false;
|
|
1347
|
+
return () => {
|
|
1348
|
+
if (released)
|
|
1349
|
+
return;
|
|
1350
|
+
released = true;
|
|
1351
|
+
release();
|
|
1352
|
+
if (this.admissionLocks.get(key) === tail)
|
|
1353
|
+
this.admissionLocks.delete(key);
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1312
1356
|
// ── Menu Protocol ──
|
|
1313
1357
|
extractTopicName(msg) {
|
|
1314
1358
|
const raw = msg.topicName
|
|
@@ -1717,7 +1761,22 @@ export class MessageBridge {
|
|
|
1717
1761
|
if (!this.cmdHandler.isCommand(content))
|
|
1718
1762
|
return false;
|
|
1719
1763
|
logger.info(`[${channel}] ${channelId}: ${content}${source === 'card-trigger' ? ' [card]' : ''}`);
|
|
1720
|
-
const
|
|
1764
|
+
const adapter = this.processor.getChannelInfo?.(channel)?.adapter;
|
|
1765
|
+
const cmdResult = await this.cmdHandler.handle(content, channel, channelId, async (_cid, text, opts, updatePayload) => {
|
|
1766
|
+
if (updatePayload && adapter?.send) {
|
|
1767
|
+
const envelope = buildEnvelope({
|
|
1768
|
+
taskId: `cmd-update-${randomBytes(5).toString('hex')}`,
|
|
1769
|
+
channel,
|
|
1770
|
+
channelId,
|
|
1771
|
+
agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
|
|
1772
|
+
chatmode: 'interactive',
|
|
1773
|
+
replyContext: opts ?? replyContext,
|
|
1774
|
+
});
|
|
1775
|
+
await adapter.send(envelope, updatePayload);
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
await sendReply(text, opts, updatePayload);
|
|
1779
|
+
}, userId, threadId, chatType, source, messageId, selfAID, authSubject?.identity, authSubject, replyContext);
|
|
1721
1780
|
logger.debug(`[MessageBridge] handleCommand: result type=${typeof cmdResult}`);
|
|
1722
1781
|
if (cmdResult === undefined)
|
|
1723
1782
|
return false;
|
|
@@ -1735,7 +1794,6 @@ export class MessageBridge {
|
|
|
1735
1794
|
return true;
|
|
1736
1795
|
}
|
|
1737
1796
|
// 出站走 adapter.send 统一入口
|
|
1738
|
-
const adapter = this.processor.getChannelInfo?.(channel)?.adapter;
|
|
1739
1797
|
let responseSessionId;
|
|
1740
1798
|
try {
|
|
1741
1799
|
const responseSession = threadId
|
|
@@ -233,6 +233,39 @@ export function messageLogPath(chatDir) {
|
|
|
233
233
|
export function hasMessageLogOperation(chatDir, operationId) {
|
|
234
234
|
return findMessageLogOperationMessageId(chatDir, operationId) !== undefined;
|
|
235
235
|
}
|
|
236
|
+
/** Find the newest completed outbound operation whose ID starts with prefix. */
|
|
237
|
+
export function findLatestMessageLogOperation(chatDir, operationPrefix) {
|
|
238
|
+
const file = messageLogPath(chatDir);
|
|
239
|
+
if (!fs.existsSync(file))
|
|
240
|
+
return undefined;
|
|
241
|
+
let latest;
|
|
242
|
+
try {
|
|
243
|
+
for (const line of fs.readFileSync(file, 'utf-8').split('\n')) {
|
|
244
|
+
if (!line.trim())
|
|
245
|
+
continue;
|
|
246
|
+
try {
|
|
247
|
+
const entry = JSON.parse(line);
|
|
248
|
+
if (entry?.dir !== 'out'
|
|
249
|
+
|| typeof entry.operationId !== 'string'
|
|
250
|
+
|| (!entry.operationId.startsWith(operationPrefix))
|
|
251
|
+
|| typeof entry.msgId !== 'string'
|
|
252
|
+
|| !entry.msgId)
|
|
253
|
+
continue;
|
|
254
|
+
const ts = typeof entry.ts === 'number' && Number.isFinite(entry.ts) ? entry.ts : 0;
|
|
255
|
+
if (!latest || ts >= latest.ts) {
|
|
256
|
+
latest = { messageId: entry.msgId, operationId: entry.operationId, ts };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// skip malformed message log lines
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
return latest;
|
|
268
|
+
}
|
|
236
269
|
export function findMessageLogOperationMessageId(chatDir, operationId) {
|
|
237
270
|
const file = messageLogPath(chatDir);
|
|
238
271
|
if (!fs.existsSync(file))
|
|
@@ -88,6 +88,17 @@ export class MessageQueue {
|
|
|
88
88
|
this.markActiveTerminal(event.sessionId, event.reason || 'interrupted');
|
|
89
89
|
}
|
|
90
90
|
});
|
|
91
|
+
subscribe.call(eventBus, 'task:started', event => {
|
|
92
|
+
if (!('sessionId' in event) || !event.sessionId)
|
|
93
|
+
return;
|
|
94
|
+
const taskEvent = event;
|
|
95
|
+
for (const [queueKey, state] of this.activeStates) {
|
|
96
|
+
if (this.matchesSession(queueKey, taskEvent.sessionId)) {
|
|
97
|
+
state.taskId = taskEvent.taskId;
|
|
98
|
+
state.generation = taskEvent.generation;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
91
102
|
subscribe.call(eventBus, 'task:completed', event => {
|
|
92
103
|
if ('sessionId' in event && event.sessionId)
|
|
93
104
|
this.markActiveTerminal(event.sessionId, 'completed');
|
|
@@ -623,6 +634,8 @@ export class MessageQueue {
|
|
|
623
634
|
sessionId: sessionKey,
|
|
624
635
|
reason: 'daemon_restart',
|
|
625
636
|
agentName,
|
|
637
|
+
taskId: this.activeStates.get(queueKey)?.taskId,
|
|
638
|
+
generation: this.activeStates.get(queueKey)?.generation,
|
|
626
639
|
causation: item.message.causation,
|
|
627
640
|
});
|
|
628
641
|
barriers.push(this.triggerInterrupt(queueKey, sessionKey, this.activeStates.get(queueKey)?.baseagent, agentName, 'daemon_restart'));
|
|
@@ -1022,6 +1035,8 @@ export class MessageQueue {
|
|
|
1022
1035
|
sessionId: sessionKey,
|
|
1023
1036
|
reason: 'new_message',
|
|
1024
1037
|
agentName: this.processingAgent.get(queueKey),
|
|
1038
|
+
taskId: this.activeStates.get(queueKey)?.taskId,
|
|
1039
|
+
generation: this.activeStates.get(queueKey)?.generation,
|
|
1025
1040
|
causation: this.activeBatches.get(queueKey)?.message.causation,
|
|
1026
1041
|
});
|
|
1027
1042
|
if (this.interruptCallback) {
|
|
@@ -1518,6 +1533,8 @@ export class MessageQueue {
|
|
|
1518
1533
|
sessionId: sessionKey,
|
|
1519
1534
|
reason: 'recalled',
|
|
1520
1535
|
agentName,
|
|
1536
|
+
taskId: activeState?.taskId,
|
|
1537
|
+
generation: activeState?.generation,
|
|
1521
1538
|
causation: active?.message.causation,
|
|
1522
1539
|
});
|
|
1523
1540
|
const barrier = this.interruptCallback
|
|
@@ -1766,6 +1783,8 @@ export class MessageQueue {
|
|
|
1766
1783
|
sessionId: sessionKey,
|
|
1767
1784
|
reason: 'stop',
|
|
1768
1785
|
agentName: name,
|
|
1786
|
+
taskId: this.activeStates.get(queueKey)?.taskId,
|
|
1787
|
+
generation: this.activeStates.get(queueKey)?.generation,
|
|
1769
1788
|
causation: this.activeBatches.get(queueKey)?.message.causation,
|
|
1770
1789
|
});
|
|
1771
1790
|
if (this.interruptCallback) {
|
|
@@ -2038,6 +2057,8 @@ export class MessageQueue {
|
|
|
2038
2057
|
sessionId: sessionKey,
|
|
2039
2058
|
reason: 'new_message',
|
|
2040
2059
|
agentName: targetAgentName,
|
|
2060
|
+
taskId: this.activeStates.get(targetQueueKey)?.taskId,
|
|
2061
|
+
generation: this.activeStates.get(targetQueueKey)?.generation,
|
|
2041
2062
|
causation: this.activeBatches.get(targetQueueKey)?.message.causation,
|
|
2042
2063
|
});
|
|
2043
2064
|
if (this.interruptCallback) {
|