evolcore 0.0.19 → 0.0.21

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 (136) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +60 -9
  3. package/bin/install-codex-managed-hooks.mjs +61 -0
  4. package/dist/agents/baseagent.js +10 -6
  5. package/dist/agents/claude-runner.js +383 -111
  6. package/dist/agents/codex-app-server-client.js +10 -2
  7. package/dist/agents/codex-runner.js +405 -137
  8. package/dist/agents/ecagent-runner.js +174 -63
  9. package/dist/agents/gemini-runner.js +130 -30
  10. package/dist/agents/request-identity.js +25 -0
  11. package/dist/agents/runner-types.js +19 -0
  12. package/dist/aun/aid/agentmd.js +66 -2
  13. package/dist/aun/aid/identity.js +4 -1
  14. package/dist/aun/aid/index.js +1 -1
  15. package/dist/aun/msg/group.js +86 -9
  16. package/dist/aun/msg/history.js +213 -36
  17. package/dist/aun/msg/managed-operation.js +58 -9
  18. package/dist/aun/msg/p2p.js +26 -11
  19. package/dist/aun/outbox.js +295 -68
  20. package/dist/aun/service-proxy.js +43 -25
  21. package/dist/channels/aun.js +1004 -273
  22. package/dist/channels/daemon.js +6 -1
  23. package/dist/cli/agent-command.js +4 -3
  24. package/dist/cli/agent.js +66 -56
  25. package/dist/cli/aun-commands.js +177 -42
  26. package/dist/cli/command-log.js +10 -11
  27. package/dist/cli/contact.js +1 -0
  28. package/dist/cli/daemon-commands.js +81 -121
  29. package/dist/cli/index.js +1 -0
  30. package/dist/cli/init.js +76 -24
  31. package/dist/cli/restart-monitor.js +3 -3
  32. package/dist/cli/task-context.js +46 -0
  33. package/dist/cli/trigger-command.js +1 -1
  34. package/dist/cli/watch-logs.js +10 -3
  35. package/dist/config/builtin-roles.js +1 -0
  36. package/dist/config/config-field-policy.js +16 -5
  37. package/dist/config/config-manager.js +226 -24
  38. package/dist/config/config-operation-service.js +1 -2
  39. package/dist/config/contact-operation-service.js +32 -1
  40. package/dist/config/contact-request-service.js +44 -0
  41. package/dist/config/daemon-services.js +186 -0
  42. package/dist/config/gateway-config.js +29 -16
  43. package/dist/config/lifecycle.js +16 -5
  44. package/dist/config/role-service.js +54 -3
  45. package/dist/config/schema-migration.js +550 -0
  46. package/dist/config-store.js +161 -12
  47. package/dist/core/agent-application-service.js +279 -0
  48. package/dist/core/audit/log-integrity.js +102 -0
  49. package/dist/core/auth/agent-delegation.js +31 -1
  50. package/dist/core/auth/auth-gateway.js +33 -4
  51. package/dist/core/auth/authorization-audit.js +155 -4
  52. package/dist/core/auth/operation-authorizer.js +41 -1
  53. package/dist/core/auth/operation-catalog.js +9 -1
  54. package/dist/core/bootstrap-messages.js +2 -2
  55. package/dist/core/bootstrap-service.js +27 -38
  56. package/dist/core/causation/aun-association.js +7 -4
  57. package/dist/core/channel-loader.js +0 -2
  58. package/dist/core/command/agent-control.js +56 -16
  59. package/dist/core/command/command-handler.js +290 -44
  60. package/dist/core/command/connect-menu.js +3 -4
  61. package/dist/core/command/group-menu.js +5 -7
  62. package/dist/core/command/menu-handler.js +279 -80
  63. package/dist/core/command/role-menu.js +21 -11
  64. package/dist/core/command/slash-gate.js +85 -18
  65. package/dist/core/command/slash-handler.js +350 -32
  66. package/dist/core/event-catalog.js +5 -0
  67. package/dist/core/evolagent.js +9 -4
  68. package/dist/core/handoff/dispatcher.js +4 -0
  69. package/dist/core/handoff/runtime.js +10 -0
  70. package/dist/core/handoff/store.js +32 -9
  71. package/dist/core/inference/text-inference.js +7 -15
  72. package/dist/core/message/im-renderer.js +83 -84
  73. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  74. package/dist/core/message/message-bridge.js +124 -10
  75. package/dist/core/message/message-log.js +14 -7
  76. package/dist/core/message/message-queue.js +206 -16
  77. package/dist/core/message/message-utils.js +12 -5
  78. package/dist/core/message/response-engine.js +495 -72
  79. package/dist/core/message/send-receipt.js +1 -0
  80. package/dist/core/message/stream-debouncer.js +9 -2
  81. package/dist/core/model/model-catalog.js +23 -15
  82. package/dist/core/model/model-diagnostics.js +28 -10
  83. package/dist/core/permission/approval-gateway.js +180 -6
  84. package/dist/core/permission/ec-command-parser.js +465 -56
  85. package/dist/core/permission/mode.js +18 -3
  86. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  87. package/dist/core/permission/readonly-shell-query.js +263 -9
  88. package/dist/core/permission/sandbox-runtime.js +205 -13
  89. package/dist/core/permission/tool-policy.js +673 -62
  90. package/dist/core/relation/peer-identity.js +18 -0
  91. package/dist/core/session/session-fs-store.js +154 -5
  92. package/dist/core/session/session-manager.js +299 -30
  93. package/dist/core/session/session-renew.js +19 -12
  94. package/dist/core/session/session-turn-coordinator.js +11 -4
  95. package/dist/eck/kit-renderer.js +18 -9
  96. package/dist/index.js +283 -65
  97. package/dist/ipc.js +374 -24
  98. package/dist/paths.js +64 -7
  99. package/dist/response-system/context-builder.js +1 -7
  100. package/dist/trigger/anomaly-store.js +1 -0
  101. package/dist/trigger/feedback.js +56 -5
  102. package/dist/trigger/history.js +79 -4
  103. package/dist/trigger/legacy-session-history.js +2 -2
  104. package/dist/trigger/parser.js +3 -2
  105. package/dist/trigger/validation.js +6 -1
  106. package/dist/utils/atomic-write.js +27 -0
  107. package/dist/utils/ecweb-utils.js +16 -2
  108. package/dist/utils/error-utils.js +4 -1
  109. package/dist/utils/logger.js +21 -2
  110. package/dist/utils/process-tree-stats.js +24 -4
  111. package/dist/utils/process-tree-worker.js +31 -0
  112. package/dist/utils/project-path.js +1 -2
  113. package/dist/utils/stats.js +52 -18
  114. package/dist/utils/welcome.js +2 -2
  115. package/kits/docs/INDEX.md +1 -1
  116. package/kits/docs/evolcore/INDEX.md +1 -1
  117. package/kits/docs/evolcore/contact.md +7 -1
  118. package/kits/docs/evolcore/msg.md +16 -0
  119. package/kits/rules/01-overview.md +1 -1
  120. package/kits/rules/03-identity.md +1 -1
  121. package/kits/rules/05-venue.md +1 -1
  122. package/kits/schemas/_meta.json +10 -5
  123. package/kits/schemas/agent-config.schema.10.json +2 -1
  124. package/kits/schemas/agent-config.schema.11.json +421 -0
  125. package/kits/schemas/daemon.schema.5.json +135 -0
  126. package/kits/schemas/daemon.schema.6.json +131 -0
  127. package/kits/schemas/defaults.schema.5.json +119 -0
  128. package/kits/schemas/migrations/README.md +3 -1
  129. package/kits/schemas/relation-config.schema.8.json +13 -0
  130. package/kits/schemas/role-config.schema.1.json +1 -2
  131. package/kits/schemas/single-session.schema.3.json +32 -0
  132. package/kits/templates/roles/admin.json +1 -0
  133. package/kits/templates/roles/member.json +1 -0
  134. package/kits/templates/roles/visitor.json +1 -0
  135. package/package.json +7 -3
  136. package/skills/eclink/SKILL.md +2 -0
@@ -5,7 +5,7 @@ import { resolveAnthropicConfig } from './baseagent.js';
5
5
  import { buildModelRequestHeaders } from './request-identity.js';
6
6
  import { DEFAULT_PERMISSION_MODE } from '../types.js';
