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
@@ -6,13 +6,16 @@ import { AssistantMessageEventStream } from '../../ecagent/dist/runtime/utils/ev
6
6
  import { DEFAULT_PERMISSION_MODE } from '../types.js';
7
7
  import { agentSessionsDir, resolveRoot } from '../paths.js';
8
8
  import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
9
- import { normalizePermissionMode } from '../core/permission/mode.js';
9
+ import { normalizeExecutionPermissionMode, normalizePermissionMode } from '../core/permission/mode.js';
10
10
  import { checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
11
11
  import { logger } from '../utils/logger.js';
12
12
  import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
13
13
  import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
14
14
  import { resolveEcagentConfig } from './baseagent.js';
15
15
  import { buildModelRequestHeaders } from './request-identity.js';
16
+ import { appendRequestPath } from './request-identity.js';
17
+ import { getManagedTaskTempDir } from '../cli/task-context.js';
18
+ import { hasTrustedFullAccessContext } from './runner-types.js';
16
19
  const PROVIDER_ID = 'evolcore-gateway';
17
20
  const DEFAULT_SYSTEM_PROMPT = `You are ecagent, the coding agent built into EvolCore.
18
21
  Work directly in the current project. Inspect relevant files before changing them, keep edits scoped, and verify the result.
@@ -248,7 +251,7 @@ function createOpenAiTransport(config) {
248
251
  const replacement = await options?.onPayload?.(payload, model);
249
252
  if (replacement !== undefined)
250
253
  payload = replacement;
251
- response = await fetch(`${model.baseUrl.replace(/\/+$/, '')}/chat/completions`, {
254
+ response = await fetch(appendRequestPath(model.baseUrl, 'chat/completions', config.queryParams), {
252
255
  method: 'POST',
253
256
  headers: {
254
257
  accept: 'text/event-stream',
@@ -459,14 +462,14 @@ function isWithin(root, candidate) {
459
462
  const relative = path.relative(root, candidate);
460
463
  return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
461
464
  }
462
- export function resolveEcagentToolPath(projectPath, candidate, forWrite = false, bootstrapAgentMdPath) {
465
+ export function resolveEcagentToolPath(projectPath, candidate, forWrite = false, bootstrapAgentMdPath, fullaccess = false) {
463
466
  const root = fs.realpathSync(projectPath);
464
467
  const resolved = path.resolve(root, candidate || '.');
465
468
  const allowedBootstrapFile = bootstrapAgentMdPath
466
469
  ? path.resolve(bootstrapAgentMdPath)
467
470
  : undefined;
468
471
  const isBootstrapAgentMd = allowedBootstrapFile === resolved;
469
- if (!isWithin(root, resolved) && !isBootstrapAgentMd) {
472
+ if (!fullaccess && !isWithin(root, resolved) && !isBootstrapAgentMd) {
470
473
  throw new Error(`Path is outside the project: ${candidate}`);
471
474
  }
472
475
  if (isBootstrapAgentMd) {
@@ -488,7 +491,7 @@ export function resolveEcagentToolPath(projectPath, candidate, forWrite = false,
488
491
  while (!fs.existsSync(cursor) && cursor !== path.dirname(cursor))
489
492
  cursor = path.dirname(cursor);
490
493
  const canonical = fs.realpathSync(cursor);
491
- if (!isWithin(root, canonical))
494
+ if (!fullaccess && !isWithin(root, canonical))
492
495
  throw new Error(`Path resolves outside the project: ${candidate}`);
493
496
  return resolved;
494
497
  }
@@ -541,7 +544,7 @@ async function walkFiles(root, signal) {
541
544
  function textResult(text, details = {}) {
542
545
  return { content: [{ type: 'text', text: truncate(text) }], details };
543
546
  }
544
- function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath) {
547
+ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullaccess = false) {
545
548
  const read = {
546
549
  name: 'Read', label: 'Read', description: 'Read a UTF-8 text file from the project.',
547
550
  parameters: Type.Object({
@@ -550,7 +553,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath) {
550
553
  limit: Type.Optional(Type.Number({ minimum: 1 })),
551
554
  }),
552
555
  async execute(_id, params, signal) {
553
- const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath);
556
+ const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath, fullaccess);
554
557
  const stat = await fs.promises.stat(file);
555
558
  if (stat.size > MAX_READ_BYTES)
556
559
  throw new Error(`File exceeds ${MAX_READ_BYTES} byte read limit`);
@@ -567,7 +570,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath) {
567
570
  name: 'Glob', label: 'Glob', description: 'Find project files matching a glob pattern.',
568
571
  parameters: Type.Object({ pattern: Type.String(), path: Type.Optional(Type.String()) }),
569
572
  async execute(_id, params, signal) {
570
- const root = resolveEcagentToolPath(projectPath, params.path ?? '.');
573
+ const root = resolveEcagentToolPath(projectPath, params.path ?? '.', false, undefined, fullaccess);
571
574
  const matcher = globRegex(params.pattern.replaceAll('\\', '/'));
572
575
  const files = (await walkFiles(root, signal))
573
576
  .map(file => path.relative(root, file).replaceAll(path.sep, '/'))
@@ -583,7 +586,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath) {
583
586
  case_insensitive: Type.Optional(Type.Boolean()), literal: Type.Optional(Type.Boolean()),
584
587
  }),
585
588
  async execute(_id, params, signal) {
586
- const root = resolveEcagentToolPath(projectPath, params.path ?? '.');
589
+ const root = resolveEcagentToolPath(projectPath, params.path ?? '.', false, undefined, fullaccess);
587
590
  const files = (await fs.promises.stat(root)).isFile() ? [root] : await walkFiles(root, signal);
588
591
  const fileMatcher = params.glob ? globRegex(params.glob.replaceAll('\\', '/')) : undefined;
589
592
  const flags = params.case_insensitive ? 'i' : '';
@@ -616,7 +619,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath) {
616
619
  name: 'Write', label: 'Write', description: 'Create or completely overwrite a project file.',
617
620
  parameters: Type.Object({ file_path: Type.String(), content: Type.String() }), executionMode: 'sequential',
618
621
  async execute(_id, params, signal) {
619
- const file = resolveEcagentToolPath(projectPath, params.file_path, true, bootstrapAgentMdPath);
622
+ const file = resolveEcagentToolPath(projectPath, params.file_path, true, bootstrapAgentMdPath, fullaccess);
620
623
  await fs.promises.mkdir(path.dirname(file), { recursive: true });
621
624
  await fs.promises.writeFile(file, params.content, { encoding: 'utf8', signal });
622
625
  return textResult(`Wrote ${Buffer.byteLength(params.content)} bytes to ${path.relative(projectPath, file)}`, { file });
@@ -629,7 +632,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath) {
629
632
  replace_all: Type.Optional(Type.Boolean()),
630
633
  }), executionMode: 'sequential',
631
634
  async execute(_id, params, signal) {
632
- const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath);
635
+ const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath, fullaccess);
633
636
  const value = await fs.promises.readFile(file, { encoding: 'utf8', signal });
634
637
  if (!params.old_string)
635
638
  throw new Error('old_string must not be empty');
@@ -685,39 +688,43 @@ function errorType(message) {
685
688
  export class EcagentRunner {
686
689
  name = 'ecagent';
687
690
  capabilities = {
691
+ fullaccess: true,
688
692
  clear: true, compact: true, fork: true, askUserQuestion: false, planApproval: false, fileRewind: 'unsupported',
689
693
  };
690
694
  config;
695
+ agentAid;
691
696
  onSessionIdUpdate;
692
697
  model;
693
698
  effort;
694
699
  permissionMode = DEFAULT_PERMISSION_MODE;
695
700
  runtimes = new Map();
696
701
  activeStreams = new Map();
697
- pendingInterrupts = new Set();
702
+ /** Queue identity prevents an old run's finally from consuming a new run's interrupt. */
703
+ pendingInterrupts = new Map();
698
704
  permissionContexts = new Map();
699
705
  permissionGateway;
700
706
  sendPromptFn;
701
707
  onCompactStart;
702
708
  modelCache;
703
- constructor(config, callbacks) {
709
+ constructor(config, callbacks, runtime) {
704
710
  this.config = {
705
711
  ...config,
706
712
  headers: buildModelRequestHeaders({
707
713
  baseagent: 'ecagent',
708
714
  baseUrl: config.baseUrl,
709
- agentAid: config.evolcoreAgentAid,
715
+ agentAid: runtime?.agentAid,
710
716
  configuredHeaders: config.headers,
711
717
  }),
712
718
  };
713
719
  this.model = config.model;
714
720
  this.effort = config.effort;
721
+ this.agentAid = runtime?.agentAid;
715
722
  this.onSessionIdUpdate = callbacks?.onSessionIdUpdate;
716
723
  }
717
724
  sessionsRoot() {
718
- if (!this.config.evolcoreAgentAid)
725
+ if (!this.agentAid)
719
726
  throw new Error('ecagent requires an EvolCore Agent AID');
720
- return agentSessionsDir(this.config.evolcoreAgentAid);
727
+ return agentSessionsDir(this.agentAid);
721
728
  }
722
729
  setModel(model) { this.model = model; }
723
730
  getModel() { return this.model; }
@@ -744,7 +751,7 @@ export class EcagentRunner {
744
751
  const stale = this.modelCache?.ids;
745
752
  try {
746
753
  const signal = AbortSignal.timeout(MODEL_LIST_TIMEOUT_MS);
747
- const response = await fetch(`${this.config.baseUrl.replace(/\/+$/, '')}/models`, {
754
+ const response = await fetch(appendRequestPath(this.config.baseUrl, 'models', this.config.queryParams), {
748
755
  headers: {
749
756
  authorization: `Bearer ${this.config.apiKey}`,
750
757
  ...this.config.headers,
@@ -769,20 +776,53 @@ export class EcagentRunner {
769
776
  }
770
777
  }
771
778
  registerStream(key, stream) { this.activeStreams.set(key, stream); }
772
- cleanupStream(key) { this.activeStreams.delete(key); }
779
+ cleanupStream(key) {
780
+ const stream = this.activeStreams.get(key);
781
+ if (!stream)
782
+ return;
783
+ this.activeStreams.delete(key);
784
+ if (this.pendingInterrupts.get(key) === stream)
785
+ this.pendingInterrupts.delete(key);
786
+ }
773
787
  hasActiveStream(key) { return this.activeStreams.has(key) || !!this.runtimes.get(key)?.harness; }
774
788
  updateSessionId(sessionId, agentSessionId) {
775
- if (!agentSessionId) {
776
- this.runtimes.delete(sessionId);
777
- }
778
- else {
779
- const runtime = this.runtimes.get(sessionId);
780
- if (runtime)
781
- runtime.agentSessionId = agentSessionId;
789
+ const previousRuntime = this.runtimes.get(sessionId);
790
+ const applyLocal = () => {
791
+ if (!agentSessionId) {
792
+ if (this.runtimes.get(sessionId) === previousRuntime)
793
+ this.runtimes.delete(sessionId);
794
+ }
795
+ else if (previousRuntime && this.runtimes.get(sessionId) === previousRuntime) {
796
+ previousRuntime.agentSessionId = agentSessionId;
797
+ }
798
+ };
799
+ if (!this.onSessionIdUpdate) {
800
+ applyLocal();
801
+ return;
782
802
  }
783
- void this.onSessionIdUpdate?.(sessionId, agentSessionId);
803
+ // A discovered non-empty ID is not published to the shared runtime until
804
+ // the authoritative Session activation accepts it. Empty callbacks are
805
+ // local detach notifications and are safe to apply after completion.
806
+ void Promise.resolve(this.onSessionIdUpdate(sessionId, agentSessionId)).then(result => {
807
+ if (result === undefined || result === 'activated' || result === 'already_active' || result === 'legacy_updated' || !agentSessionId) {
808
+ applyLocal();
809
+ }
810
+ }).catch(error => {
811
+ logger.warn(`[EcagentRunner] session binding callback failed: ${error instanceof Error ? error.message : String(error)}`);
812
+ if (!agentSessionId)
813
+ applyLocal();
814
+ });
784
815
  }
785
816
  async runQuery(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, _sessionManager, modelOverride, runtimeEnv = {}) {
817
+ // Keep managed tasks from silently inheriting the daemon-wide TMPDIR when
818
+ // their session-owned runtime directory is unavailable.
819
+ getManagedTaskTempDir(runtimeEnv);
820
+ const topicBinding = modelOverride?.backend?.kind === 'topic';
821
+ if (topicBinding && (!modelOverride?.turn || modelOverride.turn.sessionId !== sessionId || !this.onSessionIdUpdate)) {
822
+ const error = new Error('topic backend run requires an active TurnLease and binding callback');
823
+ error.code = 'TOPIC_TURN_LEASE_REQUIRED';
824
+ throw error;
825
+ }
786
826
  const queue = new AsyncEventQueue();
787
827
  this.activeStreams.set(sessionId, queue);
788
828
  void this.executeRun(queue, {
@@ -797,15 +837,19 @@ export class EcagentRunner {
797
837
  try {
798
838
  await fs.promises.mkdir(request.projectPath, { recursive: true });
799
839
  const previousRuntime = this.runtimes.get(request.sessionId);
800
- const runtime = await this.getOrCreateRuntime(request.sessionId, request.projectPath, request.initialAgentSessionId);
801
- if (!previousRuntime || request.initialAgentSessionId !== runtime.agentSessionId) {
840
+ const runtime = await this.getOrCreateRuntime(request.sessionId, request.projectPath, request.initialAgentSessionId, request.modelOverride);
841
+ if (previousRuntime !== runtime) {
802
842
  queue.push({ type: 'session_id', sessionId: runtime.agentSessionId });
803
843
  }
804
844
  const modelId = request.modelOverride?.model || this.model;
805
845
  const effort = request.modelOverride?.effortMode === 'model_default'
806
846
  ? undefined
807
847
  : request.modelOverride?.effort ?? this.effort;
808
- const permissionMode = normalizePermissionMode(request.modelOverride?.permissionMode || this.permissionMode).mode;
848
+ const permissionMode = normalizeExecutionPermissionMode(request.modelOverride?.permissionMode || this.permissionMode);
849
+ const fullaccess = permissionMode === 'fullaccess';
850
+ if (fullaccess && !hasTrustedFullAccessContext(this.permissionContexts.get(request.sessionId))) {
851
+ throw new Error('Ecagent fullaccess requires a trusted per-call execution authorization');
852
+ }
809
853
  const model = createModel(modelId, this.config.baseUrl);
810
854
  const nodeEnv = new NodeExecutionEnv({ cwd: request.projectPath, shellEnv: { ...process.env, ...request.runtimeEnv, EVOLCORE_SESSION_ID: request.sessionId, EVOLCORE_HOME: resolveRoot() } });
811
855
  const models = createModels();
@@ -828,7 +872,7 @@ export class EcagentRunner {
828
872
  models,
829
873
  model,
830
874
  thinkingLevel: normalizeThinkingLevel(effort),
831
- tools: createTools(nodeEnv, request.projectPath, request.runtimeEnv, request.modelOverride?.bootstrapAgentMdPath),
875
+ tools: createTools(nodeEnv, request.projectPath, request.runtimeEnv, request.modelOverride?.bootstrapAgentMdPath, fullaccess),
832
876
  systemPrompt: [DEFAULT_SYSTEM_PROMPT, request.systemPromptAppend].filter(Boolean).join('\n\n'),
833
877
  streamOptions: {
834
878
  timeoutMs: this.config.timeoutMs,
@@ -837,7 +881,8 @@ export class EcagentRunner {
837
881
  },
838
882
  });
839
883
  runtime.harness = harness;
840
- if (this.pendingInterrupts.delete(request.sessionId)) {
884
+ if (this.pendingInterrupts.get(request.sessionId) === queue) {
885
+ this.pendingInterrupts.delete(request.sessionId);
841
886
  await harness.abort();
842
887
  runtime.harness = undefined;
843
888
  throw new Error('ecagent run interrupted');
@@ -903,15 +948,28 @@ export class EcagentRunner {
903
948
  finally {
904
949
  if (!completeSent)
905
950
  queue.push({ type: 'complete', isError: true, terminalReason: 'error', durationMs: Date.now() - stats.startedAt });
906
- this.pendingInterrupts.delete(request.sessionId);
907
- this.activeStreams.delete(request.sessionId);
951
+ if (this.activeStreams.get(request.sessionId) === queue) {
952
+ this.activeStreams.delete(request.sessionId);
953
+ if (this.pendingInterrupts.get(request.sessionId) === queue) {
954
+ this.pendingInterrupts.delete(request.sessionId);
955
+ }
956
+ }
908
957
  queue.end();
909
958
  }
910
959
  }
911
- async getOrCreateRuntime(sessionId, projectPath, requestedId) {
960
+ async getOrCreateRuntime(sessionId, projectPath, requestedId, modelOverride) {
912
961
  const current = this.runtimes.get(sessionId);
913
- const targetId = requestedId || current?.agentSessionId;
914
- if (current && current.projectPath === projectPath && (!targetId || current.agentSessionId === targetId))
962
+ const topicBinding = modelOverride?.backend?.kind === 'topic';
963
+ const requestedTopicAgentSessionId = topicBinding
964
+ ? modelOverride?.backend?.agentSessionId ?? null
965
+ : undefined;
966
+ const targetId = topicBinding
967
+ ? (requestedTopicAgentSessionId || undefined)
968
+ : requestedId || current?.agentSessionId;
969
+ if (current
970
+ && current.projectPath === projectPath
971
+ && (!targetId || current.agentSessionId === targetId)
972
+ && (!topicBinding || (!!targetId && current.agentSessionId === targetId)))
915
973
  return current;
916
974
  const env = new NodeExecutionEnv({ cwd: projectPath });
917
975
  const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: this.sessionsRoot() });
@@ -919,21 +977,42 @@ export class EcagentRunner {
919
977
  if (targetId) {
920
978
  const metadata = (await repo.list({ cwd: projectPath })).find(item => item.id === targetId);
921
979
  if (!metadata) {
980
+ if (topicBinding) {
981
+ const error = new Error(`bound topic backend is unavailable: ${targetId}`);
982
+ error.code = 'TOPIC_BACKEND_UNAVAILABLE';
983
+ throw error;
984
+ }
922
985
  logger.warn(`[EcagentRunner] backing session ${targetId} was not found; creating a new session`);
923
986
  session = await repo.create({ cwd: projectPath });
924
987
  }
925
988
  else
926
989
  session = await repo.open(metadata);
927
990
  }
928
- else
929
- session = await repo.create({ cwd: projectPath, id: sessionId });
991
+ else {
992
+ // Topic UNBOUND is an explicit backend boundary. Let ecagent allocate a
993
+ // fresh provider ID; reusing the EvolCore Session ID would make every
994
+ // /renew cycle reopen the same provider session under a new filename.
995
+ session = topicBinding
996
+ ? await repo.create({ cwd: projectPath })
997
+ : await repo.create({ cwd: projectPath, id: sessionId });
998
+ }
930
999
  const metadata = await session.getMetadata();
931
1000
  const runtime = { agentSessionId: metadata.id, projectPath, repo, session };
932
- this.runtimes.set(sessionId, runtime);
933
- if (metadata.id !== targetId) {
934
- await this.onSessionIdUpdate?.(sessionId, metadata.id);
1001
+ if ((topicBinding && metadata.id !== requestedTopicAgentSessionId) || metadata.id !== targetId) {
1002
+ const turn = modelOverride?.turn;
1003
+ const result = this.onSessionIdUpdate
1004
+ ? (turn === undefined
1005
+ ? await this.onSessionIdUpdate(sessionId, metadata.id)
1006
+ : await this.onSessionIdUpdate(sessionId, metadata.id, { turn }))
1007
+ : 'legacy_updated';
1008
+ if (result !== undefined && result !== 'activated' && result !== 'already_active' && result !== 'legacy_updated') {
1009
+ const error = new Error(`backend discovery rejected: ${result}`);
1010
+ error.code = 'TOPIC_BACKEND_DISCOVERY_REJECTED';
1011
+ throw error;
1012
+ }
935
1013
  // The public event is emitted by the active query immediately after creation.
936
1014
  }
1015
+ this.runtimes.set(sessionId, runtime);
937
1016
  return runtime;
938
1017
  }
939
1018
  mapHarnessEvent(queue, event, stats, model, effort) {
@@ -987,7 +1066,12 @@ export class EcagentRunner {
987
1066
  }
988
1067
  async authorizeTool(sessionId, projectPath, mode, toolName, input) {
989
1068
  const permissionContext = this.permissionContexts.get(sessionId);
990
- const recordBlockedOperation = async (policyCode, operationInput = input) => {
1069
+ if (mode === 'fullaccess') {
1070
+ return hasTrustedFullAccessContext(permissionContext)
1071
+ ? {}
1072
+ : { block: true, reason: 'Untrusted fullaccess execution context' };
1073
+ }
1074
+ const recordBlockedOperation = async (policyCode, operationInput = input, matchedPath) => {
991
1075
  const summary = summarizeToolInputForAudit(toolName, operationInput).slice(0, 512);
992
1076
  auditToolPreflightDenial({
993
1077
  toolName,
@@ -996,12 +1080,14 @@ export class EcagentRunner {
996
1080
  summary,
997
1081
  sessionId,
998
1082
  agentAid: permissionContext?.selfAid,
1083
+ agentName: permissionContext?.agentName,
999
1084
  permissionMode: mode,
1000
1085
  channel: permissionContext?.channel,
1001
1086
  actorId: permissionContext?.userId,
1002
1087
  role: permissionContext?.role,
1003
1088
  selfAid: permissionContext?.selfAid,
1004
1089
  taskId: permissionContext?.taskId,
1090
+ matchedPath,
1005
1091
  });
1006
1092
  try {
1007
1093
  await permissionContext?.recordExecutionAnomaly?.({
@@ -1013,6 +1099,7 @@ export class EcagentRunner {
1013
1099
  policyCode,
1014
1100
  decisionSource: 'policy',
1015
1101
  agentAid: permissionContext?.selfAid,
1102
+ agentName: permissionContext?.agentName,
1016
1103
  permissionMode: mode,
1017
1104
  summary,
1018
1105
  effect: 'operation_skipped',
@@ -1029,6 +1116,7 @@ export class EcagentRunner {
1029
1116
  }
1030
1117
  const preflight = evaluateToolPreflight(toolName, input, {
1031
1118
  sessionId,
1119
+ managedTempDir: permissionContext?.managedTempDir,
1032
1120
  selfAid: permissionContext?.selfAid,
1033
1121
  channel: permissionContext?.channel,
1034
1122
  userId: permissionContext?.userId,
@@ -1039,7 +1127,7 @@ export class EcagentRunner {
1039
1127
  });
1040
1128
  if (preflight.behavior === 'deny') {
1041
1129
  if (preflight.policyCode) {
1042
- await recordBlockedOperation(preflight.policyCode, input);
1130
+ await recordBlockedOperation(preflight.policyCode, input, preflight.matchedPath);
1043
1131
  }
1044
1132
  return { block: true, reason: preflight.message };
1045
1133
  }
@@ -1048,7 +1136,7 @@ export class EcagentRunner {
1048
1136
  const checkedInput = preflight.input;
1049
1137
  if (mode === 'readonly') {
1050
1138
  const decision = checkReadonly(toolName, checkedInput, projectPath, {
1051
- sessionId, channel: permissionContext?.channel, peerId: permissionContext?.userId, role: permissionContext?.role,
1139
+ sessionId, managedTempDir: permissionContext?.managedTempDir, channel: permissionContext?.channel, peerId: permissionContext?.userId, role: permissionContext?.role,
1052
1140
  allowReadonlySourceDiagnostics: permissionContext?.allowReadonlySourceDiagnostics === true,
1053
1141
  });
1054
1142
  if (decision.behavior === 'deny') {
@@ -1077,8 +1165,9 @@ export class EcagentRunner {
1077
1165
  await harness.abort();
1078
1166
  return;
1079
1167
  }
1080
- if (this.activeStreams.has(sessionKey))
1081
- this.pendingInterrupts.add(sessionKey);
1168
+ const stream = this.activeStreams.get(sessionKey);
1169
+ if (stream)
1170
+ this.pendingInterrupts.set(sessionKey, stream);
1082
1171
  }
1083
1172
  injectUserMessage(sessionId, text) {
1084
1173
  const harness = this.runtimes.get(sessionId)?.harness;
@@ -1086,22 +1175,33 @@ export class EcagentRunner {
1086
1175
  void harness.steer(text).catch(error => logger.warn('[EcagentRunner] Failed to steer active run:', error));
1087
1176
  }
1088
1177
  async closeSession(sessionId) {
1178
+ const capturedPermissionContext = this.permissionContexts.get(sessionId);
1089
1179
  this.permissionContexts.get(sessionId)?.pauseController?.cancel();
1180
+ const runtime = this.runtimes.get(sessionId);
1181
+ const stream = this.activeStreams.get(sessionId);
1090
1182
  await this.interrupt(sessionId).catch(() => { });
1091
- this.runtimes.delete(sessionId);
1092
- this.activeStreams.delete(sessionId);
1093
- this.pendingInterrupts.delete(sessionId);
1094
- this.permissionContexts.delete(sessionId);
1183
+ if (runtime && this.runtimes.get(sessionId) === runtime)
1184
+ this.runtimes.delete(sessionId);
1185
+ if (stream && this.activeStreams.get(sessionId) === stream) {
1186
+ this.activeStreams.delete(sessionId);
1187
+ if (this.pendingInterrupts.get(sessionId) === stream)
1188
+ this.pendingInterrupts.delete(sessionId);
1189
+ }
1190
+ if (this.permissionContexts.get(sessionId) === capturedPermissionContext)
1191
+ this.permissionContexts.delete(sessionId);
1095
1192
  }
1096
1193
  async clearSession(sessionId, agentSessionId, projectPath) {
1097
1194
  try {
1195
+ const currentRuntime = this.runtimes.get(sessionId);
1098
1196
  await this.interrupt(sessionId);
1099
1197
  const env = new NodeExecutionEnv({ cwd: projectPath });
1100
1198
  const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: this.sessionsRoot() });
1101
1199
  const metadata = (await repo.list({ cwd: path.resolve(projectPath) })).find(item => item.id === agentSessionId);
1102
1200
  if (metadata)
1103
1201
  await repo.delete(metadata);
1104
- this.runtimes.delete(sessionId);
1202
+ if (currentRuntime?.agentSessionId === agentSessionId
1203
+ && this.runtimes.get(sessionId) === currentRuntime)
1204
+ this.runtimes.delete(sessionId);
1105
1205
  await this.onSessionIdUpdate?.(sessionId, '');
1106
1206
  return !!metadata;
1107
1207
  }
@@ -1110,11 +1210,12 @@ export class EcagentRunner {
1110
1210
  return false;
1111
1211
  }
1112
1212
  }
1113
- async compactSession(sessionId, agentSessionId, projectPath) {
1213
+ async compactSession(sessionId, agentSessionId, projectPath, modelOverride) {
1214
+ const startedAt = Date.now();
1114
1215
  try {
1115
1216
  if (this.activeStreams.has(sessionId) || this.runtimes.get(sessionId)?.harness) {
1116
1217
  logger.warn(`[EcagentRunner] compact skipped while session ${sessionId} is active`);
1117
- return false;
1218
+ return { ok: false, code: 'invalid_state', message: 'Session is active', durationMs: Date.now() - startedAt };
1118
1219
  }
1119
1220
  const cwd = path.resolve(projectPath);
1120
1221
  const cached = this.runtimes.get(sessionId);
@@ -1122,7 +1223,11 @@ export class EcagentRunner {
1122
1223
  ? cached
1123
1224
  : await this.openExistingRuntime(agentSessionId, cwd);
1124
1225
  this.onCompactStart?.(sessionId);
1125
- const model = createModel(this.model, this.config.baseUrl);
1226
+ const compactModel = modelOverride?.model || this.model;
1227
+ const compactEffort = modelOverride?.effortMode === 'model_default'
1228
+ ? undefined
1229
+ : modelOverride?.effort ?? this.effort;
1230
+ const model = createModel(compactModel, this.config.baseUrl);
1126
1231
  const env = new NodeExecutionEnv({ cwd: runtime.projectPath });
1127
1232
  const models = createModels();
1128
1233
  const transport = createOpenAiTransport(this.config);
@@ -1130,7 +1235,7 @@ export class EcagentRunner {
1130
1235
  id: PROVIDER_ID, auth: { apiKey: { name: 'EvolCore gateway', login: async () => ({ type: 'api_key', key: this.config.apiKey }), resolve: async () => ({ auth: { apiKey: this.config.apiKey } }) } },
1131
1236
  models: [model], api: transport,
1132
1237
  }));
1133
- const harness = new AgentHarness({ env, session: runtime.session, models, model, thinkingLevel: normalizeThinkingLevel(this.effort), tools: [] });
1238
+ const harness = new AgentHarness({ env, session: runtime.session, models, model, thinkingLevel: normalizeThinkingLevel(compactEffort), tools: [] });
1134
1239
  runtime.harness = harness;
1135
1240
  try {
1136
1241
  await harness.compact();
@@ -1138,16 +1243,22 @@ export class EcagentRunner {
1138
1243
  finally {
1139
1244
  runtime.harness = undefined;
1140
1245
  }
1141
- this.runtimes.set(sessionId, runtime);
1142
- return true;
1246
+ // Do not let an older compact operation overwrite a runtime that was
1247
+ // published by a later topic turn.
1248
+ if (this.runtimes.get(sessionId) === cached) {
1249
+ this.runtimes.set(sessionId, runtime);
1250
+ }
1251
+ return { ok: true, durationMs: Date.now() - startedAt };
1143
1252
  }
1144
1253
  catch (error) {
1145
- logger.error('[EcagentRunner] compact failed:', error);
1146
- return false;
1254
+ const message = error instanceof Error ? error.message : String(error);
1255
+ const code = /aborted/i.test(message) ? 'aborted' : /invalid_state/i.test(message) ? 'invalid_state' : 'sdk_error';
1256
+ logger.error(`[EcagentRunner] compact failed: session=${sessionId} agent=${agentSessionId} code=${code} durationMs=${Date.now() - startedAt} error=${message}`);
1257
+ return { ok: false, code, message, durationMs: Date.now() - startedAt };
1147
1258
  }
1148
1259
  }
1149
- compact(sessionId, agentSessionId, projectPath) {
1150
- return this.compactSession(sessionId, agentSessionId, projectPath);
1260
+ compact(sessionId, agentSessionId, projectPath, modelOverride) {
1261
+ return this.compactSession(sessionId, agentSessionId, projectPath, modelOverride);
1151
1262
  }
1152
1263
  resolveSessionFile(agentSessionId, projectPath) {
1153
1264
  const dir = path.join(this.sessionsRoot(), `--${path.resolve(projectPath).replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`);
@@ -1250,8 +1361,8 @@ export class EcagentAgentPlugin {
1250
1361
  const override = agent.config.baseagents?.ecagent;
1251
1362
  if (!override)
1252
1363
  return null;
1253
- const configured = { ...override, evolcoreAgentAid: agent.aid, evolcoreAgentConfig: agent.config };
1364
+ const configured = { ...override };
1254
1365
  const resolved = resolveEcagentConfig({ agents: { ecagent: configured } }, configured);
1255
- return { evolagentName: agent.name, baseagent: 'ecagent', agent: new EcagentRunner(resolved, callbacks) };
1366
+ return { evolagentName: agent.name, baseagent: 'ecagent', agent: new EcagentRunner(resolved, callbacks, { agentAid: agent.aid }) };
1256
1367
  }
1257
1368
  }