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
@@ -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,6 +1066,11 @@ export class EcagentRunner {
987
1066
  }
988
1067
  async authorizeTool(sessionId, projectPath, mode, toolName, input) {
989
1068
  const permissionContext = this.permissionContexts.get(sessionId);
1069
+ if (mode === 'fullaccess') {
1070
+ return hasTrustedFullAccessContext(permissionContext)
1071
+ ? {}
1072
+ : { block: true, reason: 'Untrusted fullaccess execution context' };
1073
+ }
990
1074
  const recordBlockedOperation = async (policyCode, operationInput = input, matchedPath) => {
991
1075
  const summary = summarizeToolInputForAudit(toolName, operationInput).slice(0, 512);
992
1076
  auditToolPreflightDenial({
@@ -996,6 +1080,7 @@ 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,
@@ -1014,6 +1099,7 @@ export class EcagentRunner {
1014
1099
  policyCode,
1015
1100
  decisionSource: 'policy',
1016
1101
  agentAid: permissionContext?.selfAid,
1102
+ agentName: permissionContext?.agentName,
1017
1103
  permissionMode: mode,
1018
1104
  summary,
1019
1105
  effect: 'operation_skipped',
@@ -1030,6 +1116,7 @@ export class EcagentRunner {
1030
1116
  }
1031
1117
  const preflight = evaluateToolPreflight(toolName, input, {
1032
1118
  sessionId,
1119
+ managedTempDir: permissionContext?.managedTempDir,
1033
1120
  selfAid: permissionContext?.selfAid,
1034
1121
  channel: permissionContext?.channel,
1035
1122
  userId: permissionContext?.userId,
@@ -1049,7 +1136,7 @@ export class EcagentRunner {
1049
1136
  const checkedInput = preflight.input;
1050
1137
  if (mode === 'readonly') {
1051
1138
  const decision = checkReadonly(toolName, checkedInput, projectPath, {
1052
- sessionId, channel: permissionContext?.channel, peerId: permissionContext?.userId, role: permissionContext?.role,
1139
+ sessionId, managedTempDir: permissionContext?.managedTempDir, channel: permissionContext?.channel, peerId: permissionContext?.userId, role: permissionContext?.role,
1053
1140
  allowReadonlySourceDiagnostics: permissionContext?.allowReadonlySourceDiagnostics === true,
1054
1141
  });
1055
1142
  if (decision.behavior === 'deny') {
@@ -1078,8 +1165,9 @@ export class EcagentRunner {
1078
1165
  await harness.abort();
1079
1166
  return;
1080
1167
  }
1081
- if (this.activeStreams.has(sessionKey))
1082
- this.pendingInterrupts.add(sessionKey);
1168
+ const stream = this.activeStreams.get(sessionKey);
1169
+ if (stream)
1170
+ this.pendingInterrupts.set(sessionKey, stream);
1083
1171
  }
1084
1172
  injectUserMessage(sessionId, text) {
1085
1173
  const harness = this.runtimes.get(sessionId)?.harness;
@@ -1087,22 +1175,33 @@ export class EcagentRunner {
1087
1175
  void harness.steer(text).catch(error => logger.warn('[EcagentRunner] Failed to steer active run:', error));
1088
1176
  }
1089
1177
  async closeSession(sessionId) {
1178
+ const capturedPermissionContext = this.permissionContexts.get(sessionId);
1090
1179
  this.permissionContexts.get(sessionId)?.pauseController?.cancel();
1180
+ const runtime = this.runtimes.get(sessionId);
1181
+ const stream = this.activeStreams.get(sessionId);
1091
1182
  await this.interrupt(sessionId).catch(() => { });
1092
- this.runtimes.delete(sessionId);
1093
- this.activeStreams.delete(sessionId);
1094
- this.pendingInterrupts.delete(sessionId);
1095
- 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);
1096
1192
  }
1097
1193
  async clearSession(sessionId, agentSessionId, projectPath) {
1098
1194
  try {
1195
+ const currentRuntime = this.runtimes.get(sessionId);
1099
1196
  await this.interrupt(sessionId);
1100
1197
  const env = new NodeExecutionEnv({ cwd: projectPath });
1101
1198
  const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: this.sessionsRoot() });
1102
1199
  const metadata = (await repo.list({ cwd: path.resolve(projectPath) })).find(item => item.id === agentSessionId);
1103
1200
  if (metadata)
1104
1201
  await repo.delete(metadata);
1105
- this.runtimes.delete(sessionId);
1202
+ if (currentRuntime?.agentSessionId === agentSessionId
1203
+ && this.runtimes.get(sessionId) === currentRuntime)
1204
+ this.runtimes.delete(sessionId);
1106
1205
  await this.onSessionIdUpdate?.(sessionId, '');
1107
1206
  return !!metadata;
1108
1207
  }
@@ -1111,11 +1210,12 @@ export class EcagentRunner {
1111
1210
  return false;
1112
1211
  }
1113
1212
  }
1114
- async compactSession(sessionId, agentSessionId, projectPath) {
1213
+ async compactSession(sessionId, agentSessionId, projectPath, modelOverride) {
1214
+ const startedAt = Date.now();
1115
1215
  try {
1116
1216
  if (this.activeStreams.has(sessionId) || this.runtimes.get(sessionId)?.harness) {
1117
1217
  logger.warn(`[EcagentRunner] compact skipped while session ${sessionId} is active`);
1118
- return false;
1218
+ return { ok: false, code: 'invalid_state', message: 'Session is active', durationMs: Date.now() - startedAt };
1119
1219
  }
1120
1220
  const cwd = path.resolve(projectPath);
1121
1221
  const cached = this.runtimes.get(sessionId);
@@ -1123,7 +1223,11 @@ export class EcagentRunner {
1123
1223
  ? cached
1124
1224
  : await this.openExistingRuntime(agentSessionId, cwd);
1125
1225
  this.onCompactStart?.(sessionId);
1126
- 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);
1127
1231
  const env = new NodeExecutionEnv({ cwd: runtime.projectPath });
1128
1232
  const models = createModels();
1129
1233
  const transport = createOpenAiTransport(this.config);
@@ -1131,7 +1235,7 @@ export class EcagentRunner {
1131
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 } }) } },
1132
1236
  models: [model], api: transport,
1133
1237
  }));
1134
- 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: [] });
1135
1239
  runtime.harness = harness;
1136
1240
  try {
1137
1241
  await harness.compact();
@@ -1139,16 +1243,22 @@ export class EcagentRunner {
1139
1243
  finally {
1140
1244
  runtime.harness = undefined;
1141
1245
  }
1142
- this.runtimes.set(sessionId, runtime);
1143
- 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 };
1144
1252
  }
1145
1253
  catch (error) {
1146
- logger.error('[EcagentRunner] compact failed:', error);
1147
- 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 };
1148
1258
  }
1149
1259
  }
1150
- compact(sessionId, agentSessionId, projectPath) {
1151
- return this.compactSession(sessionId, agentSessionId, projectPath);
1260
+ compact(sessionId, agentSessionId, projectPath, modelOverride) {
1261
+ return this.compactSession(sessionId, agentSessionId, projectPath, modelOverride);
1152
1262
  }
1153
1263
  resolveSessionFile(agentSessionId, projectPath) {
1154
1264
  const dir = path.join(this.sessionsRoot(), `--${path.resolve(projectPath).replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`);
@@ -1251,8 +1361,8 @@ export class EcagentAgentPlugin {
1251
1361
  const override = agent.config.baseagents?.ecagent;
1252
1362
  if (!override)
1253
1363
  return null;
1254
- const configured = { ...override, evolcoreAgentAid: agent.aid, evolcoreAgentConfig: agent.config };
1364
+ const configured = { ...override };
1255
1365
  const resolved = resolveEcagentConfig({ agents: { ecagent: configured } }, configured);
1256
- 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 }) };
1257
1367
  }
1258
1368
  }