evolcore 0.0.20 → 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 (147) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +58 -9
  3. package/bin/codex-managed-hook.mjs +3 -0
  4. package/bin/install-codex-managed-hooks.mjs +3 -1
  5. package/dist/agents/baseagent.js +10 -6
  6. package/dist/agents/claude-runner.js +393 -108
  7. package/dist/agents/codex-app-server-client.js +41 -7
  8. package/dist/agents/codex-runner.js +1292 -220
  9. package/dist/agents/ecagent-runner.js +171 -61
  10. package/dist/agents/gemini-runner.js +130 -30
  11. package/dist/agents/request-identity.js +25 -0
  12. package/dist/agents/runner-types.js +19 -0
  13. package/dist/aun/aid/agentmd.js +59 -2
  14. package/dist/aun/aid/identity.js +4 -1
  15. package/dist/aun/aid/index.js +1 -1
  16. package/dist/aun/msg/group.js +72 -6
  17. package/dist/aun/msg/history.js +213 -36
  18. package/dist/aun/msg/managed-operation.js +58 -9
  19. package/dist/aun/msg/p2p.js +5 -0
  20. package/dist/aun/outbox.js +189 -80
  21. package/dist/aun/service-proxy.js +43 -25
  22. package/dist/channels/aun.js +618 -123
  23. package/dist/channels/daemon.js +6 -1
  24. package/dist/cli/agent-command.js +4 -3
  25. package/dist/cli/agent.js +66 -56
  26. package/dist/cli/aun-commands.js +177 -42
  27. package/dist/cli/command-log.js +10 -11
  28. package/dist/cli/contact.js +1 -0
  29. package/dist/cli/daemon-commands.js +98 -123
  30. package/dist/cli/init.js +27 -15
  31. package/dist/cli/task-context.js +50 -0
  32. package/dist/cli/trigger-command.js +14 -5
  33. package/dist/cli/watch-logs.js +10 -3
  34. package/dist/config/builtin-roles.js +1 -0
  35. package/dist/config/config-field-policy.js +19 -5
  36. package/dist/config/config-manager.js +167 -22
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/contact-operation-service.js +32 -1
  39. package/dist/config/contact-request-service.js +44 -0
  40. package/dist/config/daemon-services.js +186 -0
  41. package/dist/config/gateway-config.js +20 -9
  42. package/dist/config/role-service.js +54 -3
  43. package/dist/config/schema-migration.js +550 -0
  44. package/dist/config-store.js +151 -9
  45. package/dist/core/agent-application-service.js +279 -0
  46. package/dist/core/audit/log-integrity.js +102 -0
  47. package/dist/core/auth/agent-delegation.js +43 -1
  48. package/dist/core/auth/auth-gateway.js +41 -4
  49. package/dist/core/auth/authorization-audit.js +216 -8
  50. package/dist/core/auth/operation-authorizer.js +41 -1
  51. package/dist/core/auth/operation-catalog.js +9 -1
  52. package/dist/core/bootstrap-messages.js +8 -0
  53. package/dist/core/bootstrap-service.js +99 -27
  54. package/dist/core/causation/aun-association.js +7 -4
  55. package/dist/core/command/agent-control.js +56 -16
  56. package/dist/core/command/command-handler.js +311 -44
  57. package/dist/core/command/connect-menu.js +3 -4
  58. package/dist/core/command/group-menu.js +5 -7
  59. package/dist/core/command/menu-handler.js +288 -80
  60. package/dist/core/command/menu-protocol.js +1 -1
  61. package/dist/core/command/role-menu.js +21 -11
  62. package/dist/core/command/slash-gate.js +85 -18
  63. package/dist/core/command/slash-handler.js +377 -36
  64. package/dist/core/data-migration.js +11 -1
  65. package/dist/core/event-catalog.js +37 -0
  66. package/dist/core/evolagent.js +4 -0
  67. package/dist/core/handoff/dispatcher.js +4 -0
  68. package/dist/core/handoff/runtime.js +33 -3
  69. package/dist/core/handoff/store.js +32 -9
  70. package/dist/core/inference/text-inference.js +7 -15
  71. package/dist/core/message/im-renderer.js +90 -87
  72. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  73. package/dist/core/message/message-bridge.js +184 -12
  74. package/dist/core/message/message-log.js +47 -7
  75. package/dist/core/message/message-queue.js +227 -16
  76. package/dist/core/message/message-utils.js +12 -5
  77. package/dist/core/message/response-engine.js +658 -109
  78. package/dist/core/message/send-receipt.js +1 -0
  79. package/dist/core/message/stream-debouncer.js +9 -2
  80. package/dist/core/model/model-catalog.js +23 -15
  81. package/dist/core/model/model-diagnostics.js +28 -10
  82. package/dist/core/permission/approval-gateway.js +180 -6
  83. package/dist/core/permission/ec-command-parser.js +627 -69
  84. package/dist/core/permission/mode.js +18 -3
  85. package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
  86. package/dist/core/permission/readonly-shell-query.js +263 -9
  87. package/dist/core/permission/sandbox-runtime.js +159 -1
  88. package/dist/core/permission/tool-error-code.js +12 -0
  89. package/dist/core/permission/tool-policy.js +618 -23
  90. package/dist/core/session/session-fs-store.js +154 -5
  91. package/dist/core/session/session-manager.js +329 -30
  92. package/dist/core/session/session-renew.js +37 -13
  93. package/dist/core/session/session-turn-coordinator.js +16 -5
  94. package/dist/eck/kit-renderer.js +1 -1
  95. package/dist/index.js +316 -50
  96. package/dist/ipc.js +459 -29
  97. package/dist/paths.js +82 -7
  98. package/dist/response-system/context-builder.js +1 -7
  99. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  100. package/dist/stats/price-resolver.js +4 -0
  101. package/dist/trigger/anomaly-store.js +1 -0
  102. package/dist/trigger/feedback.js +70 -7
  103. package/dist/trigger/history.js +79 -4
  104. package/dist/trigger/legacy-session-history.js +2 -2
  105. package/dist/trigger/parser.js +13 -3
  106. package/dist/trigger/scheduler.js +20 -3
  107. package/dist/trigger/validation.js +6 -1
  108. package/dist/utils/atomic-write.js +27 -0
  109. package/dist/utils/ecweb-utils.js +16 -2
  110. package/dist/utils/error-utils.js +4 -1
  111. package/dist/utils/logger.js +30 -6
  112. package/dist/utils/process-tree-stats.js +24 -4
  113. package/dist/utils/process-tree-worker.js +31 -0
  114. package/dist/utils/project-path.js +1 -2
  115. package/dist/utils/tool-summary.js +59 -0
  116. package/dist/utils/windows-shell-trust.js +201 -0
  117. package/kits/docs/INDEX.md +1 -1
  118. package/kits/docs/evolcore/INDEX.md +3 -3
  119. package/kits/docs/evolcore/agent-create.md +146 -0
  120. package/kits/docs/evolcore/agent.md +6 -0
  121. package/kits/docs/evolcore/contact.md +7 -1
  122. package/kits/docs/evolcore/group-collaboration.md +251 -0
  123. package/kits/docs/evolcore/group-rules.md +1 -19
  124. package/kits/docs/evolcore/group.md +3 -1
  125. package/kits/docs/evolcore/msg.md +16 -0
  126. package/kits/docs/evolcore/trigger.md +6 -3
  127. package/kits/docs/prompt-loading-architecture.md +6 -0
  128. package/kits/eck_message_manifest.json +6 -6
  129. package/kits/schemas/_meta.json +7 -4
  130. package/kits/schemas/agent-config.schema.11.json +13 -0
  131. package/kits/schemas/agent-config.schema.12.json +427 -0
  132. package/kits/schemas/daemon.schema.5.json +0 -1
  133. package/kits/schemas/daemon.schema.6.json +131 -0
  134. package/kits/schemas/defaults.schema.5.json +15 -3
  135. package/kits/schemas/migrations/README.md +3 -1
  136. package/kits/schemas/relation-config.schema.8.json +13 -0
  137. package/kits/schemas/role-config.schema.1.json +1 -2
  138. package/kits/schemas/single-session.schema.3.json +32 -0
  139. package/kits/templates/message-fragments/item.md +1 -1
  140. package/kits/templates/roles/admin.json +1 -0
  141. package/kits/templates/roles/member.json +1 -0
  142. package/kits/templates/roles/visitor.json +1 -0
  143. package/kits/templates/system-fragments/bootstrap.md +2 -1
  144. package/kits/templates/system-fragments/commands.md +2 -2
  145. package/package.json +6 -3
  146. package/skills/eclink/SKILL.md +2 -0
  147. package/dist/config/aun-gateway-config.js +0 -2