7
7
  import { renderActionAsText } from '../core/interaction-router.js';
8
- import { buildEnvelope, sendInteractionPayload } from '../core/message/message-utils.js';
8
+ import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from '../core/message/message-utils.js';
9
9
  import path from 'path';
10
10
  import fs from 'fs';
11
11
  import os from 'os';
@@ -21,12 +21,13 @@ import { getPackageRoot, resolvePaths } from '../paths.js';
21
21
  import { resolveEffective } from '../config/config-manager.js';
22
22
  import { sanitizeSessionTitle } from '../core/session/session-title.js';
23
23
  import { resolveClaudeCapabilityRunOptionsForProject } from '../core/capability/capability-manager.js';
24
- import { normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
25
- import { buildClaudeProtectedFilesystem, containsHClassReference, containsLClassReference, isHClassPath, isLClassPath, isSameOrDescendant, resolveProtectedCandidate, } from '../core/protected-paths.js';
26
- import { buildHClassGuardCommand, createSandboxInitializationError, getActiveSandboxInitializationFailure, isSandboxInitializationFailure, recordSandboxInitializationFailure, SANDBOX_INITIALIZATION_COOLDOWN_MS, SANDBOX_INITIALIZATION_FAILED, ensureClaudeGitWorktreeConfig, prependExecutableDirectory, resolveBubblewrapPath, shouldFailIfClaudeSandboxUnavailable, shouldLogSandboxCircuitNotice, } from '../core/permission/sandbox-runtime.js';
24
+ import { normalizeExecutionPermissionMode, normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
25
+ import { buildClaudeProtectedFilesystem, containsHClassReference, containsLClassReference, isHClassPath, isLClassPath, isSameOrDescendant, resolveProtectedCandidate, } from '../core/permission/protected-paths.js';
26
+ import { buildHClassGuardCommand, collectSandboxGuardDiagnostics, createSandboxInitializationError, getActiveSandboxInitializationFailure, isSandboxInitializationFailure, recordSandboxInitializationFailure, SANDBOX_INITIALIZATION_COOLDOWN_MS, SANDBOX_INITIALIZATION_FAILED, ensureClaudeGitWorktreeConfig, prependExecutableDirectory, resolveBubblewrapPath, summarizeBubblewrapArgsForLog, summarizeSandboxGuardDiagnostics, shouldFailIfClaudeSandboxUnavailable, shouldLogSandboxCircuitNotice, } from '../core/permission/sandbox-runtime.js';
27
27
  import { buildClaudeUnixSocketAllowlist } from '../core/permission/unix-socket-policy.js';
28
28
  import { auditToolInfrastructureFailure, auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
29
- import { isManagedSessionRuntimeDir } from '../cli/task-context.js';
29
+ import { getManagedTaskTempDir, isManagedSessionRuntimeDir } from '../cli/task-context.js';
30
+ import { hasTrustedFullAccessContext } from './runner-types.js';
30
31
  import { contextTokensForUsage, usageForContext, isClaudeContextUsageModel, isOneMillionContextModel, realContextWindowForModel, autoCompactWindowForModel } from './runner-types.js';
31
32
  export { hasCompact, hasModelSwitcher, hasPermissionController } from './runner-types.js';
32
33
  // Built-in tools execute inside the Claude runtime and are covered by the
@@ -65,10 +66,18 @@ const CLAUDE_BUILTIN_TOOLS = new Set([
65
66
  'Write',
66
67
  ]);
67
68
  function spawnClaudeWithHClassGuard(options, root, onStderr) {
69
+ const preflight = collectSandboxGuardDiagnostics(root);
70
+ logger.info(`[ClaudeSandbox] H-class guard preflight ${JSON.stringify(summarizeSandboxGuardDiagnostics(preflight))}`);
68
71
  const guarded = buildHClassGuardCommand(options.command, options.args, root);
69
72
  if (!guarded) {
70
73
  throw new Error('Claude owner bypass 缺少 EvolCore H 类路径隔离运行时,已拒绝启动');
71
74
  }
75
+ const launch = summarizeBubblewrapArgsForLog(guarded.args);
76
+ const postBuild = collectSandboxGuardDiagnostics(root);
77
+ logger.info(`[ClaudeSandbox] H-class guard launch ${JSON.stringify({
78
+ ...launch,
79
+ diagnostics: summarizeSandboxGuardDiagnostics(postBuild),
80
+ })}`);
72
81
  const child = spawn(guarded.command, guarded.args, {
73
82
  cwd: options.cwd,
74
83
  env: options.env,
@@ -111,23 +120,6 @@ function stableClaudePermissionValue(value, depth = 0) {
111
120
  .sort()
112
121
  .map(key => [key, stableClaudePermissionValue(record[key], depth + 1)]));
113
122
  }
114
- function collectClaudePermissionStrings(value, output = []) {
115
- if (typeof value === 'string') {
116
- output.push(value);
117
- return output;
118
- }
119
- if (Array.isArray(value)) {
120
- for (const entry of value)
121
- collectClaudePermissionStrings(entry, output);
122
- return output;
123
- }
124
- if (!value || typeof value !== 'object')
125
- return output;
126
- for (const entry of Object.values(value)) {
127
- collectClaudePermissionStrings(entry, output);
128
- }
129
- return output;
130
- }
131
123
  function inspectClaudePermissionExpansion(input, options, projectPath) {
132
124
  const blockedPath = typeof options.blockedPath === 'string' && options.blockedPath.length > 0
133
125
  ? options.blockedPath
@@ -161,9 +153,6 @@ function inspectClaudePermissionExpansion(input, options, projectPath) {
161
153
  break;
162
154
  }
163
155
  }
164
- if (!protectedReason && collectClaudePermissionStrings(rawSuggestions).some(containsHClassReference)) {
165
- protectedReason = '🔒 Claude SDK 权限规则涉及 EvolCore H 类受保护路径,已拒绝';
166
- }
167
156
  const hasDirectoryExpansion = rawSuggestions.some(entry => {
168
157
  if (!entry || typeof entry !== 'object' || Array.isArray(entry))
169
158
  return false;
@@ -600,7 +589,7 @@ class MessageStream {
600
589
  }
601
590
  export class AgentRunner {
602
591
  name = 'claude';
603
- capabilities = { clear: true, compact: true, fork: true, forkAtTurn: true, askUserQuestion: true, planApproval: true, fileRewind: 'checkpoint' };
592
+ capabilities = { fullaccess: true, clear: true, compact: true, fork: true, forkAtTurn: true, askUserQuestion: true, planApproval: true, fileRewind: 'checkpoint' };
604
593
  apiKey;
605
594
  model;
606
595
  effort;
@@ -609,6 +598,8 @@ export class AgentRunner {
609
598
  customHeaders;
610
599
  customQueryParams;
611
600
  config;
601
+ agentAid;
602
+ agentConfig;
612
603
  activeSessions = new Map();
613
604
  activeStreams = new Map();
614
605
  activeMessageStreams = new Map();
@@ -636,6 +627,7 @@ export class AgentRunner {
636
627
  reason: detail.message.slice(0, 512),
637
628
  sessionId,
638
629
  agentAid: context?.selfAid,
630
+ agentName: context?.agentName,
639
631
  permissionMode,
640
632
  role: context?.role,
641
633
  callId: inputId,
@@ -654,6 +646,7 @@ export class AgentRunner {
654
646
  policyCode: SANDBOX_INITIALIZATION_FAILED,
655
647
  decisionSource: 'infrastructure',
656
648
  agentAid: context?.selfAid,
649
+ agentName: context?.agentName,
657
650
  permissionMode: permissionMode,
658
651
  summary: detail.message.slice(0, 512),
659
652
  effect: 'operation_skipped',
@@ -687,12 +680,17 @@ export class AgentRunner {
687
680
  this.cleanupSandboxSettingsFile(sessionId, filePath);
688
681
  }
689
682
  }
690
- constructor(apiKey, model, onSessionIdUpdate, baseUrl, config) {
683
+ constructor(apiKey, model, onSessionIdUpdate, baseUrl, config, runtime) {
684
+ // Compatibility for legacy direct callers that still pass the removed
685
+ // internal fields in a synthetic config. AgentLoader uses runtime binding.
686
+ const legacy = config?.agents?.claude;
691
687
  this.apiKey = apiKey;
692
688
  this.model = model || 'sonnet';
693
689
  this.effort = undefined;
694
690
  this.baseUrl = baseUrl;
695
691
  this.config = config;
692
+ this.agentAid = runtime?.agentAid ?? legacy?.['evolcoreAgentAid'];
693
+ this.agentConfig = runtime?.agentConfig ?? legacy?.['evolcoreAgentConfig'];
696
694
  this.onSessionIdUpdate = onSessionIdUpdate;
697
695
  if (config) {
698
696
  const anthropic = resolveAnthropicConfig(config);
@@ -700,7 +698,7 @@ export class AgentRunner {
700
698
  this.customHeaders = buildModelRequestHeaders({
701
699
  baseagent: 'claude',
702
700
  baseUrl: anthropic.baseUrl,
703
- agentAid: config.agents?.claude?.evolcoreAgentAid,
701
+ agentAid: this.agentAid,
704
702
  configuredHeaders: anthropic.headers,
705
703
  });
706
704
  this.customQueryParams = anthropic.queryParams;
@@ -744,11 +742,10 @@ export class AgentRunner {
744
742
  return entry?.cache;
745
743
  }
746
744
  async resolveCapabilityRunOptions(projectPath) {
747
- const claudeConfig = this.config?.agents?.claude;
748
- let agentConfig = claudeConfig?.evolcoreAgentConfig;
749
- if (claudeConfig?.evolcoreAgentAid) {
745
+ let agentConfig = this.agentConfig;
746
+ if (this.agentAid) {
750
747
  try {
751
- agentConfig = resolveEffective({ self: claudeConfig.evolcoreAgentAid }, { cache: true, expand: true });
748
+ agentConfig = resolveEffective({ self: this.agentAid }, { cache: true, expand: true });
752
749
  }
753
750
  catch { }
754
751
  }
@@ -826,14 +823,16 @@ export class AgentRunner {
826
823
  this.permissionContexts.set(sessionId, context);
827
824
  }
828
825
  toSdkPermissionMode(mode) {
826
+ if (mode === 'fullaccess')
827
+ return 'bypassPermissions';
829
828
  const normalized = normalizePermissionMode(mode ?? this.permissionMode);
830
829
  // Public permission policy is enforced by PreToolUse. Keeping the SDK in
831
830
  // default mode preserves canUseTool reachability for request mode.
832
831
  return normalized.workflow === 'plan' ? 'plan' : 'default';
833
832
  }
834
833
  // ── Compactable 接口 ──
835
- async compact(sessionId, agentSessionId, projectPath) {
836
- return this.compactSession(sessionId, agentSessionId, projectPath);
834
+ async compact(sessionId, agentSessionId, projectPath, modelOverride) {
835
+ return this.compactSession(sessionId, agentSessionId, projectPath, modelOverride);
837
836
  }
838
837
  syncFromUserSettings() {
839
838
  try {
@@ -972,7 +971,7 @@ export class AgentRunner {
972
971
  : '回复 /ask 1,或 /ask <自定义内容>';
973
972
  const fallbackText = `💬 ${q.header || q.question}\n${q.header ? q.question + '\n' : ''}${optionLines}\n\n${answerHint}`;
974
973
  const result = await sendInteractionPayload(permCtx.adapter, envelope, interaction, fallbackText, permCtx.replyContext);
975
- cardSent = !!result;
974
+ cardSent = isInteractionSendAccepted(result);
976
975
  }
977
976
  catch (err) {
978
977
  logger.warn(`[AgentRunner] AskUserQuestion card send failed for q${i}:`, err);
@@ -1393,7 +1392,7 @@ export class AgentRunner {
1393
1392
  });
1394
1393
  const fallbackText = '📋 计划审批:AI 已完成规划,等待审批。\n回复 /ask 1 批准 / /ask 2 拒绝';
1395
1394
  const result = await sendInteractionPayload(permCtx.adapter, envelope, interaction, fallbackText, permCtx.replyContext);
1396
- cardSent = !!result;
1395
+ cardSent = isInteractionSendAccepted(result);
1397
1396
  }
1398
1397
  catch (err) {
1399
1398
  logger.warn('[AgentRunner] ExitPlanMode card send failed:', err);
@@ -1441,7 +1440,7 @@ export class AgentRunner {
1441
1440
  * SDK 原始事件 → 标准 AgentEvent 转换
1442
1441
  * 所有 SDK 特有的事件类型引用封装在此方法内
1443
1442
  */
1444
- async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false, sandboxSettingsFile, permissionMode) {
1443
+ async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false, sandboxSettingsFile, permissionMode, turn) {
1445
1444
  let lastSessionId;
1446
1445
  let hasTurnActivity = false;
1447
1446
  let ignoredPreTurnResult = false;
@@ -1458,6 +1457,7 @@ export class AgentRunner {
1458
1457
  let turnCount = 0;
1459
1458
  const seenMessageIds = new Set();
1460
1459
  let lastModelCall;
1460
+ let lastAssistantWasSynthetic = false;
1461
1461
  let lastAssistantUuid;
1462
1462
  // 流式收集各次大模型调用(fallback:SDK iterations 为空时使用)
1463
1463
  const collectedCalls = [];
@@ -1466,8 +1466,15 @@ export class AgentRunner {
1466
1466
  // 提取 session_id(任意 SDK 事件都可能携带)
1467
1467
  if (event.session_id && event.session_id !== lastSessionId) {
1468
1468
  lastSessionId = event.session_id;
1469
- this.updateSessionId(sessionId, event.session_id);
1470
- yield { type: 'session_id', sessionId: event.session_id };
1469
+ if (await this.acceptDiscoveredSessionId(sessionId, event.session_id, turn)) {
1470
+ yield { type: 'session_id', sessionId: event.session_id };
1471
+ }
1472
+ else {
1473
+ logger.warn(`[AgentRunner] Rejected stale backend discovery: sessionId=${sessionId} backend=${event.session_id} turn=${turn?.turnId ?? 'legacy'}`);
1474
+ const error = new Error('backend discovery rejected by active turn lease');
1475
+ error.code = 'TOPIC_BACKEND_DISCOVERY_REJECTED';
1476
+ throw error;
1477
+ }
1471
1478
  }
1472
1479
  if (event.type === 'user' && event.uuid === inputId) {
1473
1480
  hasTurnActivity = true;
@@ -1563,7 +1570,9 @@ export class AgentRunner {
1563
1570
  const streamEvent = event.event;
1564
1571
  if (streamEvent?.type === 'message_start' && streamEvent.message?.usage) {
1565
1572
  hasTurnActivity = true;
1573
+ lastAssistantWasSynthetic = streamEvent.message.model === '<synthetic>';
1566
1574
  lastModelCall = {
1575
+ messageId: streamEvent.message.id,
1567
1576
  uuid: event.uuid,
1568
1577
  model: streamEvent.message.model,
1569
1578
  tokenUsage: streamEvent.message.usage,
@@ -1573,6 +1582,7 @@ export class AgentRunner {
1573
1582
  call_index: collectedCalls.length,
1574
1583
  model: streamEvent.message.model ?? callModel ?? this.model,
1575
1584
  request_id: event.request_id,
1585
+ message_id: streamEvent.message.id,
1576
1586
  tokenUsage: { ...streamEvent.message.usage },
1577
1587
  });
1578
1588
  }
@@ -1625,27 +1635,40 @@ export class AgentRunner {
1625
1635
  yield { type: 'state_changed', state: event.state };
1626
1636
  }
1627
1637
  // assistant: 提取 tool_use 和文本(仅无 text_delta 时提取文本)
1628
- if (event.type === 'assistant' && event.message?.content) {
1629
- hasTurnActivity = true;
1630
- lastAssistantUuid = event.uuid ?? lastAssistantUuid;
1631
- const msgId = event.message.id;
1632
- if (!msgId || !seenMessageIds.has(msgId)) {
1633
- if (msgId)
1634
- seenMessageIds.add(msgId);
1635
- turnCount++;
1636
- }
1637
- if (event.message.usage) {
1638
+ if (event.type === 'assistant') {
1639
+ const isSyntheticAssistant = event.message?.model === '<synthetic>'
1640
+ || (typeof event.error === 'string' && event.isApiErrorMessage === true);
1641
+ lastAssistantWasSynthetic = isSyntheticAssistant;
1642
+ if (event.message && (event.message.usage || isSyntheticAssistant)) {
1643
+ // A message id is stable across the partial stream and the final
1644
+ // assistant event. Never merge a synthetic/API-error assistant into
1645
+ // the preceding real call, even if an upstream adapter reuses an id
1646
+ // or omits the usage object on the error frame.
1647
+ const sameModelCall = !isSyntheticAssistant
1648
+ && typeof event.message.id === 'string'
1649
+ && event.message.id === lastModelCall?.messageId;
1638
1650
  lastModelCall = {
1639
- ...lastModelCall,
1651
+ ...(sameModelCall ? lastModelCall : undefined),
1640
1652
  messageId: event.message.id,
1653
+ uuid: event.uuid,
1641
1654
  requestId: event.request_id,
1642
1655
  model: event.message.model,
1643
- tokenUsage: {
1656
+ tokenUsage: sameModelCall ? {
1644
1657
  ...event.message.usage,
1645
1658
  ...(lastModelCall?.tokenUsage ?? {}),
1646
- },
1659
+ } : { ...(event.message.usage ?? {}) },
1647
1660
  };
1648
1661
  }
1662
+ if (!event.message?.content)
1663
+ continue;
1664
+ hasTurnActivity = true;
1665
+ lastAssistantUuid = event.uuid ?? lastAssistantUuid;
1666
+ const msgId = event.message.id;
1667
+ if (!msgId || !seenMessageIds.has(msgId)) {
1668
+ if (msgId)
1669
+ seenMessageIds.add(msgId);
1670
+ turnCount++;
1671
+ }
1649
1672
  // 统计本轮 base agent 全部输出字符数(text + tool_use input)
1650
1673
  let turnOutputChars = 0;
1651
1674
  for (const content of event.message.content) {
@@ -1720,6 +1743,18 @@ export class AgentRunner {
1720
1743
  // 非 Claude(DeepSeek/OpenAI 兼容):cache_read 是服务端 KV cache 不占上下文窗口,
1721
1744
  // input_tokens 本身就是完整的上下文输入量。
1722
1745
  const u = event.usage;
1746
+ // Claude Code represents transport/API failures as a fresh
1747
+ // `<synthetic>` assistant message. Its usage describes the failed
1748
+ // terminal request (normally zero); event.usage remains the aggregate
1749
+ // of earlier successful calls in the query and must not be presented
1750
+ // as the failed request's context usage.
1751
+ const isSyntheticError = event.is_error === true
1752
+ && (lastAssistantWasSynthetic || lastModelCall?.model === '<synthetic>');
1753
+ const syntheticUsage = lastModelCall?.tokenUsage ?? {
1754
+ input_tokens: 0,
1755
+ output_tokens: 0,
1756
+ };
1757
+ const terminalUsage = isSyntheticError ? syntheticUsage : u;
1723
1758
  const effectiveModel = callModel ?? this.model;
1724
1759
  const isClaudeModel = isClaudeContextUsageModel(effectiveModel);
1725
1760
  const totalTokens = contextTokensForUsage(u, !!isClaudeModel);
@@ -1762,7 +1797,11 @@ export class AgentRunner {
1762
1797
  // 组装 modelCalls:优先 SDK iterations,fallback 流式收集,兜底降级单行。
1763
1798
  const callModel_ = callModel ?? this.model;
1764
1799
  let modelCalls;
1765
- const iterArr = Array.isArray(u?.iterations) && u.iterations.length > 0 ? u.iterations : null;
1800
+ const iterArr = !isSyntheticError
1801
+ && Array.isArray(u?.iterations)
1802
+ && u.iterations.length > 0
1803
+ ? u.iterations
1804
+ : null;
1766
1805
  if (iterArr) {
1767
1806
  modelCalls = iterArr.map((it, i) => ({
1768
1807
  call_index: i, model: callModel_, tokenUsage: it, contextUsage: contextUsageForCall(it),
@@ -1774,16 +1813,19 @@ export class AgentRunner {
1774
1813
  contextUsage: contextUsageForCall(call.tokenUsage),
1775
1814
  }));
1776
1815
  }
1777
- else if (u) {
1816
+ else if (u && !isSyntheticError) {
1778
1817
  // 降级:无逐次数据,写一条累计行
1779
1818
  modelCalls = [{ call_index: 0, model: callModel_, tokenUsage: u, contextUsage: contextUsageForCall(u), degraded: true }];
1780
1819
  }
1781
1820
  // `event.usage` is aggregate query usage when the SDK made multiple
1782
1821
  // model calls. The top-level context field must describe a single
1783
- // current request; aggregate usage remains available in tokenUsage.
1784
- const contextUsage = lastModelCall?.contextUsage
1785
- ?? modelCalls?.at(-1)?.contextUsage
1786
- ?? aggregateContextUsage;
1822
+ // current request. For normal results aggregate usage remains available
1823
+ // in tokenUsage; synthetic errors use their own terminal usage instead.
1824
+ const contextUsage = isSyntheticError
1825
+ ? lastModelCall?.contextUsage
1826
+ : lastModelCall?.contextUsage
1827
+ ?? modelCalls?.at(-1)?.contextUsage
1828
+ ?? aggregateContextUsage;
1787
1829
  const completeEvent = {
1788
1830
  type: 'complete',
1789
1831
  result: cleanResult,
@@ -1796,7 +1838,7 @@ export class AgentRunner {
1796
1838
  terminalReason: event.terminal_reason,
1797
1839
  sessionTitle: event.session_title,
1798
1840
  numTurns: event.num_turns,
1799
- tokenUsage: event.usage,
1841
+ tokenUsage: terminalUsage,
1800
1842
  contextUsage,
1801
1843
  lastModelCall,
1802
1844
  modelCalls,
@@ -1871,6 +1913,7 @@ export class AgentRunner {
1871
1913
  policyCode: 'claude_startup_arguments_too_long',
1872
1914
  decisionSource: 'infrastructure',
1873
1915
  agentAid: this.permissionContexts.get(sessionId)?.selfAid,
1916
+ agentName: this.permissionContexts.get(sessionId)?.agentName,
1874
1917
  summary: detail.message.slice(0, 512),
1875
1918
  effect: 'operation_skipped',
1876
1919
  });
@@ -1904,6 +1947,9 @@ export class AgentRunner {
1904
1947
  }
1905
1948
  }
1906
1949
  async runQuery(sessionId, prompt, projectPath, initialClaudeSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
1950
+ // Validate the daemon-injected session namespace before the SDK inherits
1951
+ // the process environment for any temporary state.
1952
+ getManagedTaskTempDir(runtimeEnv);
1907
1953
  // 记录当前 evolcore session ID,用于 Agent ctl 环境变量注入
1908
1954
  // 同步用户级配置到内存
1909
1955
  this.syncFromUserSettings();
@@ -1922,8 +1968,22 @@ export class AgentRunner {
1922
1968
  }
1923
1969
  ensureDir(projectPath);
1924
1970
  ensureDir(path.join(projectPath, '.claude'));
1925
- // 优先使用传入的 agentSessionId(从数据库恢复),否则使用内存中的
1926
- let agentSessionId = initialClaudeSessionId || this.activeSessions.get(sessionId);
1971
+ const topicBinding = modelOverride?.backend?.kind === 'topic';
1972
+ if (topicBinding && (!modelOverride?.turn || modelOverride.turn.sessionId !== sessionId || !this.onSessionIdUpdate)) {
1973
+ const error = new Error('topic backend run requires an active TurnLease and binding callback');
1974
+ error.code = 'TOPIC_TURN_LEASE_REQUIRED';
1975
+ throw error;
1976
+ }
1977
+ const topicAgentSessionId = topicBinding ? modelOverride?.backend?.agentSessionId ?? null : null;
1978
+ const cachedAgentSessionId = topicBinding
1979
+ ? (topicAgentSessionId && this.activeSessions.get(sessionId) === topicAgentSessionId
1980
+ ? topicAgentSessionId
1981
+ : undefined)
1982
+ : this.activeSessions.get(sessionId);
1983
+ let agentSessionId = initialClaudeSessionId || cachedAgentSessionId;
1984
+ if (topicBinding && topicAgentSessionId !== agentSessionId) {
1985
+ agentSessionId = topicAgentSessionId || undefined;
1986
+ }
1927
1987
  // 验证会话文件是否存在且有效(仅在有 agentSessionId 时)
1928
1988
  if (agentSessionId) {
1929
1989
  const homeDir = os.homedir();
@@ -1954,11 +2014,18 @@ export class AgentRunner {
1954
2014
  }
1955
2015
  }
1956
2016
  if (!isValid) {
2017
+ if (topicBinding) {
2018
+ const error = new Error(`bound topic backend is unavailable: ${agentSessionId}`);
2019
+ error.code = 'TOPIC_BACKEND_UNAVAILABLE';
2020
+ throw error;
2021
+ }
1957
2022
  logger.warn(`[AgentRunner] Invalid session file, starting new session`);
1958
2023
  agentSessionId = undefined;
1959
2024
  this.activeSessions.delete(sessionId);
1960
2025
  if (this.onSessionIdUpdate) {
1961
- this.onSessionIdUpdate(sessionId, '');
2026
+ void Promise.resolve(this.onSessionIdUpdate(sessionId, '')).catch(error => {
2027
+ logger.debug(`[AgentRunner] ignored empty session binding callback failure: ${error instanceof Error ? error.message : String(error)}`);
2028
+ });
1962
2029
  }
1963
2030
  }
1964
2031
  }
@@ -1973,9 +2040,11 @@ export class AgentRunner {
1973
2040
  // 缺省回落 agent 级 this.permissionMode。作为 per-call 入参(hook/canUseTool 闭包捕获),
1974
2041
  // 不写实例字段,多对端并发互不污染(与 model/effort 同构)。
1975
2042
  const requestedPermissionMode = modelOverride?.permissionMode || this.permissionMode;
1976
- const normalizedPermission = normalizePermissionMode(requestedPermissionMode);
1977
- const callPermissionMode = normalizedPermission.mode;
2043
+ const callPermissionMode = normalizeExecutionPermissionMode(requestedPermissionMode);
1978
2044
  const runPermissionContext = this.permissionContexts.get(sessionId);
2045
+ if (callPermissionMode === 'fullaccess' && !hasTrustedFullAccessContext(runPermissionContext)) {
2046
+ throw new Error('Claude fullaccess requires a trusted per-call execution authorization');
2047
+ }
1979
2048
  const executionSandbox = resolvePhaseOneExecutionSandbox(callPermissionMode, runPermissionContext?.role);
1980
2049
  const inputId = modelOverride?.turn?.inputId ?? crypto.randomUUID();
1981
2050
  if (executionSandbox.state !== 'off') {
@@ -1990,6 +2059,7 @@ export class AgentRunner {
1990
2059
  reason: detail.message.slice(0, 512),
1991
2060
  sessionId,
1992
2061
  agentAid: context?.selfAid,
2062
+ agentName: context?.agentName,
1993
2063
  permissionMode: callPermissionMode,
1994
2064
  role: context?.role,
1995
2065
  callId: inputId,
@@ -2008,6 +2078,7 @@ export class AgentRunner {
2008
2078
  policyCode: SANDBOX_INITIALIZATION_FAILED,
2009
2079
  decisionSource: 'infrastructure',
2010
2080
  agentAid: context?.selfAid,
2081
+ agentName: context?.agentName,
2011
2082
  permissionMode: callPermissionMode,
2012
2083
  summary: detail.message.slice(0, 512),
2013
2084
  effect: 'operation_skipped',
@@ -2041,7 +2112,7 @@ export class AgentRunner {
2041
2112
  ...(updatedInput ? { updatedInput } : {}),
2042
2113
  },
2043
2114
  });
2044
- const recordPreflightDenial = async (toolName, toolInput, preflight, requestId, policyCode) => {
2115
+ const recordPreflightDenial = async (toolName, toolInput, preflight, requestId, policyCode, matchedPath) => {
2045
2116
  const code = policyCode ?? (preflight?.behavior === 'deny' ? preflight.policyCode : undefined);
2046
2117
  if (!code)
2047
2118
  return;
@@ -2054,6 +2125,7 @@ export class AgentRunner {
2054
2125
  summary,
2055
2126
  sessionId,
2056
2127
  agentAid: ctx?.selfAid,
2128
+ agentName: ctx?.agentName,
2057
2129
  permissionMode: callPermissionMode,
2058
2130
  channel: ctx?.channel,
2059
2131
  actorId: ctx?.userId,
@@ -2061,6 +2133,7 @@ export class AgentRunner {
2061
2133
  selfAid: ctx?.selfAid,
2062
2134
  requestId: typeof requestId === 'string' ? requestId : undefined,
2063
2135
  taskId: ctx?.taskId,
2136
+ matchedPath: matchedPath ?? (preflight?.behavior === 'deny' ? preflight.matchedPath : undefined),
2064
2137
  });
2065
2138
  try {
2066
2139
  await ctx?.recordExecutionAnomaly?.({
@@ -2073,6 +2146,7 @@ export class AgentRunner {
2073
2146
  policyCode: code,
2074
2147
  decisionSource: 'policy',
2075
2148
  agentAid: ctx?.selfAid,
2149
+ agentName: ctx?.agentName,
2076
2150
  permissionMode: callPermissionMode,
2077
2151
  summary,
2078
2152
  effect: 'operation_skipped',
@@ -2121,6 +2195,14 @@ export class AgentRunner {
2121
2195
  delete toolInput.pages;
2122
2196
  }
2123
2197
  let updatedInput = toolInput === originalInput ? undefined : toolInput;
2198
+ if (callPermissionMode === 'fullaccess') {
2199
+ if (!hasTrustedFullAccessContext(this.permissionContexts.get(sessionId))) {
2200
+ return hookDecision('deny', 'fullaccess execution authorization is no longer active', updatedInput);
2201
+ }
2202
+ return hookDecision('allow', undefined, toolName === 'Bash'
2203
+ ? { ...toolInput, dangerouslyDisableSandbox: true }
2204
+ : updatedInput);
2205
+ }
2124
2206
  const preparedSafeOutputCommand = toolName === 'Bash' && typeof toolInput.command === 'string'
2125
2207
  ? toolInput.command
2126
2208
  : undefined;
@@ -2142,6 +2224,7 @@ export class AgentRunner {
2142
2224
  : undefined;
2143
2225
  const preflight = evaluateToolPreflight(toolName, toolInput, {
2144
2226
  sessionId,
2227
+ managedTempDir: permCtx?.managedTempDir,
2145
2228
  selfAid: permCtx?.selfAid,
2146
2229
  channel: permCtx?.channel,
2147
2230
  userId: permCtx?.userId,
@@ -2178,6 +2261,7 @@ export class AgentRunner {
2178
2261
  const permCtx = this.permissionContexts.get(sessionId);
2179
2262
  const readonlyContext = {
2180
2263
  sessionId,
2264
+ managedTempDir: permCtx?.managedTempDir,
2181
2265
  channel: permCtx?.channel,
2182
2266
  peerId: permCtx?.userId,
2183
2267
  role: permCtx?.role,
@@ -2186,7 +2270,7 @@ export class AgentRunner {
2186
2270
  };
2187
2271
  const roResult = checkReadonly(toolName, toolInput, projectPath, readonlyContext);
2188
2272
  if (roResult.behavior === 'deny') {
2189
- await recordPreflightDenial(toolName, toolInput, undefined, input.tool_use_id ?? input.toolUseID, roResult.policyCode ?? 'readonly_mode');
2273
+ await recordPreflightDenial(toolName, toolInput, undefined, input.tool_use_id ?? input.toolUseID, roResult.policyCode ?? 'readonly_mode', roResult.matchedPath);
2190
2274
  return hookDecision('deny', roResult.message, updatedInput);
2191
2275
  }
2192
2276
  return hookDecision('allow', undefined, updatedInput);
@@ -2249,6 +2333,20 @@ export class AgentRunner {
2249
2333
  if (toolName === 'ExitPlanMode') {
2250
2334
  return await this.handleExitPlanMode(sessionId, input, options);
2251
2335
  }
2336
+ if (callPermissionMode === 'fullaccess') {
2337
+ if (!hasTrustedFullAccessContext(this.permissionContexts.get(sessionId))) {
2338
+ return {
2339
+ behavior: 'deny',
2340
+ message: 'fullaccess execution authorization is no longer active',
2341
+ decisionClassification: 'user_reject',
2342
+ };
2343
+ }
2344
+ return {
2345
+ behavior: 'allow',
2346
+ updatedInput: toolName === 'Bash' ? { ...input, dangerouslyDisableSandbox: true } : input,
2347
+ decisionClassification: 'user_permanent',
2348
+ };
2349
+ }
2252
2350
  if (toolName === 'Bash' && 'dangerouslyDisableSandbox' in input) {
2253
2351
  input = { ...input };
2254
2352
  delete input.dangerouslyDisableSandbox;
@@ -2266,6 +2364,7 @@ export class AgentRunner {
2266
2364
  const permCtx = this.permissionContexts.get(sessionId);
2267
2365
  const preflight = evaluateToolPreflight(toolName, input, {
2268
2366
  sessionId,
2367
+ managedTempDir: permCtx?.managedTempDir,
2269
2368
  selfAid: permCtx?.selfAid,
2270
2369
  channel: permCtx?.channel,
2271
2370
  userId: permCtx?.userId,
@@ -2338,6 +2437,7 @@ export class AgentRunner {
2338
2437
  const permCtx = this.permissionContexts.get(sessionId);
2339
2438
  const readonlyContext = {
2340
2439
  sessionId,
2440
+ managedTempDir: permCtx?.managedTempDir,
2341
2441
  channel: permCtx?.channel,
2342
2442
  peerId: permCtx?.userId,
2343
2443
  role: permCtx?.role,
@@ -2346,7 +2446,7 @@ export class AgentRunner {
2346
2446
  };
2347
2447
  const roResult = checkReadonly(toolName, input, projectPath, readonlyContext);
2348
2448
  if (roResult.behavior === 'deny') {
2349
- await recordPreflightDenial(toolName, input, undefined, options.toolUseID, roResult.policyCode ?? 'readonly_mode');
2449
+ await recordPreflightDenial(toolName, input, undefined, options.toolUseID, roResult.policyCode ?? 'readonly_mode', roResult.matchedPath);
2350
2450
  return { behavior: 'deny', message: roResult.message, decisionClassification: 'user_reject' };
2351
2451
  }
2352
2452
  return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_permanent' };
@@ -2402,7 +2502,9 @@ export class AgentRunner {
2402
2502
  };
2403
2503
  };
2404
2504
  const useSettingSources = this.config?.agents?.claude?.useSettingSources !== false;
2405
- const settingSources = useSettingSources ? ['project', 'user'] : [];
2505
+ const settingSources = callPermissionMode === 'fullaccess'
2506
+ ? []
2507
+ : useSettingSources ? ['project', 'user'] : [];
2406
2508
  const enableSummaries = this.config?.agents?.claude?.agentProgressSummaries !== false;
2407
2509
  const excludeDynamic = this.config?.agents?.claude?.excludeDynamicSections === true;
2408
2510
  // 公共 options(新旧模式共用)
@@ -2425,7 +2527,9 @@ export class AgentRunner {
2425
2527
  }
2426
2528
  const sdkModel = resolveSdkModel(callModel, this.baseUrl);
2427
2529
  const capabilityOptions = await this.resolveCapabilityRunOptions(projectPath);
2428
- const managedSettings = buildClaudeManagedLockdownSettings(capabilityOptions);
2530
+ const managedSettings = callPermissionMode === 'fullaccess'
2531
+ ? undefined
2532
+ : buildClaudeManagedLockdownSettings(capabilityOptions);
2429
2533
  const sandboxOptions = executionSandbox.state === 'off'
2430
2534
  ? { enabled: false }
2431
2535
  : await (async () => {
@@ -2465,9 +2569,11 @@ export class AgentRunner {
2465
2569
  };
2466
2570
  })();
2467
2571
  if (executionSandbox.state === 'off') {
2468
- logger.info(process.platform === 'linux'
2469
- ? '[ClaudeSandbox] task sandbox disabled for authenticated owner bypass; outer H-class guard remains active'
2470
- : '[ClaudeSandbox] disabled for authenticated owner bypass task');
2572
+ logger.info(callPermissionMode === 'fullaccess'
2573
+ ? '[ClaudeSandbox] disabled for trusted per-call fullaccess execution'
2574
+ : process.platform === 'linux'
2575
+ ? '[ClaudeSandbox] task sandbox disabled for authenticated owner bypass; outer H-class guard remains active'
2576
+ : '[ClaudeSandbox] disabled for authenticated owner bypass task');
2471
2577
  }
2472
2578
  const handleClaudeStderr = (msg) => {
2473
2579
  const trimmed = msg.trim();
@@ -2493,16 +2599,17 @@ export class AgentRunner {
2493
2599
  model: sdkModel,
2494
2600
  ...capabilityOptions,
2495
2601
  strictMcpConfig: false,
2496
- managedSettings,
2602
+ ...(managedSettings ? { managedSettings } : {}),
2497
2603
  ...(callEffort ? { effort: callEffort } : {}),
2498
2604
  ...(this.claudeExecutablePath ? { pathToClaudeCodeExecutable: this.claudeExecutablePath } : {}),
2499
- ...(executionSandbox.state === 'off' && process.platform === 'linux' ? {
2605
+ ...(executionSandbox.state === 'off' && callPermissionMode !== 'fullaccess' && process.platform === 'linux' ? {
2500
2606
  spawnClaudeCodeProcess: (options) => spawnClaudeWithHClassGuard(options, resolvePaths().root, handleClaudeStderr),
2501
2607
  } : {}),
2502
2608
  autoCompactWindow: autoCompactWindowForModel(sdkModel),
2503
2609
  advisorModel: 'haiku',
2504
2610
  canUseTool: canUseToolCallback,
2505
2611
  permissionMode: sdkPermissionMode,
2612
+ ...(callPermissionMode === 'fullaccess' ? { allowDangerouslySkipPermissions: true } : {}),
2506
2613
  sandbox: sandboxOptions,
2507
2614
  persistSession: true,
2508
2615
  includePartialMessages: true,
@@ -2624,9 +2731,14 @@ export class AgentRunner {
2624
2731
  const currentSession = await sessionManager.getSessionById?.(sessionId);
2625
2732
  if (currentSession?.metadata?.resumeAt) {
2626
2733
  resumeAt = currentSession.metadata.resumeAt;
2627
- const newMeta = { ...currentSession.metadata };
2628
- delete newMeta.resumeAt;
2629
- await sessionManager.updateSession(sessionId, { metadata: newMeta });
2734
+ if (typeof sessionManager.patchSessionMetadata === 'function') {
2735
+ await sessionManager.patchSessionMetadata(sessionId, { resumeAt: undefined });
2736
+ }
2737
+ else {
2738
+ const newMeta = { ...currentSession.metadata };
2739
+ delete newMeta.resumeAt;
2740
+ await sessionManager.updateSession(sessionId, { metadata: newMeta });
2741
+ }
2630
2742
  logger.info(`[AgentRunner] Consuming resumeAt: ${resumeAt}`);
2631
2743
  }
2632
2744
  }
@@ -2638,10 +2750,14 @@ export class AgentRunner {
2638
2750
  const msgStream = new MessageStream();
2639
2751
  if (images && images.length > 0) {
2640
2752
  logger.info('[AgentRunner] Creating query with images:', images.length, 'first image size:', images[0]?.data?.length ?? 0);
2641
- logger.debug('[AgentRunner] Skipping resume for image message to avoid history conflict');
2642
2753
  msgStream.push(prompt, images, inputId);
2643
2754
  try {
2644
- sdkStream = createQuery(msgStream);
2755
+ // Topic bindings are authoritative for every input shape. Starting
2756
+ // an unresumed Claude query for a bound topic would discover a second
2757
+ // backend and then fail activation with a binding conflict.
2758
+ sdkStream = topicBinding
2759
+ ? createQuery(msgStream, agentSessionId, resumeAt)
2760
+ : createQuery(msgStream);
2645
2761
  }
2646
2762
  catch (error) {
2647
2763
  this.cleanupSandboxSettingsFile(sessionId, sandboxSettingsFile);
@@ -2675,14 +2791,16 @@ export class AgentRunner {
2675
2791
  }
2676
2792
  this.activeMessageStreams.set(sessionId, msgStream);
2677
2793
  this.activeQueries.set(sessionId, sdkStream);
2678
- const donePromise = new Promise(resolve => this.streamDoneResolvers.set(sessionId, resolve));
2794
+ let resolveDone;
2795
+ const donePromise = new Promise(resolve => { resolveDone = resolve; });
2796
+ this.streamDoneResolvers.set(sessionId, resolveDone);
2679
2797
  this.streamDone.set(sessionId, donePromise);
2680
2798
  // 保存 interrupt 能力(不写 activeStreams,由 registerStream 管理活跃状态)
2681
2799
  if ('interrupt' in sdkStream && typeof sdkStream.interrupt === 'function') {
2682
2800
  this.interruptFns.set(sessionId, () => sdkStream.interrupt());
2683
2801
  }
2684
2802
  // 返回标准 AgentEvent 流(重试由 MessageProcessor 层负责)
2685
- const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId, sandboxSettingsFile, callPermissionMode);
2803
+ const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId, sandboxSettingsFile, callPermissionMode, modelOverride?.turn);
2686
2804
  const self = this;
2687
2805
  return (async function* () {
2688
2806
  try {
@@ -2690,10 +2808,15 @@ export class AgentRunner {
2690
2808
  }
2691
2809
  finally {
2692
2810
  self.cleanupSandboxSettingsFile(sessionId, sandboxSettingsFile);
2693
- self.streamDoneResolvers.get(sessionId)?.();
2694
- self.streamDoneResolvers.delete(sessionId);
2695
- self.streamDone.delete(sessionId);
2696
- self.activeQueries.delete(sessionId);
2811
+ resolveDone();
2812
+ if (self.streamDoneResolvers.get(sessionId) === resolveDone)
2813
+ self.streamDoneResolvers.delete(sessionId);
2814
+ if (self.streamDone.get(sessionId) === donePromise)
2815
+ self.streamDone.delete(sessionId);
2816
+ if (self.activeQueries.get(sessionId) === sdkStream)
2817
+ self.activeQueries.delete(sessionId);
2818
+ if (self.activeMessageStreams.get(sessionId) === msgStream)
2819
+ self.activeMessageStreams.delete(sessionId);
2697
2820
  }
2698
2821
  })();
2699
2822
  }
@@ -2761,19 +2884,64 @@ export class AgentRunner {
2761
2884
  injectUserMessage(sessionId, text) {
2762
2885
  this.activeMessageStreams.get(sessionId)?.push(text);
2763
2886
  }
2887
+ async acceptDiscoveredSessionId(sessionId, agentSessionId, turn) {
2888
+ const result = this.onSessionIdUpdate
2889
+ ? (turn === undefined
2890
+ ? await this.onSessionIdUpdate(sessionId, agentSessionId)
2891
+ : await this.onSessionIdUpdate(sessionId, agentSessionId, { turn }))
2892
+ : 'legacy_updated';
2893
+ const accepted = result === undefined || result === 'activated' || result === 'already_active' || result === 'legacy_updated';
2894
+ if (accepted) {
2895
+ this.activeSessions.set(sessionId, agentSessionId);
2896
+ }
2897
+ return accepted;
2898
+ }
2764
2899
  updateSessionId(sessionId, agentSessionId) {
2765
2900
  logger.info(`[AgentRunner] updateSessionId called: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
2766
- this.activeSessions.set(sessionId, agentSessionId);
2767
- if (this.onSessionIdUpdate) {
2768
- this.onSessionIdUpdate(sessionId, agentSessionId);
2901
+ const previousAgentSessionId = this.activeSessions.get(sessionId);
2902
+ const canApply = () => {
2903
+ const current = this.activeSessions.get(sessionId);
2904
+ return current === previousAgentSessionId || (previousAgentSessionId === undefined && current === undefined);
2905
+ };
2906
+ if (!this.onSessionIdUpdate) {
2907
+ if (!canApply())
2908
+ return;
2909
+ if (agentSessionId)
2910
+ this.activeSessions.set(sessionId, agentSessionId);
2911
+ else if (this.activeSessions.get(sessionId) === previousAgentSessionId)
2912
+ this.activeSessions.delete(sessionId);
2913
+ return;
2769
2914
  }
2915
+ void Promise.resolve(this.onSessionIdUpdate(sessionId, agentSessionId)).then(result => {
2916
+ if (!canApply())
2917
+ return;
2918
+ if (result === undefined || result === 'activated' || result === 'already_active' || result === 'legacy_updated') {
2919
+ if (agentSessionId)
2920
+ this.activeSessions.set(sessionId, agentSessionId);
2921
+ else if (this.activeSessions.get(sessionId) === previousAgentSessionId)
2922
+ this.activeSessions.delete(sessionId);
2923
+ }
2924
+ else if (!agentSessionId) {
2925
+ if (this.activeSessions.get(sessionId) === previousAgentSessionId)
2926
+ this.activeSessions.delete(sessionId);
2927
+ }
2928
+ }).catch(error => {
2929
+ logger.warn(`[AgentRunner] session binding callback failed: ${error instanceof Error ? error.message : String(error)}`);
2930
+ if (!agentSessionId && canApply() && this.activeSessions.get(sessionId) === previousAgentSessionId) {
2931
+ this.activeSessions.delete(sessionId);
2932
+ }
2933
+ });
2770
2934
  }
2771
- runSessionCommand(prompt, agentSessionId, projectPath) {
2935
+ runSessionCommand(prompt, agentSessionId, projectPath, modelOverride) {
2936
+ const callModel = modelOverride?.model || this.model;
2937
+ const callEffort = (modelOverride?.effortMode === 'model_default'
2938
+ ? undefined
2939
+ : modelOverride?.effort ?? this.effort);
2772
2940
  return query({
2773
2941
  prompt,
2774
2942
  options: {
2775
2943
  cwd: projectPath,
2776
- model: resolveSdkModel(this.model, this.baseUrl),
2944
+ model: resolveSdkModel(callModel, this.baseUrl),
2777
2945
  resume: agentSessionId,
2778
2946
  maxTurns: 1,
2779
2947
  tools: [],
@@ -2782,6 +2950,7 @@ export class AgentRunner {
2782
2950
  strictMcpConfig: true,
2783
2951
  settingSources: [],
2784
2952
  managedSettings: buildClaudeManagedLockdownSettings(),
2953
+ ...(callEffort ? { effort: callEffort } : {}),
2785
2954
  permissionMode: this.toSdkPermissionMode(),
2786
2955
  env: this.getAgentEnv()
2787
2956
  }
@@ -2790,31 +2959,74 @@ export class AgentRunner {
2790
2959
  /**
2791
2960
  * 主动压缩会话上下文
2792
2961
  */
2793
- async compactSession(sessionId, agentSessionId, projectPath) {
2962
+ async compactSession(sessionId, agentSessionId, projectPath, modelOverride) {
2963
+ const startedAt = Date.now();
2964
+ let eventCount = 0;
2965
+ let lastEvent;
2794
2966
  try {
2795
- logger.info(`[AgentRunner] Compacting session: ${agentSessionId}`);
2796
- const stream = this.runSessionCommand('/compact', agentSessionId, projectPath);
2967
+ logger.info(`[AgentRunner] Compacting session: session=${sessionId} agent=${agentSessionId} project=${projectPath}`);
2968
+ const stream = this.runSessionCommand('/compact', agentSessionId, projectPath, modelOverride);
2797
2969
  this.activeStreams.set(sessionId, stream);
2798
2970
  try {
2799
- let receivedBoundary = false;
2971
+ let boundary;
2972
+ let compactStatus;
2800
2973
  for await (const event of stream) {
2801
- if (event.type === 'system' && event.subtype === 'compact_boundary') {
2802
- logger.info(`[AgentRunner] Compact completed, pre_tokens: ${event.compact_metadata?.pre_tokens}`);
2803
- receivedBoundary = true;
2974
+ eventCount += 1;
2975
+ lastEvent = { type: event?.type, subtype: event?.subtype };
2976
+ if (event.type === 'system' && event.subtype === 'status' && event.compact_result) {
2977
+ compactStatus = {
2978
+ result: event.compact_result,
2979
+ error: event.compact_error,
2980
+ };
2804
2981
  }
2982
+ if (event.type === 'system' && event.subtype === 'compact_boundary')
2983
+ boundary = event;
2984
+ }
2985
+ const durationMs = Date.now() - startedAt;
2986
+ if (compactStatus?.result === 'failed') {
2987
+ const result = {
2988
+ ok: false,
2989
+ code: 'sdk_error',
2990
+ message: compactStatus.error || 'Claude SDK reported compact failure',
2991
+ durationMs,
2992
+ eventCount,
2993
+ lastEvent,
2994
+ };
2995
+ logger.warn(`[AgentRunner] Compact failed: session=${sessionId} agent=${agentSessionId} code=${result.code} events=${eventCount} durationMs=${durationMs} message=${result.message}`);
2996
+ return result;
2805
2997
  }
2806
- if (!receivedBoundary) {
2807
- logger.warn(`[AgentRunner] Compact stream ended without compact_boundary event`);
2998
+ if (!boundary) {
2999
+ const result = {
3000
+ ok: false,
3001
+ code: 'missing_boundary',
3002
+ message: 'Compact stream ended without compact_boundary event',
3003
+ durationMs,
3004
+ eventCount,
3005
+ lastEvent,
3006
+ };
3007
+ logger.warn(`[AgentRunner] Compact failed: session=${sessionId} agent=${agentSessionId} code=${result.code} events=${eventCount} durationMs=${durationMs}`);
3008
+ return result;
2808
3009
  }
2809
- return receivedBoundary;
3010
+ const result = {
3011
+ ok: true,
3012
+ durationMs,
3013
+ eventCount,
3014
+ preTokens: boundary.compact_metadata?.pre_tokens,
3015
+ postTokens: boundary.compact_metadata?.post_tokens,
3016
+ };
3017
+ logger.info(`[AgentRunner] Compact completed: session=${sessionId} agent=${agentSessionId} events=${eventCount} durationMs=${durationMs} pre_tokens=${result.preTokens ?? 'unknown'} post_tokens=${result.postTokens ?? 'unknown'}`);
3018
+ return result;
2810
3019
  }
2811
3020
  finally {
2812
3021
  this.activeStreams.delete(sessionId);
2813
3022
  }
2814
3023
  }
2815
3024
  catch (error) {
2816
- logger.error('[AgentRunner] Compact failed:', error);
2817
- return false;
3025
+ const durationMs = Date.now() - startedAt;
3026
+ const message = error instanceof Error ? error.message : String(error);
3027
+ const result = { ok: false, code: 'sdk_error', message, durationMs, eventCount, lastEvent };
3028
+ logger.error(`[AgentRunner] Compact failed: session=${sessionId} agent=${agentSessionId} project=${projectPath} code=${result.code} events=${eventCount} durationMs=${durationMs} error=${message}`);
3029
+ return result;
2818
3030
  }
2819
3031
  }
2820
3032
  /**
@@ -2835,7 +3047,7 @@ export class AgentRunner {
2835
3047
  }
2836
3048
  if (cleared) {
2837
3049
  this.activeSessions.delete(sessionId);
2838
- this.onSessionIdUpdate?.(sessionId, '');
3050
+ await this.onSessionIdUpdate?.(sessionId, '');
2839
3051
  }
2840
3052
  else {
2841
3053
  logger.warn('[AgentRunner] Clear stream ended without session reset signal');
@@ -2852,16 +3064,76 @@ export class AgentRunner {
2852
3064
  }
2853
3065
  }
2854
3066
  async closeSession(sessionId) {
3067
+ const capturedAgentSessionId = this.activeSessions.get(sessionId);
3068
+ const capturedPermissionContext = this.permissionContexts.get(sessionId);
2855
3069
  this.permissionContexts.get(sessionId)?.pauseController?.cancel();
2856
3070
  const query = this.activeQueries.get(sessionId);
3071
+ const messageStream = this.activeMessageStreams.get(sessionId);
3072
+ const registeredStream = this.activeStreams.get(sessionId);
3073
+ const interrupt = this.interruptFns.get(sessionId);
3074
+ const done = this.streamDone.get(sessionId);
3075
+ const resolveDone = this.streamDoneResolvers.get(sessionId);
3076
+ let interruptError;
3077
+ if (interrupt) {
3078
+ try {
3079
+ await interrupt();
3080
+ }
3081
+ catch (error) {
3082
+ interruptError = error;
3083
+ }
3084
+ }
3085
+ messageStream?.end();
2857
3086
  if (query && typeof query.close === 'function')
2858
- query.close();
2859
- this.activeSessions.delete(sessionId);
2860
- this.activeStreams.delete(sessionId);
2861
- this.interruptFns.delete(sessionId);
2862
- this.activeQueries.delete(sessionId);
2863
- this.permissionContexts.delete(sessionId);
3087
+ await query.close();
3088
+ if (done && registeredStream) {
3089
+ let timer;
3090
+ try {
3091
+ await Promise.race([
3092
+ done,
3093
+ new Promise((_, reject) => {
3094
+ timer = setTimeout(() => reject(new Error(`Claude session did not become quiescent: ${sessionId}`)), 30_000);
3095
+ timer.unref?.();
3096
+ }),
3097
+ ]);
3098
+ }
3099
+ finally {
3100
+ if (timer)
3101
+ clearTimeout(timer);
3102
+ }
3103
+ }
3104
+ else if (done) {
3105
+ // runQuery resolved but ResponseEngine never registered/consumed the
3106
+ // wrapper. No consumer can reach its finally block, so close the exact
3107
+ // captured lifecycle explicitly.
3108
+ resolveDone?.();
3109
+ }
3110
+ const lifecycleReplaced = (query && this.activeQueries.get(sessionId) !== undefined && this.activeQueries.get(sessionId) !== query)
3111
+ || (messageStream && this.activeMessageStreams.get(sessionId) !== undefined && this.activeMessageStreams.get(sessionId) !== messageStream)
3112
+ || (registeredStream && this.activeStreams.get(sessionId) !== undefined && this.activeStreams.get(sessionId) !== registeredStream);
3113
+ if (lifecycleReplaced) {
3114
+ const error = new Error(`Claude session lifecycle changed during close: ${sessionId}`);
3115
+ error.code = 'BACKEND_RUNTIME_REPLACED';
3116
+ throw error;
3117
+ }
3118
+ if (this.activeQueries.get(sessionId) === query)
3119
+ this.activeQueries.delete(sessionId);
3120
+ if (this.activeMessageStreams.get(sessionId) === messageStream)
3121
+ this.activeMessageStreams.delete(sessionId);
3122
+ if (this.activeStreams.get(sessionId) === registeredStream)
3123
+ this.activeStreams.delete(sessionId);
3124
+ if (this.interruptFns.get(sessionId) === interrupt)
3125
+ this.interruptFns.delete(sessionId);
3126
+ if (this.streamDone.get(sessionId) === done)
3127
+ this.streamDone.delete(sessionId);
3128
+ if (this.streamDoneResolvers.get(sessionId) === resolveDone)
3129
+ this.streamDoneResolvers.delete(sessionId);
3130
+ if (this.activeSessions.get(sessionId) === capturedAgentSessionId)
3131
+ this.activeSessions.delete(sessionId);
3132
+ if (this.permissionContexts.get(sessionId) === capturedPermissionContext)
3133
+ this.permissionContexts.delete(sessionId);
2864
3134
  this.cleanupSandboxSettingsFiles(sessionId);
3135
+ if (interruptError)
3136
+ throw interruptError;
2865
3137
  }
2866
3138
  async dispose() {
2867
3139
  const interrupts = [...this.interruptFns.values()].map(async (interrupt) => {
@@ -2977,9 +3249,9 @@ export class ClaudeAgentPlugin {
2977
3249
  const syntheticConfig = { agents: { claude: override } };
2978
3250
  const anthropic = resolveAnthropicConfig(syntheticConfig, override);
2979
3251
  const merged = {
2980
- agents: { claude: { ...(override || {}), evolcoreAgentAid: agent.aid, evolcoreAgentConfig: agent.config } },
3252
+ agents: { claude: { ...(override || {}) } },
2981
3253
  };
2982
- const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, callbacks.onSessionIdUpdate, anthropic.baseUrl, merged);
3254
+ const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, callbacks.onSessionIdUpdate, anthropic.baseUrl, merged, { agentAid: agent.aid, agentConfig: agent.config });
2983
3255
  if (anthropic.effort) {
2984
3256
  agentRunner.setEffort(anthropic.effort);
2985
3257
  }