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.
Files changed (64) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/bin/codex-managed-hook.mjs +3 -0
  3. package/bin/install-codex-managed-hooks.mjs +3 -1
  4. package/dist/agents/claude-runner.js +14 -0
  5. package/dist/agents/codex-app-server-client.js +31 -5
  6. package/dist/agents/codex-runner.js +926 -121
  7. package/dist/aun/outbox.js +7 -0
  8. package/dist/channels/aun.js +209 -35
  9. package/dist/cli/daemon-commands.js +29 -8
  10. package/dist/cli/task-context.js +4 -0
  11. package/dist/cli/trigger-command.js +13 -4
  12. package/dist/config/config-field-policy.js +3 -0
  13. package/dist/config/config-manager.js +32 -5
  14. package/dist/config/contact-book-store.js +25 -3
  15. package/dist/core/auth/agent-delegation.js +12 -0
  16. package/dist/core/auth/auth-gateway.js +8 -0
  17. package/dist/core/auth/authorization-audit.js +66 -6
  18. package/dist/core/bootstrap-messages.js +8 -0
  19. package/dist/core/bootstrap-service.js +93 -25
  20. package/dist/core/command/command-handler.js +21 -0
  21. package/dist/core/command/menu-handler.js +9 -0
  22. package/dist/core/command/menu-protocol.js +1 -1
  23. package/dist/core/command/slash-handler.js +41 -18
  24. package/dist/core/data-migration.js +11 -1
  25. package/dist/core/event-catalog.js +32 -0
  26. package/dist/core/handoff/runtime.js +23 -3
  27. package/dist/core/message/im-renderer.js +7 -3
  28. package/dist/core/message/message-bridge.js +60 -2
  29. package/dist/core/message/message-log.js +33 -0
  30. package/dist/core/message/message-queue.js +21 -0
  31. package/dist/core/message/response-engine.js +172 -41
  32. package/dist/core/permission/ec-command-parser.js +272 -70
  33. package/dist/core/permission/protected-paths.js +11 -10
  34. package/dist/core/permission/tool-error-code.js +12 -0
  35. package/dist/core/permission/tool-policy.js +46 -5
  36. package/dist/core/session/session-manager.js +30 -0
  37. package/dist/core/session/session-renew.js +18 -1
  38. package/dist/core/session/session-turn-coordinator.js +5 -1
  39. package/dist/index.js +64 -5
  40. package/dist/ipc.js +97 -17
  41. package/dist/paths.js +18 -0
  42. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  43. package/dist/stats/price-resolver.js +4 -0
  44. package/dist/trigger/feedback.js +14 -2
  45. package/dist/trigger/parser.js +10 -1
  46. package/dist/trigger/scheduler.js +20 -3
  47. package/dist/utils/logger.js +9 -4
  48. package/dist/utils/tool-summary.js +59 -0
  49. package/dist/utils/windows-shell-trust.js +201 -0
  50. package/kits/docs/evolcore/INDEX.md +2 -2
  51. package/kits/docs/evolcore/agent-create.md +146 -0
  52. package/kits/docs/evolcore/agent.md +6 -0
  53. package/kits/docs/evolcore/group-collaboration.md +251 -0
  54. package/kits/docs/evolcore/group-rules.md +1 -19
  55. package/kits/docs/evolcore/group.md +3 -1
  56. package/kits/docs/evolcore/trigger.md +6 -3
  57. package/kits/docs/prompt-loading-architecture.md +6 -0
  58. package/kits/eck_message_manifest.json +6 -6
  59. package/kits/schemas/_meta.json +3 -2
  60. package/kits/schemas/agent-config.schema.12.json +427 -0
  61. package/kits/templates/message-fragments/item.md +1 -1
  62. package/kits/templates/system-fragments/bootstrap.md +2 -1
  63. package/kits/templates/system-fragments/commands.md +2 -2
  64. 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: 'command.error', text: '❌ /renew 不接受参数,请直接使用 /renew' };
2146
+ return { kind: 'system.error', text: '❌ /renew 不接受参数,请直接使用 /renew', subtype: 'session.renew.invalid_arguments', recoverable: true };
2146
2147
  }
2147
2148
  if (!threadId) {
2148
- return { kind: 'command.error', text: '⚠️ /renew 仅支持话题会话' };
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: 'command.error', text: '⚠️ /renew 仅支持携带原生消息 ID 的普通消息' };
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: 'command.error', text: '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试' };
2164
+ return { kind: 'system.error', text: '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试', subtype: 'session.renew.state_unknown', recoverable: true };
2164
2165
  }
2165
2166
  if (!renewSession) {
2166
- return { kind: 'command.error', text: '❌ 找不到当前话题会话' };
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: 'command.error', text: '❌ /renew 缺少稳定的消息 ID,当前渠道不支持安全重试' };
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: 'command.error', text: '❌ 当前运行环境不支持安全 backend 轮换' };
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: 'command.error', text: '❌ 当前话题会话已不存在或归属已变化' };
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: 'command.error', text: '❌ 当前渠道无法安全记录该请求,未执行 backend 轮换' };
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: 'command.result', text: 'ℹ️ 该请求已接收过,不再重复执行' };
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: 'command.error',
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: 'command.error', text: '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试' };
2234
+ return { kind: 'system.error', text: '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试', subtype: 'session.renew.state_unknown', recoverable: true };
2232
2235
  }
2233
2236
  return {
2234
- kind: 'command.result',
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: 'command.error', text: `❌ backend 轮换失败:${error instanceof Error ? error.message : String(error)}` };
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: 'command.error', text: `❌ 会话名称 "${sessionName}" 已存在,请使用其他名称` };
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 { kind: 'command.result', text: `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 项目: ${this.getProjectName(projectPath)}\n 后端: ${backendBits}\n 之前的对话历史已保留,可通过 /s 查看${handoffDiscardWarning ? `\n${handoffDiscardWarning}` : ''}` };
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
- if (fs.existsSync(contactAudit)) {
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 cmdResult = await this.cmdHandler.handle(content, channel, channelId, (_cid, text, opts) => sendReply(text), userId, threadId, chatType, source, messageId, selfAID, authSubject?.identity, authSubject, replyContext);
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) {