@@ -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;
@@ -721,6 +719,20 @@ export class AgentRunner {
721
719
  ...(evolcoreSessionId ? { EVOLCORE_SESSION_ID: evolcoreSessionId } : {}),
722
720
  ...(runtimeEnv ?? {}),
723
721
  }, { lockExecutableResolution: permissionMode === 'readonly' });
722
+ const trustedRootFullAccess = permissionMode === 'fullaccess'
723
+ && typeof process.getuid === 'function'
724
+ && process.getuid() === 0
725
+ && typeof evolcoreSessionId === 'string'
726
+ && hasTrustedFullAccessContext(this.permissionContexts.get(evolcoreSessionId));
727
+ // Claude Code refuses --dangerously-skip-permissions as root unless its
728
+ // caller asserts that the execution is intentionally sandbox-managed.
729
+ // EvolCore fullaccess is an explicit daemon-owner-authorized host run, so
730
+ // scope this assertion to that single trusted call. Never inherit a
731
+ // process-wide IS_SANDBOX into ordinary or untrusted Claude sessions.
732
+ if (trustedRootFullAccess)
733
+ env.IS_SANDBOX = '1';
734
+ else
735
+ delete env.IS_SANDBOX;
724
736
  const bubblewrapPath = resolveBubblewrapPath();
725
737
  return bubblewrapPath
726
738
  ? prependExecutableDirectory(env, bubblewrapPath)
@@ -744,11 +756,10 @@ export class AgentRunner {
744
756
  return entry?.cache;
745
757
  }
746
758
  async resolveCapabilityRunOptions(projectPath) {
747
- const claudeConfig = this.config?.agents?.claude;
748
- let agentConfig = claudeConfig?.evolcoreAgentConfig;
749
- if (claudeConfig?.evolcoreAgentAid) {
759
+ let agentConfig = this.agentConfig;
760
+ if (this.agentAid) {
750
761
  try {
751
- agentConfig = resolveEffective({ self: claudeConfig.evolcoreAgentAid }, { cache: true, expand: true });
762
+ agentConfig = resolveEffective({ self: this.agentAid }, { cache: true, expand: true });
752
763
  }
753
764
  catch { }
754
765
  }
@@ -826,14 +837,16 @@ export class AgentRunner {
826
837
  this.permissionContexts.set(sessionId, context);
827
838
  }
828
839
  toSdkPermissionMode(mode) {
840
+ if (mode === 'fullaccess')
841
+ return 'bypassPermissions';
829
842
  const normalized = normalizePermissionMode(mode ?? this.permissionMode);
830
843
  // Public permission policy is enforced by PreToolUse. Keeping the SDK in
831
844
  // default mode preserves canUseTool reachability for request mode.
832
845
  return normalized.workflow === 'plan' ? 'plan' : 'default';
833
846
  }
834
847
  // ── Compactable 接口 ──
835
- async compact(sessionId, agentSessionId, projectPath) {
836
- return this.compactSession(sessionId, agentSessionId, projectPath);
848
+ async compact(sessionId, agentSessionId, projectPath, modelOverride) {
849
+ return this.compactSession(sessionId, agentSessionId, projectPath, modelOverride);
837
850
  }
838
851
  syncFromUserSettings() {
839
852
  try {
@@ -972,7 +985,7 @@ export class AgentRunner {
972
985
  : '回复 /ask 1,或 /ask <自定义内容>';
973
986
  const fallbackText = `💬 ${q.header || q.question}\n${q.header ? q.question + '\n' : ''}${optionLines}\n\n${answerHint}`;
974
987
  const result = await sendInteractionPayload(permCtx.adapter, envelope, interaction, fallbackText, permCtx.replyContext);
975
- cardSent = !!result;
988
+ cardSent = isInteractionSendAccepted(result);
976
989
  }
977
990
  catch (err) {
978
991
  logger.warn(`[AgentRunner] AskUserQuestion card send failed for q${i}:`, err);
@@ -1393,7 +1406,7 @@ export class AgentRunner {
1393
1406
  });
1394
1407
  const fallbackText = '📋 计划审批:AI 已完成规划,等待审批。\n回复 /ask 1 批准 / /ask 2 拒绝';
1395
1408
  const result = await sendInteractionPayload(permCtx.adapter, envelope, interaction, fallbackText, permCtx.replyContext);
1396
- cardSent = !!result;
1409
+ cardSent = isInteractionSendAccepted(result);
1397
1410
  }
1398
1411
  catch (err) {
1399
1412
  logger.warn('[AgentRunner] ExitPlanMode card send failed:', err);
@@ -1441,7 +1454,7 @@ export class AgentRunner {
1441
1454
  * SDK 原始事件 → 标准 AgentEvent 转换
1442
1455
  * 所有 SDK 特有的事件类型引用封装在此方法内
1443
1456
  */
1444
- async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false, sandboxSettingsFile, permissionMode) {
1457
+ async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false, sandboxSettingsFile, permissionMode, turn) {
1445
1458
  let lastSessionId;
1446
1459
  let hasTurnActivity = false;
1447
1460
  let ignoredPreTurnResult = false;
@@ -1458,6 +1471,7 @@ export class AgentRunner {
1458
1471
  let turnCount = 0;
1459
1472
  const seenMessageIds = new Set();
1460
1473
  let lastModelCall;
1474
+ let lastAssistantWasSynthetic = false;
1461
1475
  let lastAssistantUuid;
1462
1476
  // 流式收集各次大模型调用(fallback:SDK iterations 为空时使用)
1463
1477
  const collectedCalls = [];
@@ -1466,8 +1480,15 @@ export class AgentRunner {
1466
1480
  // 提取 session_id(任意 SDK 事件都可能携带)
1467
1481
  if (event.session_id && event.session_id !== lastSessionId) {
1468
1482
  lastSessionId = event.session_id;
1469
- this.updateSessionId(sessionId, event.session_id);
1470
- yield { type: 'session_id', sessionId: event.session_id };
1483
+ if (await this.acceptDiscoveredSessionId(sessionId, event.session_id, turn)) {
1484
+ yield { type: 'session_id', sessionId: event.session_id };
1485
+ }
1486
+ else {
1487
+ logger.warn(`[AgentRunner] Rejected stale backend discovery: sessionId=${sessionId} backend=${event.session_id} turn=${turn?.turnId ?? 'legacy'}`);
1488
+ const error = new Error('backend discovery rejected by active turn lease');
1489
+ error.code = 'TOPIC_BACKEND_DISCOVERY_REJECTED';
1490
+ throw error;
1491
+ }
1471
1492
  }
1472
1493
  if (event.type === 'user' && event.uuid === inputId) {
1473
1494
  hasTurnActivity = true;
@@ -1563,7 +1584,9 @@ export class AgentRunner {
1563
1584
  const streamEvent = event.event;
1564
1585
  if (streamEvent?.type === 'message_start' && streamEvent.message?.usage) {
1565
1586
  hasTurnActivity = true;
1587
+ lastAssistantWasSynthetic = streamEvent.message.model === '<synthetic>';
1566
1588
  lastModelCall = {
1589
+ messageId: streamEvent.message.id,
1567
1590
  uuid: event.uuid,
1568
1591
  model: streamEvent.message.model,
1569
1592
  tokenUsage: streamEvent.message.usage,
@@ -1573,6 +1596,7 @@ export class AgentRunner {
1573
1596
  call_index: collectedCalls.length,
1574
1597
  model: streamEvent.message.model ?? callModel ?? this.model,
1575
1598
  request_id: event.request_id,
1599
+ message_id: streamEvent.message.id,
1576
1600
  tokenUsage: { ...streamEvent.message.usage },
1577
1601
  });
1578
1602
  }
@@ -1625,27 +1649,40 @@ export class AgentRunner {
1625
1649
  yield { type: 'state_changed', state: event.state };
1626
1650
  }
1627
1651
  // 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) {
1652
+ if (event.type === 'assistant') {
1653
+ const isSyntheticAssistant = event.message?.model === '<synthetic>'
1654
+ || (typeof event.error === 'string' && event.isApiErrorMessage === true);
1655
+ lastAssistantWasSynthetic = isSyntheticAssistant;
1656
+ if (event.message && (event.message.usage || isSyntheticAssistant)) {
1657
+ // A message id is stable across the partial stream and the final
1658
+ // assistant event. Never merge a synthetic/API-error assistant into
1659
+ // the preceding real call, even if an upstream adapter reuses an id
1660
+ // or omits the usage object on the error frame.
1661
+ const sameModelCall = !isSyntheticAssistant
1662
+ && typeof event.message.id === 'string'
1663
+ && event.message.id === lastModelCall?.messageId;
1638
1664
  lastModelCall = {
1639
- ...lastModelCall,
1665
+ ...(sameModelCall ? lastModelCall : undefined),
1640
1666
  messageId: event.message.id,
1667
+ uuid: event.uuid,
1641
1668
  requestId: event.request_id,
1642
1669
  model: event.message.model,
1643
- tokenUsage: {
1670
+ tokenUsage: sameModelCall ? {
1644
1671
  ...event.message.usage,
1645
1672
  ...(lastModelCall?.tokenUsage ?? {}),
1646
- },
1673
+ } : { ...(event.message.usage ?? {}) },
1647
1674
  };
1648
1675
  }
1676
+ if (!event.message?.content)
1677
+ continue;
1678
+ hasTurnActivity = true;
1679
+ lastAssistantUuid = event.uuid ?? lastAssistantUuid;
1680
+ const msgId = event.message.id;
1681
+ if (!msgId || !seenMessageIds.has(msgId)) {
1682
+ if (msgId)
1683
+ seenMessageIds.add(msgId);
1684
+ turnCount++;
1685
+ }
1649
1686
  // 统计本轮 base agent 全部输出字符数(text + tool_use input)
1650
1687
  let turnOutputChars = 0;
1651
1688
  for (const content of event.message.content) {
@@ -1720,6 +1757,18 @@ export class AgentRunner {
1720
1757
  // 非 Claude(DeepSeek/OpenAI 兼容):cache_read 是服务端 KV cache 不占上下文窗口,
1721
1758
  // input_tokens 本身就是完整的上下文输入量。
1722
1759
  const u = event.usage;
1760
+ // Claude Code represents transport/API failures as a fresh
1761
+ // `<synthetic>` assistant message. Its usage describes the failed
1762
+ // terminal request (normally zero); event.usage remains the aggregate
1763
+ // of earlier successful calls in the query and must not be presented
1764
+ // as the failed request's context usage.
1765
+ const isSyntheticError = event.is_error === true
1766
+ && (lastAssistantWasSynthetic || lastModelCall?.model === '<synthetic>');
1767
+ const syntheticUsage = lastModelCall?.tokenUsage ?? {
1768
+ input_tokens: 0,
1769
+ output_tokens: 0,
1770
+ };
1771
+ const terminalUsage = isSyntheticError ? syntheticUsage : u;
1723
1772
  const effectiveModel = callModel ?? this.model;
1724
1773
  const isClaudeModel = isClaudeContextUsageModel(effectiveModel);
1725
1774
  const totalTokens = contextTokensForUsage(u, !!isClaudeModel);
@@ -1762,7 +1811,11 @@ export class AgentRunner {
1762
1811
  // 组装 modelCalls:优先 SDK iterations,fallback 流式收集,兜底降级单行。
1763
1812
  const callModel_ = callModel ?? this.model;
1764
1813
  let modelCalls;
1765
- const iterArr = Array.isArray(u?.iterations) && u.iterations.length > 0 ? u.iterations : null;
1814
+ const iterArr = !isSyntheticError
1815
+ && Array.isArray(u?.iterations)
1816
+ && u.iterations.length > 0
1817
+ ? u.iterations
1818
+ : null;
1766
1819
  if (iterArr) {
1767
1820
  modelCalls = iterArr.map((it, i) => ({
1768
1821
  call_index: i, model: callModel_, tokenUsage: it, contextUsage: contextUsageForCall(it),
@@ -1774,16 +1827,19 @@ export class AgentRunner {
1774
1827
  contextUsage: contextUsageForCall(call.tokenUsage),
1775
1828
  }));
1776
1829
  }
1777
- else if (u) {
1830
+ else if (u && !isSyntheticError) {
1778
1831
  // 降级:无逐次数据,写一条累计行
1779
1832
  modelCalls = [{ call_index: 0, model: callModel_, tokenUsage: u, contextUsage: contextUsageForCall(u), degraded: true }];
1780
1833
  }
1781
1834
  // `event.usage` is aggregate query usage when the SDK made multiple
1782
1835
  // 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;
1836
+ // current request. For normal results aggregate usage remains available
1837
+ // in tokenUsage; synthetic errors use their own terminal usage instead.
1838
+ const contextUsage = isSyntheticError
1839
+ ? lastModelCall?.contextUsage
1840
+ : lastModelCall?.contextUsage
1841
+ ?? modelCalls?.at(-1)?.contextUsage
1842
+ ?? aggregateContextUsage;
1787
1843
  const completeEvent = {
1788
1844
  type: 'complete',
1789
1845
  result: cleanResult,
@@ -1796,7 +1852,7 @@ export class AgentRunner {
1796
1852
  terminalReason: event.terminal_reason,
1797
1853
  sessionTitle: event.session_title,
1798
1854
  numTurns: event.num_turns,
1799
- tokenUsage: event.usage,
1855
+ tokenUsage: terminalUsage,
1800
1856
  contextUsage,
1801
1857
  lastModelCall,
1802
1858
  modelCalls,
@@ -1871,6 +1927,7 @@ export class AgentRunner {
1871
1927
  policyCode: 'claude_startup_arguments_too_long',
1872
1928
  decisionSource: 'infrastructure',
1873
1929
  agentAid: this.permissionContexts.get(sessionId)?.selfAid,
1930
+ agentName: this.permissionContexts.get(sessionId)?.agentName,
1874
1931
  summary: detail.message.slice(0, 512),
1875
1932
  effect: 'operation_skipped',
1876
1933
  });
@@ -1904,6 +1961,9 @@ export class AgentRunner {
1904
1961
  }
1905
1962
  }
1906
1963
  async runQuery(sessionId, prompt, projectPath, initialClaudeSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
1964
+ // Validate the daemon-injected session namespace before the SDK inherits
1965
+ // the process environment for any temporary state.
1966
+ getManagedTaskTempDir(runtimeEnv);
1907
1967
  // 记录当前 evolcore session ID,用于 Agent ctl 环境变量注入
1908
1968
  // 同步用户级配置到内存
1909
1969
  this.syncFromUserSettings();
@@ -1922,8 +1982,22 @@ export class AgentRunner {
1922
1982
  }
1923
1983
  ensureDir(projectPath);
1924
1984
  ensureDir(path.join(projectPath, '.claude'));
1925
- // 优先使用传入的 agentSessionId(从数据库恢复),否则使用内存中的
1926
- let agentSessionId = initialClaudeSessionId || this.activeSessions.get(sessionId);
1985
+ const topicBinding = modelOverride?.backend?.kind === 'topic';
1986
+ if (topicBinding && (!modelOverride?.turn || modelOverride.turn.sessionId !== sessionId || !this.onSessionIdUpdate)) {
1987
+ const error = new Error('topic backend run requires an active TurnLease and binding callback');
1988
+ error.code = 'TOPIC_TURN_LEASE_REQUIRED';
1989
+ throw error;
1990
+ }
1991
+ const topicAgentSessionId = topicBinding ? modelOverride?.backend?.agentSessionId ?? null : null;
1992
+ const cachedAgentSessionId = topicBinding
1993
+ ? (topicAgentSessionId && this.activeSessions.get(sessionId) === topicAgentSessionId
1994
+ ? topicAgentSessionId
1995
+ : undefined)
1996
+ : this.activeSessions.get(sessionId);
1997
+ let agentSessionId = initialClaudeSessionId || cachedAgentSessionId;
1998
+ if (topicBinding && topicAgentSessionId !== agentSessionId) {
1999
+ agentSessionId = topicAgentSessionId || undefined;
2000
+ }
1927
2001
  // 验证会话文件是否存在且有效(仅在有 agentSessionId 时)
1928
2002
  if (agentSessionId) {
1929
2003
  const homeDir = os.homedir();
@@ -1954,11 +2028,18 @@ export class AgentRunner {
1954
2028
  }
1955
2029
  }
1956
2030
  if (!isValid) {
2031
+ if (topicBinding) {
2032
+ const error = new Error(`bound topic backend is unavailable: ${agentSessionId}`);
2033
+ error.code = 'TOPIC_BACKEND_UNAVAILABLE';
2034
+ throw error;
2035
+ }
1957
2036
  logger.warn(`[AgentRunner] Invalid session file, starting new session`);
1958
2037
  agentSessionId = undefined;
1959
2038
  this.activeSessions.delete(sessionId);
1960
2039
  if (this.onSessionIdUpdate) {
1961
- this.onSessionIdUpdate(sessionId, '');
2040
+ void Promise.resolve(this.onSessionIdUpdate(sessionId, '')).catch(error => {
2041
+ logger.debug(`[AgentRunner] ignored empty session binding callback failure: ${error instanceof Error ? error.message : String(error)}`);
2042
+ });
1962
2043
  }
1963
2044
  }
1964
2045
  }
@@ -1973,9 +2054,11 @@ export class AgentRunner {
1973
2054
  // 缺省回落 agent 级 this.permissionMode。作为 per-call 入参(hook/canUseTool 闭包捕获),
1974
2055
  // 不写实例字段,多对端并发互不污染(与 model/effort 同构)。
1975
2056
  const requestedPermissionMode = modelOverride?.permissionMode || this.permissionMode;
1976
- const normalizedPermission = normalizePermissionMode(requestedPermissionMode);
1977
- const callPermissionMode = normalizedPermission.mode;
2057
+ const callPermissionMode = normalizeExecutionPermissionMode(requestedPermissionMode);
1978
2058
  const runPermissionContext = this.permissionContexts.get(sessionId);
2059
+ if (callPermissionMode === 'fullaccess' && !hasTrustedFullAccessContext(runPermissionContext)) {
2060
+ throw new Error('Claude fullaccess requires a trusted per-call execution authorization');
2061
+ }
1979
2062
  const executionSandbox = resolvePhaseOneExecutionSandbox(callPermissionMode, runPermissionContext?.role);
1980
2063
  const inputId = modelOverride?.turn?.inputId ?? crypto.randomUUID();
1981
2064
  if (executionSandbox.state !== 'off') {
@@ -1990,6 +2073,7 @@ export class AgentRunner {
1990
2073
  reason: detail.message.slice(0, 512),
1991
2074
  sessionId,
1992
2075
  agentAid: context?.selfAid,
2076
+ agentName: context?.agentName,
1993
2077
  permissionMode: callPermissionMode,
1994
2078
  role: context?.role,
1995
2079
  callId: inputId,
@@ -2008,6 +2092,7 @@ export class AgentRunner {
2008
2092
  policyCode: SANDBOX_INITIALIZATION_FAILED,
2009
2093
  decisionSource: 'infrastructure',
2010
2094
  agentAid: context?.selfAid,
2095
+ agentName: context?.agentName,
2011
2096
  permissionMode: callPermissionMode,
2012
2097
  summary: detail.message.slice(0, 512),
2013
2098
  effect: 'operation_skipped',
@@ -2054,6 +2139,7 @@ export class AgentRunner {
2054
2139
  summary,
2055
2140
  sessionId,
2056
2141
  agentAid: ctx?.selfAid,
2142
+ agentName: ctx?.agentName,
2057
2143
  permissionMode: callPermissionMode,
2058
2144
  channel: ctx?.channel,
2059
2145
  actorId: ctx?.userId,
@@ -2074,6 +2160,7 @@ export class AgentRunner {
2074
2160
  policyCode: code,
2075
2161
  decisionSource: 'policy',
2076
2162
  agentAid: ctx?.selfAid,
2163
+ agentName: ctx?.agentName,
2077
2164
  permissionMode: callPermissionMode,
2078
2165
  summary,
2079
2166
  effect: 'operation_skipped',
@@ -2122,6 +2209,14 @@ export class AgentRunner {
2122
2209
  delete toolInput.pages;
2123
2210
  }
2124
2211
  let updatedInput = toolInput === originalInput ? undefined : toolInput;
2212
+ if (callPermissionMode === 'fullaccess') {
2213
+ if (!hasTrustedFullAccessContext(this.permissionContexts.get(sessionId))) {
2214
+ return hookDecision('deny', 'fullaccess execution authorization is no longer active', updatedInput);
2215
+ }
2216
+ return hookDecision('allow', undefined, toolName === 'Bash'
2217
+ ? { ...toolInput, dangerouslyDisableSandbox: true }
2218
+ : updatedInput);
2219
+ }
2125
2220
  const preparedSafeOutputCommand = toolName === 'Bash' && typeof toolInput.command === 'string'
2126
2221
  ? toolInput.command
2127
2222
  : undefined;
@@ -2143,6 +2238,7 @@ export class AgentRunner {
2143
2238
  : undefined;
2144
2239
  const preflight = evaluateToolPreflight(toolName, toolInput, {
2145
2240
  sessionId,
2241
+ managedTempDir: permCtx?.managedTempDir,
2146
2242
  selfAid: permCtx?.selfAid,
2147
2243
  channel: permCtx?.channel,
2148
2244
  userId: permCtx?.userId,
@@ -2179,6 +2275,7 @@ export class AgentRunner {
2179
2275
  const permCtx = this.permissionContexts.get(sessionId);
2180
2276
  const readonlyContext = {
2181
2277
  sessionId,
2278
+ managedTempDir: permCtx?.managedTempDir,
2182
2279
  channel: permCtx?.channel,
2183
2280
  peerId: permCtx?.userId,
2184
2281
  role: permCtx?.role,
@@ -2250,6 +2347,20 @@ export class AgentRunner {
2250
2347
  if (toolName === 'ExitPlanMode') {
2251
2348
  return await this.handleExitPlanMode(sessionId, input, options);
2252
2349
  }
2350
+ if (callPermissionMode === 'fullaccess') {
2351
+ if (!hasTrustedFullAccessContext(this.permissionContexts.get(sessionId))) {
2352
+ return {
2353
+ behavior: 'deny',
2354
+ message: 'fullaccess execution authorization is no longer active',
2355
+ decisionClassification: 'user_reject',
2356
+ };
2357
+ }
2358
+ return {
2359
+ behavior: 'allow',
2360
+ updatedInput: toolName === 'Bash' ? { ...input, dangerouslyDisableSandbox: true } : input,
2361
+ decisionClassification: 'user_permanent',
2362
+ };
2363
+ }
2253
2364
  if (toolName === 'Bash' && 'dangerouslyDisableSandbox' in input) {
2254
2365
  input = { ...input };
2255
2366
  delete input.dangerouslyDisableSandbox;
@@ -2267,6 +2378,7 @@ export class AgentRunner {
2267
2378
  const permCtx = this.permissionContexts.get(sessionId);
2268
2379
  const preflight = evaluateToolPreflight(toolName, input, {
2269
2380
  sessionId,
2381
+ managedTempDir: permCtx?.managedTempDir,
2270
2382
  selfAid: permCtx?.selfAid,
2271
2383
  channel: permCtx?.channel,
2272
2384
  userId: permCtx?.userId,
@@ -2339,6 +2451,7 @@ export class AgentRunner {
2339
2451
  const permCtx = this.permissionContexts.get(sessionId);
2340
2452
  const readonlyContext = {
2341
2453
  sessionId,
2454
+ managedTempDir: permCtx?.managedTempDir,
2342
2455
  channel: permCtx?.channel,
2343
2456
  peerId: permCtx?.userId,
2344
2457
  role: permCtx?.role,
@@ -2403,7 +2516,9 @@ export class AgentRunner {
2403
2516
  };
2404
2517
  };
2405
2518
  const useSettingSources = this.config?.agents?.claude?.useSettingSources !== false;
2406
- const settingSources = useSettingSources ? ['project', 'user'] : [];
2519
+ const settingSources = callPermissionMode === 'fullaccess'
2520
+ ? []
2521
+ : useSettingSources ? ['project', 'user'] : [];
2407
2522
  const enableSummaries = this.config?.agents?.claude?.agentProgressSummaries !== false;
2408
2523
  const excludeDynamic = this.config?.agents?.claude?.excludeDynamicSections === true;
2409
2524
  // 公共 options(新旧模式共用)
@@ -2426,7 +2541,9 @@ export class AgentRunner {
2426
2541
  }
2427
2542
  const sdkModel = resolveSdkModel(callModel, this.baseUrl);
2428
2543
  const capabilityOptions = await this.resolveCapabilityRunOptions(projectPath);
2429
- const managedSettings = buildClaudeManagedLockdownSettings(capabilityOptions);
2544
+ const managedSettings = callPermissionMode === 'fullaccess'
2545
+ ? undefined
2546
+ : buildClaudeManagedLockdownSettings(capabilityOptions);
2430
2547
  const sandboxOptions = executionSandbox.state === 'off'
2431
2548
  ? { enabled: false }
2432
2549
  : await (async () => {
@@ -2466,9 +2583,11 @@ export class AgentRunner {
2466
2583
  };
2467
2584
  })();
2468
2585
  if (executionSandbox.state === 'off') {
2469
- logger.info(process.platform === 'linux'
2470
- ? '[ClaudeSandbox] task sandbox disabled for authenticated owner bypass; outer H-class guard remains active'
2471
- : '[ClaudeSandbox] disabled for authenticated owner bypass task');
2586
+ logger.info(callPermissionMode === 'fullaccess'
2587
+ ? '[ClaudeSandbox] disabled for trusted per-call fullaccess execution'
2588
+ : process.platform === 'linux'
2589
+ ? '[ClaudeSandbox] task sandbox disabled for authenticated owner bypass; outer H-class guard remains active'
2590
+ : '[ClaudeSandbox] disabled for authenticated owner bypass task');
2472
2591
  }
2473
2592
  const handleClaudeStderr = (msg) => {
2474
2593
  const trimmed = msg.trim();
@@ -2494,16 +2613,17 @@ export class AgentRunner {
2494
2613
  model: sdkModel,
2495
2614
  ...capabilityOptions,
2496
2615
  strictMcpConfig: false,
2497
- managedSettings,
2616
+ ...(managedSettings ? { managedSettings } : {}),
2498
2617
  ...(callEffort ? { effort: callEffort } : {}),
2499
2618
  ...(this.claudeExecutablePath ? { pathToClaudeCodeExecutable: this.claudeExecutablePath } : {}),
2500
- ...(executionSandbox.state === 'off' && process.platform === 'linux' ? {
2619
+ ...(executionSandbox.state === 'off' && callPermissionMode !== 'fullaccess' && process.platform === 'linux' ? {
2501
2620
  spawnClaudeCodeProcess: (options) => spawnClaudeWithHClassGuard(options, resolvePaths().root, handleClaudeStderr),
2502
2621
  } : {}),
2503
2622
  autoCompactWindow: autoCompactWindowForModel(sdkModel),
2504
2623
  advisorModel: 'haiku',
2505
2624
  canUseTool: canUseToolCallback,
2506
2625
  permissionMode: sdkPermissionMode,
2626
+ ...(callPermissionMode === 'fullaccess' ? { allowDangerouslySkipPermissions: true } : {}),
2507
2627
  sandbox: sandboxOptions,
2508
2628
  persistSession: true,
2509
2629
  includePartialMessages: true,
@@ -2625,9 +2745,14 @@ export class AgentRunner {
2625
2745
  const currentSession = await sessionManager.getSessionById?.(sessionId);
2626
2746
  if (currentSession?.metadata?.resumeAt) {
2627
2747
  resumeAt = currentSession.metadata.resumeAt;
2628
- const newMeta = { ...currentSession.metadata };
2629
- delete newMeta.resumeAt;
2630
- await sessionManager.updateSession(sessionId, { metadata: newMeta });
2748
+ if (typeof sessionManager.patchSessionMetadata === 'function') {
2749
+ await sessionManager.patchSessionMetadata(sessionId, { resumeAt: undefined });
2750
+ }
2751
+ else {
2752
+ const newMeta = { ...currentSession.metadata };
2753
+ delete newMeta.resumeAt;
2754
+ await sessionManager.updateSession(sessionId, { metadata: newMeta });
2755
+ }
2631
2756
  logger.info(`[AgentRunner] Consuming resumeAt: ${resumeAt}`);
2632
2757
  }
2633
2758
  }
@@ -2639,10 +2764,14 @@ export class AgentRunner {
2639
2764
  const msgStream = new MessageStream();
2640
2765
  if (images && images.length > 0) {
2641
2766
  logger.info('[AgentRunner] Creating query with images:', images.length, 'first image size:', images[0]?.data?.length ?? 0);
2642
- logger.debug('[AgentRunner] Skipping resume for image message to avoid history conflict');
2643
2767
  msgStream.push(prompt, images, inputId);
2644
2768
  try {
2645
- sdkStream = createQuery(msgStream);
2769
+ // Topic bindings are authoritative for every input shape. Starting
2770
+ // an unresumed Claude query for a bound topic would discover a second
2771
+ // backend and then fail activation with a binding conflict.
2772
+ sdkStream = topicBinding
2773
+ ? createQuery(msgStream, agentSessionId, resumeAt)
2774
+ : createQuery(msgStream);
2646
2775
  }
2647
2776
  catch (error) {
2648
2777
  this.cleanupSandboxSettingsFile(sessionId, sandboxSettingsFile);
@@ -2676,14 +2805,16 @@ export class AgentRunner {
2676
2805
  }
2677
2806
  this.activeMessageStreams.set(sessionId, msgStream);
2678
2807
  this.activeQueries.set(sessionId, sdkStream);
2679
- const donePromise = new Promise(resolve => this.streamDoneResolvers.set(sessionId, resolve));
2808
+ let resolveDone;
2809
+ const donePromise = new Promise(resolve => { resolveDone = resolve; });
2810
+ this.streamDoneResolvers.set(sessionId, resolveDone);
2680
2811
  this.streamDone.set(sessionId, donePromise);
2681
2812
  // 保存 interrupt 能力(不写 activeStreams,由 registerStream 管理活跃状态)
2682
2813
  if ('interrupt' in sdkStream && typeof sdkStream.interrupt === 'function') {
2683
2814
  this.interruptFns.set(sessionId, () => sdkStream.interrupt());
2684
2815
  }
2685
2816
  // 返回标准 AgentEvent 流(重试由 MessageProcessor 层负责)
2686
- const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId, sandboxSettingsFile, callPermissionMode);
2817
+ const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId, sandboxSettingsFile, callPermissionMode, modelOverride?.turn);
2687
2818
  const self = this;
2688
2819
  return (async function* () {
2689
2820
  try {
@@ -2691,10 +2822,15 @@ export class AgentRunner {
2691
2822
  }
2692
2823
  finally {
2693
2824
  self.cleanupSandboxSettingsFile(sessionId, sandboxSettingsFile);
2694
- self.streamDoneResolvers.get(sessionId)?.();
2695
- self.streamDoneResolvers.delete(sessionId);
2696
- self.streamDone.delete(sessionId);
2697
- self.activeQueries.delete(sessionId);
2825
+ resolveDone();
2826
+ if (self.streamDoneResolvers.get(sessionId) === resolveDone)
2827
+ self.streamDoneResolvers.delete(sessionId);
2828
+ if (self.streamDone.get(sessionId) === donePromise)
2829
+ self.streamDone.delete(sessionId);
2830
+ if (self.activeQueries.get(sessionId) === sdkStream)
2831
+ self.activeQueries.delete(sessionId);
2832
+ if (self.activeMessageStreams.get(sessionId) === msgStream)
2833
+ self.activeMessageStreams.delete(sessionId);
2698
2834
  }
2699
2835
  })();
2700
2836
  }
@@ -2762,19 +2898,64 @@ export class AgentRunner {
2762
2898
  injectUserMessage(sessionId, text) {
2763
2899
  this.activeMessageStreams.get(sessionId)?.push(text);
2764
2900
  }
2901
+ async acceptDiscoveredSessionId(sessionId, agentSessionId, turn) {
2902
+ const result = this.onSessionIdUpdate
2903
+ ? (turn === undefined
2904
+ ? await this.onSessionIdUpdate(sessionId, agentSessionId)
2905
+ : await this.onSessionIdUpdate(sessionId, agentSessionId, { turn }))
2906
+ : 'legacy_updated';
2907
+ const accepted = result === undefined || result === 'activated' || result === 'already_active' || result === 'legacy_updated';
2908
+ if (accepted) {
2909
+ this.activeSessions.set(sessionId, agentSessionId);
2910
+ }
2911
+ return accepted;
2912
+ }
2765
2913
  updateSessionId(sessionId, agentSessionId) {
2766
2914
  logger.info(`[AgentRunner] updateSessionId called: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
2767
- this.activeSessions.set(sessionId, agentSessionId);
2768
- if (this.onSessionIdUpdate) {
2769
- this.onSessionIdUpdate(sessionId, agentSessionId);
2915
+ const previousAgentSessionId = this.activeSessions.get(sessionId);
2916
+ const canApply = () => {
2917
+ const current = this.activeSessions.get(sessionId);
2918
+ return current === previousAgentSessionId || (previousAgentSessionId === undefined && current === undefined);
2919
+ };
2920
+ if (!this.onSessionIdUpdate) {
2921
+ if (!canApply())
2922
+ return;
2923
+ if (agentSessionId)
2924
+ this.activeSessions.set(sessionId, agentSessionId);
2925
+ else if (this.activeSessions.get(sessionId) === previousAgentSessionId)
2926
+ this.activeSessions.delete(sessionId);
2927
+ return;
2770
2928
  }
2929
+ void Promise.resolve(this.onSessionIdUpdate(sessionId, agentSessionId)).then(result => {
2930
+ if (!canApply())
2931
+ return;
2932
+ if (result === undefined || result === 'activated' || result === 'already_active' || result === 'legacy_updated') {
2933
+ if (agentSessionId)
2934
+ this.activeSessions.set(sessionId, agentSessionId);
2935
+ else if (this.activeSessions.get(sessionId) === previousAgentSessionId)
2936
+ this.activeSessions.delete(sessionId);
2937
+ }
2938
+ else if (!agentSessionId) {
2939
+ if (this.activeSessions.get(sessionId) === previousAgentSessionId)
2940
+ this.activeSessions.delete(sessionId);
2941
+ }
2942
+ }).catch(error => {
2943
+ logger.warn(`[AgentRunner] session binding callback failed: ${error instanceof Error ? error.message : String(error)}`);
2944
+ if (!agentSessionId && canApply() && this.activeSessions.get(sessionId) === previousAgentSessionId) {
2945
+ this.activeSessions.delete(sessionId);
2946
+ }
2947
+ });
2771
2948
  }
2772
- runSessionCommand(prompt, agentSessionId, projectPath) {
2949
+ runSessionCommand(prompt, agentSessionId, projectPath, modelOverride) {
2950
+ const callModel = modelOverride?.model || this.model;
2951
+ const callEffort = (modelOverride?.effortMode === 'model_default'
2952
+ ? undefined
2953
+ : modelOverride?.effort ?? this.effort);
2773
2954
  return query({
2774
2955
  prompt,
2775
2956
  options: {
2776
2957
  cwd: projectPath,
2777
- model: resolveSdkModel(this.model, this.baseUrl),
2958
+ model: resolveSdkModel(callModel, this.baseUrl),
2778
2959
  resume: agentSessionId,
2779
2960
  maxTurns: 1,
2780
2961
  tools: [],
@@ -2783,6 +2964,7 @@ export class AgentRunner {
2783
2964
  strictMcpConfig: true,
2784
2965
  settingSources: [],
2785
2966
  managedSettings: buildClaudeManagedLockdownSettings(),
2967
+ ...(callEffort ? { effort: callEffort } : {}),
2786
2968
  permissionMode: this.toSdkPermissionMode(),
2787
2969
  env: this.getAgentEnv()
2788
2970
  }
@@ -2791,31 +2973,74 @@ export class AgentRunner {
2791
2973
  /**
2792
2974
  * 主动压缩会话上下文
2793
2975
  */
2794
- async compactSession(sessionId, agentSessionId, projectPath) {
2976
+ async compactSession(sessionId, agentSessionId, projectPath, modelOverride) {
2977
+ const startedAt = Date.now();
2978
+ let eventCount = 0;
2979
+ let lastEvent;
2795
2980
  try {
2796
- logger.info(`[AgentRunner] Compacting session: ${agentSessionId}`);
2797
- const stream = this.runSessionCommand('/compact', agentSessionId, projectPath);
2981
+ logger.info(`[AgentRunner] Compacting session: session=${sessionId} agent=${agentSessionId} project=${projectPath}`);
2982
+ const stream = this.runSessionCommand('/compact', agentSessionId, projectPath, modelOverride);
2798
2983
  this.activeStreams.set(sessionId, stream);
2799
2984
  try {
2800
- let receivedBoundary = false;
2985
+ let boundary;
2986
+ let compactStatus;
2801
2987
  for await (const event of stream) {
2802
- if (event.type === 'system' && event.subtype === 'compact_boundary') {
2803
- logger.info(`[AgentRunner] Compact completed, pre_tokens: ${event.compact_metadata?.pre_tokens}`);
2804
- receivedBoundary = true;
2988
+ eventCount += 1;
2989
+ lastEvent = { type: event?.type, subtype: event?.subtype };
2990
+ if (event.type === 'system' && event.subtype === 'status' && event.compact_result) {
2991
+ compactStatus = {
2992
+ result: event.compact_result,
2993
+ error: event.compact_error,
2994
+ };
2805
2995
  }
2996
+ if (event.type === 'system' && event.subtype === 'compact_boundary')
2997
+ boundary = event;
2998
+ }
2999
+ const durationMs = Date.now() - startedAt;
3000
+ if (compactStatus?.result === 'failed') {
3001
+ const result = {
3002
+ ok: false,
3003
+ code: 'sdk_error',
3004
+ message: compactStatus.error || 'Claude SDK reported compact failure',
3005
+ durationMs,
3006
+ eventCount,
3007
+ lastEvent,
3008
+ };
3009
+ logger.warn(`[AgentRunner] Compact failed: session=${sessionId} agent=${agentSessionId} code=${result.code} events=${eventCount} durationMs=${durationMs} message=${result.message}`);
3010
+ return result;
2806
3011
  }
2807
- if (!receivedBoundary) {
2808
- logger.warn(`[AgentRunner] Compact stream ended without compact_boundary event`);
3012
+ if (!boundary) {
3013
+ const result = {
3014
+ ok: false,
3015
+ code: 'missing_boundary',
3016
+ message: 'Compact stream ended without compact_boundary event',
3017
+ durationMs,
3018
+ eventCount,
3019
+ lastEvent,
3020
+ };
3021
+ logger.warn(`[AgentRunner] Compact failed: session=${sessionId} agent=${agentSessionId} code=${result.code} events=${eventCount} durationMs=${durationMs}`);
3022
+ return result;
2809
3023
  }
2810
- return receivedBoundary;
3024
+ const result = {
3025
+ ok: true,
3026
+ durationMs,
3027
+ eventCount,
3028
+ preTokens: boundary.compact_metadata?.pre_tokens,
3029
+ postTokens: boundary.compact_metadata?.post_tokens,
3030
+ };
3031
+ logger.info(`[AgentRunner] Compact completed: session=${sessionId} agent=${agentSessionId} events=${eventCount} durationMs=${durationMs} pre_tokens=${result.preTokens ?? 'unknown'} post_tokens=${result.postTokens ?? 'unknown'}`);
3032
+ return result;
2811
3033
  }
2812
3034
  finally {
2813
3035
  this.activeStreams.delete(sessionId);
2814
3036
  }
2815
3037
  }
2816
3038
  catch (error) {
2817
- logger.error('[AgentRunner] Compact failed:', error);
2818
- return false;
3039
+ const durationMs = Date.now() - startedAt;
3040
+ const message = error instanceof Error ? error.message : String(error);
3041
+ const result = { ok: false, code: 'sdk_error', message, durationMs, eventCount, lastEvent };
3042
+ logger.error(`[AgentRunner] Compact failed: session=${sessionId} agent=${agentSessionId} project=${projectPath} code=${result.code} events=${eventCount} durationMs=${durationMs} error=${message}`);
3043
+ return result;
2819
3044
  }
2820
3045
  }
2821
3046
  /**
@@ -2836,7 +3061,7 @@ export class AgentRunner {
2836
3061
  }
2837
3062
  if (cleared) {
2838
3063
  this.activeSessions.delete(sessionId);
2839
- this.onSessionIdUpdate?.(sessionId, '');
3064
+ await this.onSessionIdUpdate?.(sessionId, '');
2840
3065
  }
2841
3066
  else {
2842
3067
  logger.warn('[AgentRunner] Clear stream ended without session reset signal');
@@ -2853,16 +3078,76 @@ export class AgentRunner {
2853
3078
  }
2854
3079
  }
2855
3080
  async closeSession(sessionId) {
3081
+ const capturedAgentSessionId = this.activeSessions.get(sessionId);
3082
+ const capturedPermissionContext = this.permissionContexts.get(sessionId);
2856
3083
  this.permissionContexts.get(sessionId)?.pauseController?.cancel();
2857
3084
  const query = this.activeQueries.get(sessionId);
3085
+ const messageStream = this.activeMessageStreams.get(sessionId);
3086
+ const registeredStream = this.activeStreams.get(sessionId);
3087
+ const interrupt = this.interruptFns.get(sessionId);
3088
+ const done = this.streamDone.get(sessionId);
3089
+ const resolveDone = this.streamDoneResolvers.get(sessionId);
3090
+ let interruptError;
3091
+ if (interrupt) {
3092
+ try {
3093
+ await interrupt();
3094
+ }
3095
+ catch (error) {
3096
+ interruptError = error;
3097
+ }
3098
+ }
3099
+ messageStream?.end();
2858
3100
  if (query && typeof query.close === 'function')
2859
- query.close();
2860
- this.activeSessions.delete(sessionId);
2861
- this.activeStreams.delete(sessionId);
2862
- this.interruptFns.delete(sessionId);
2863
- this.activeQueries.delete(sessionId);
2864
- this.permissionContexts.delete(sessionId);
3101
+ await query.close();
3102
+ if (done && registeredStream) {
3103
+ let timer;
3104
+ try {
3105
+ await Promise.race([
3106
+ done,
3107
+ new Promise((_, reject) => {
3108
+ timer = setTimeout(() => reject(new Error(`Claude session did not become quiescent: ${sessionId}`)), 30_000);
3109
+ timer.unref?.();
3110
+ }),
3111
+ ]);
3112
+ }
3113
+ finally {
3114
+ if (timer)
3115
+ clearTimeout(timer);
3116
+ }
3117
+ }
3118
+ else if (done) {
3119
+ // runQuery resolved but ResponseEngine never registered/consumed the
3120
+ // wrapper. No consumer can reach its finally block, so close the exact
3121
+ // captured lifecycle explicitly.
3122
+ resolveDone?.();
3123
+ }
3124
+ const lifecycleReplaced = (query && this.activeQueries.get(sessionId) !== undefined && this.activeQueries.get(sessionId) !== query)
3125
+ || (messageStream && this.activeMessageStreams.get(sessionId) !== undefined && this.activeMessageStreams.get(sessionId) !== messageStream)
3126
+ || (registeredStream && this.activeStreams.get(sessionId) !== undefined && this.activeStreams.get(sessionId) !== registeredStream);
3127
+ if (lifecycleReplaced) {
3128
+ const error = new Error(`Claude session lifecycle changed during close: ${sessionId}`);
3129
+ error.code = 'BACKEND_RUNTIME_REPLACED';
3130
+ throw error;
3131
+ }
3132
+ if (this.activeQueries.get(sessionId) === query)
3133
+ this.activeQueries.delete(sessionId);
3134
+ if (this.activeMessageStreams.get(sessionId) === messageStream)
3135
+ this.activeMessageStreams.delete(sessionId);
3136
+ if (this.activeStreams.get(sessionId) === registeredStream)
3137
+ this.activeStreams.delete(sessionId);
3138
+ if (this.interruptFns.get(sessionId) === interrupt)
3139
+ this.interruptFns.delete(sessionId);
3140
+ if (this.streamDone.get(sessionId) === done)
3141
+ this.streamDone.delete(sessionId);
3142
+ if (this.streamDoneResolvers.get(sessionId) === resolveDone)
3143
+ this.streamDoneResolvers.delete(sessionId);
3144
+ if (this.activeSessions.get(sessionId) === capturedAgentSessionId)
3145
+ this.activeSessions.delete(sessionId);
3146
+ if (this.permissionContexts.get(sessionId) === capturedPermissionContext)
3147
+ this.permissionContexts.delete(sessionId);
2865
3148
  this.cleanupSandboxSettingsFiles(sessionId);
3149
+ if (interruptError)
3150
+ throw interruptError;
2866
3151
  }
2867
3152
  async dispose() {
2868
3153
  const interrupts = [...this.interruptFns.values()].map(async (interrupt) => {
@@ -2978,9 +3263,9 @@ export class ClaudeAgentPlugin {
2978
3263
  const syntheticConfig = { agents: { claude: override } };
2979
3264
  const anthropic = resolveAnthropicConfig(syntheticConfig, override);
2980
3265
  const merged = {
2981
- agents: { claude: { ...(override || {}), evolcoreAgentAid: agent.aid, evolcoreAgentConfig: agent.config } },
3266
+ agents: { claude: { ...(override || {}) } },
2982
3267
  };
2983
- const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, callbacks.onSessionIdUpdate, anthropic.baseUrl, merged);
3268
+ const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, callbacks.onSessionIdUpdate, anthropic.baseUrl, merged, { agentAid: agent.aid, agentConfig: agent.config });
2984
3269
  if (anthropic.effort) {
2985
3270
  agentRunner.setEffort(anthropic.effort);
2986
3271
  }