evolcore 0.0.21 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/bin/codex-managed-hook.mjs +3 -0
  3. package/bin/install-codex-managed-hooks.mjs +3 -1
  4. package/dist/agents/claude-runner.js +14 -0
  5. package/dist/agents/codex-app-server-client.js +31 -5
  6. package/dist/agents/codex-runner.js +926 -121
  7. package/dist/aun/outbox.js +7 -0
  8. package/dist/channels/aun.js +209 -35
  9. package/dist/cli/daemon-commands.js +29 -8
  10. package/dist/cli/task-context.js +4 -0
  11. package/dist/cli/trigger-command.js +13 -4
  12. package/dist/config/config-field-policy.js +3 -0
  13. package/dist/config/config-manager.js +32 -5
  14. package/dist/config/contact-book-store.js +25 -3
  15. package/dist/core/auth/agent-delegation.js +12 -0
  16. package/dist/core/auth/auth-gateway.js +8 -0
  17. package/dist/core/auth/authorization-audit.js +66 -6
  18. package/dist/core/bootstrap-messages.js +8 -0
  19. package/dist/core/bootstrap-service.js +93 -25
  20. package/dist/core/command/command-handler.js +21 -0
  21. package/dist/core/command/menu-handler.js +9 -0
  22. package/dist/core/command/menu-protocol.js +1 -1
  23. package/dist/core/command/slash-handler.js +41 -18
  24. package/dist/core/data-migration.js +11 -1
  25. package/dist/core/event-catalog.js +32 -0
  26. package/dist/core/handoff/runtime.js +23 -3
  27. package/dist/core/message/im-renderer.js +7 -3
  28. package/dist/core/message/message-bridge.js +60 -2
  29. package/dist/core/message/message-log.js +33 -0
  30. package/dist/core/message/message-queue.js +21 -0
  31. package/dist/core/message/response-engine.js +172 -41
  32. package/dist/core/permission/ec-command-parser.js +272 -70
  33. package/dist/core/permission/protected-paths.js +11 -10
  34. package/dist/core/permission/tool-error-code.js +12 -0
  35. package/dist/core/permission/tool-policy.js +46 -5
  36. package/dist/core/session/session-manager.js +30 -0
  37. package/dist/core/session/session-renew.js +18 -1
  38. package/dist/core/session/session-turn-coordinator.js +5 -1
  39. package/dist/index.js +64 -5
  40. package/dist/ipc.js +97 -17
  41. package/dist/paths.js +18 -0
  42. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  43. package/dist/stats/price-resolver.js +4 -0
  44. package/dist/trigger/feedback.js +14 -2
  45. package/dist/trigger/parser.js +10 -1
  46. package/dist/trigger/scheduler.js +20 -3
  47. package/dist/utils/logger.js +9 -4
  48. package/dist/utils/tool-summary.js +59 -0
  49. package/dist/utils/windows-shell-trust.js +201 -0
  50. package/kits/docs/evolcore/INDEX.md +2 -2
  51. package/kits/docs/evolcore/agent-create.md +146 -0
  52. package/kits/docs/evolcore/agent.md +6 -0
  53. package/kits/docs/evolcore/group-collaboration.md +251 -0
  54. package/kits/docs/evolcore/group-rules.md +1 -19
  55. package/kits/docs/evolcore/group.md +3 -1
  56. package/kits/docs/evolcore/trigger.md +6 -3
  57. package/kits/docs/prompt-loading-architecture.md +6 -0
  58. package/kits/eck_message_manifest.json +6 -6
  59. package/kits/schemas/_meta.json +3 -2
  60. package/kits/schemas/agent-config.schema.12.json +427 -0
  61. package/kits/templates/message-fragments/item.md +1 -1
  62. package/kits/templates/system-fragments/bootstrap.md +2 -1
  63. package/kits/templates/system-fragments/commands.md +2 -2
  64. package/package.json +2 -2
@@ -4,7 +4,7 @@ import { randomUUID } from 'crypto';
4
4
  import { logger } from '../../utils/logger.js';
5
5
  import { resolveRoot } from '../../paths.js';
6
6
  import { containsHClassReference, containsLClassReference, checkProtectedPathAccess, getExistingHClassMaskTargets, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, resolveProtectedCandidateWithoutFinalSymlink, } from './protected-paths.js';
7
- import { classifyEvolcoreShellCommand, hasCodexCmdCarrierEcIntent, parseCodexToolCommandArgv, parseBoundedOutputShellCommand, parseLiteralShellArgv, resolveCodexShellCarrierArgv, resolveCodexShellCarrierString, } from './ec-command-parser.js';
7
+ import { classifyEvolcoreShellCommand, hasCodexCmdCarrierEcIntent, hasPowerShellDelegationAssignment, parseCodexToolCommand, parseBoundedOutputShellCommand, parseLiteralShellArgv, resolveCodexShellCarrierArgv, resolveCodexShellCarrierString, } from './ec-command-parser.js';
8
8
  import { analyzeReadonlyShellQuery, } from './readonly-shell-query.js';
9
9
  /** Resolve the session-owned temporary root supplied by the execution host. */
10
10
  export function resolveManagedTempDir(env = process.env) {
@@ -1966,7 +1966,31 @@ export function evaluateToolPreflight(toolName, input, context) {
1966
1966
  policyCode: 'managed_ec_discovery_forbidden',
1967
1967
  };
1968
1968
  }
1969
- const argv = context.sessionId ? parseCodexToolCommandArgv(input) : null;
1969
+ const parserOptions = {
1970
+ verifyShellExecutable: process.platform === 'win32',
1971
+ ...(context.delegationCarrier
1972
+ ? { expectedDelegationToken: context.delegationCarrier }
1973
+ : {}),
1974
+ };
1975
+ const parsedCodex = context.sessionId ? parseCodexToolCommand(input, parserOptions) : null;
1976
+ if (context.sessionId && parsedCodex && !parsedCodex.ok && parsedCodex.issue === 'untrusted-shell') {
1977
+ return {
1978
+ behavior: 'deny',
1979
+ input,
1980
+ message: '🔒 Shell 可执行文件未通过 Windows Authenticode 签名校验,禁止进入 EvolCore EC 特权路径。',
1981
+ policyCode: 'ec_shell_untrusted_executable',
1982
+ };
1983
+ }
1984
+ if (context.sessionId && parsedCodex?.ok && parsedCodex.delegationToken
1985
+ && !context.delegationCarrier) {
1986
+ return {
1987
+ behavior: 'deny',
1988
+ input,
1989
+ message: '🔒 PowerShell 命令中的 EVOLCORE_DELEGATION_TOKEN 必须与当前托管 thread carrier 绑定;无法确认时拒绝执行。',
1990
+ policyCode: 'delegation_token_unbound',
1991
+ };
1992
+ }
1993
+ const argv = parsedCodex?.ok ? parsedCodex.argv : null;
1970
1994
  const managedAidArgv = argv?.at(-2) === '--format' && argv.at(-1) === 'json'
1971
1995
  ? argv.slice(0, -2)
1972
1996
  : argv;
@@ -2003,9 +2027,18 @@ export function evaluateToolPreflight(toolName, input, context) {
2003
2027
  }
2004
2028
  const shellDialect = stringShellCarrier?.dialect
2005
2029
  ?? (hasCodexCmdCarrierEcIntent(input) ? 'cmd' : undefined);
2006
- const ecCommand = classifyEvolcoreShellCommand(policyCommand, shellDialect
2007
- ? { dialect: shellDialect }
2008
- : {});
2030
+ const hasPowerShellCarrierAssignment = stringShellCarrier?.dialect === 'powershell'
2031
+ && hasPowerShellDelegationAssignment(stringShellCarrier.command);
2032
+ const ecCommand = hasPowerShellCarrierAssignment && parsedCodex
2033
+ ? parsedCodex.ok && parsedCodex.argv[0] === 'ec'
2034
+ ? { kind: 'literal' }
2035
+ : {
2036
+ kind: 'composite',
2037
+ issue: parsedCodex.ok ? 'shell-composition' : parsedCodex.issue,
2038
+ }
2039
+ : classifyEvolcoreShellCommand(policyCommand, shellDialect
2040
+ ? { dialect: shellDialect, ...parserOptions }
2041
+ : parserOptions);
2009
2042
  if (ecCommand.kind === 'literal') {
2010
2043
  return { behavior: 'allow', input, reason: 'ec-command' };
2011
2044
  }
@@ -2013,6 +2046,14 @@ export function evaluateToolPreflight(toolName, input, context) {
2013
2046
  return prepareBoundedOutputInput(input, ecCommand.command, 'ec', context);
2014
2047
  }
2015
2048
  if (ecCommand.kind === 'composite') {
2049
+ if (ecCommand.issue === 'untrusted-shell') {
2050
+ return {
2051
+ behavior: 'deny',
2052
+ input,
2053
+ message: '🔒 Shell 可执行文件未通过 Windows Authenticode 签名校验,禁止进入 EvolCore EC 特权路径。',
2054
+ policyCode: 'ec_shell_untrusted_executable',
2055
+ };
2056
+ }
2016
2057
  if (ecCommand.issue === 'unsafe-expansion') {
2017
2058
  const message = shellDialect === 'powershell'
2018
2059
  ? '🔒 EC 双引号正文包含未转义的 PowerShell 展开;纯文字请优先使用单引号,或用 PowerShell 反引号转义 $ 和正文内的双引号。PowerShell 传给 ec 时会还原为原文字面量'
@@ -828,6 +828,7 @@ export class SessionManager {
828
828
  }
829
829
  // Create new session
830
830
  const sessionMetadata = { ...(metadata || {}) };
831
+ sessionMetadata.__sessionCreatedAt = Date.now();
831
832
  const newIdentity = identity ?? this.resolveIdentity(channel, userId, chatType, identityConversationId);
832
833
  const resolvedBaseagent = baseagent || 'claude';
833
834
  const session = {
@@ -1111,6 +1112,33 @@ export class SessionManager {
1111
1112
  }
1112
1113
  return 'legacy_updated';
1113
1114
  }
1115
+ /**
1116
+ * Clear a main-session backend only when the durable latest snapshot still
1117
+ * points at the caller's expected ID. This is the recovery counterpart to
1118
+ * provider-side "session not found" errors and must never rotate a topic.
1119
+ */
1120
+ async clearMainSessionBackendIfMatches(sessionId, expectedAgentSessionId, metadataKeys = []) {
1121
+ const loaded = this.loadSessionForUpdate(sessionId);
1122
+ if (!loaded)
1123
+ return 'missing';
1124
+ const { current } = loaded;
1125
+ if (current.threadId)
1126
+ return 'topic';
1127
+ const currentId = normalizeBackendSessionId(current.agentSessionId);
1128
+ if (!currentId)
1129
+ return 'already_clear';
1130
+ if (currentId !== normalizeBackendSessionId(expectedAgentSessionId))
1131
+ return 'mismatch';
1132
+ current.agentSessionId = undefined;
1133
+ if (metadataKeys.length > 0 && current.metadata) {
1134
+ const metadata = { ...current.metadata };
1135
+ for (const key of metadataKeys)
1136
+ delete metadata[key];
1137
+ current.metadata = metadata;
1138
+ }
1139
+ this.persistSession(current, 'sync');
1140
+ return 'cleared';
1141
+ }
1114
1142
  async switchAgent(channel, channelId, projectPath, newBaseagent) {
1115
1143
  const inheritedChatType = this.getActiveChatType(channel, channelId);
1116
1144
  const identity = this.deriveChannelIdentity(channel, channelId);
@@ -1424,6 +1452,7 @@ export class SessionManager {
1424
1452
  throw new Error(`[SessionManager] createNewSession: baseagent is empty for channel=${channel} channelId=${channelId}`);
1425
1453
  }
1426
1454
  const metadata = newSessionStableMetadata(inheritedChatType, activeMetadata, identityMetadata);
1455
+ metadata.__sessionCreatedAt = Date.now();
1427
1456
  if (identityMetadata?.sessionBoundaryOperationId) {
1428
1457
  metadata.sessionBoundaryOperationId = identityMetadata.sessionBoundaryOperationId;
1429
1458
  }
@@ -1586,6 +1615,7 @@ export class SessionManager {
1586
1615
  delete metadata.agentSessions;
1587
1616
  delete metadata.resumeAt;
1588
1617
  delete metadata.replyContext;
1618
+ metadata.__sessionRenewedAt = Date.now();
1589
1619
  const session = {
1590
1620
  id: generateSessionId(),
1591
1621
  channel: sourceSession.channel,
@@ -13,6 +13,9 @@ const MAX_CONTEXT_CHARS = 16_000;
13
13
  const MAX_MESSAGE_CHARS = 2_000;
14
14
  const MODEL_TIMEOUT_MS = 10_000;
15
15
  const RECENT_DECISION_TTL_MS = 30_000;
16
+ const FRESH_EMPTY_SESSION_TTL_MS = 30_000;
17
+ const RENEWED_AT_METADATA_KEY = '__sessionRenewedAt';
18
+ const CREATED_AT_METADATA_KEY = '__sessionCreatedAt';
16
19
  const NEW_CONFIDENCE_THRESHOLD = 0.85;
17
20
  const AUXILIARY_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
18
21
  const EXPLICIT_NEW_RE = /^(?:新话题|换个话题|重新开始|新开会话|新会话)[::,,;;。!!\n]/;
@@ -91,7 +94,9 @@ export class SessionRenewService {
91
94
  const routeKey = this.routeKey(request);
92
95
  return await this.withLock(routeKey, async () => {
93
96
  const cached = this.recentDecisions.get(routeKey);
94
- if (cached && cached.candidateSessionId === request.session.id && cached.expiresAt > this.now()) {
97
+ if (cached
98
+ && cached.expiresAt > this.now()
99
+ && (cached.candidateSessionId === request.session.id || cached.selectedSessionId === request.session.id)) {
95
100
  const selected = cached.selectedSessionId === request.session.id
96
101
  ? request.session
97
102
  : await this.sessionManager.getSessionById(cached.selectedSessionId);
@@ -121,6 +126,18 @@ export class SessionRenewService {
121
126
  if (!last) {
122
127
  if (request.isNewSession)
123
128
  return { session: request.session, renewed: false };
129
+ // A renewed session is intentionally created before its first inbound
130
+ // message is appended. A concurrent message can therefore observe an
131
+ // empty, freshly-created session. Treat that state as already renewed;
132
+ // do not create a second logical session for the same route.
133
+ const metadata = request.session.metadata;
134
+ const renewedAt = Number(metadata?.[RENEWED_AT_METADATA_KEY] ?? metadata?.[CREATED_AT_METADATA_KEY]);
135
+ const sessionAgeMs = Number.isFinite(renewedAt)
136
+ ? Math.max(0, this.now() - renewedAt)
137
+ : Number.POSITIVE_INFINITY;
138
+ if (Number.isFinite(renewedAt) && renewedAt <= this.now() && sessionAgeMs <= FRESH_EMPTY_SESSION_TTL_MS) {
139
+ return { session: request.session, renewed: false, decision: 'continue', source: 'missing_history' };
140
+ }
124
141
  const idleMs = this.now() - request.session.updatedAt;
125
142
  return await this.finishDecision(routeKey, request, 'new', 'missing_history', idleMs, evaluationStartedAt);
126
143
  }
@@ -48,10 +48,14 @@ export class SessionTurnCoordinator {
48
48
  && state.lastTerminal.taskId === lease.taskId
49
49
  && state.lastTerminal.generation === lease.generation;
50
50
  }
51
- invalidate(sessionId, reason) {
51
+ invalidate(sessionId, reason, expected) {
52
52
  const state = this.states.get(sessionId);
53
53
  if (!state)
54
54
  return undefined;
55
+ if (expected?.taskId && state.active?.taskId !== expected.taskId)
56
+ return undefined;
57
+ if (expected?.generation !== undefined && state.active?.generation !== expected.generation)
58
+ return undefined;
55
59
  if (state.active?.status === 'interrupting' && state.active.cancelReason === reason) {
56
60
  return cloneTurnMetadata(state);
57
61
  }
package/dist/index.js CHANGED
@@ -88,12 +88,12 @@ import { TriggerFeedbackDispatcher } from './trigger/feedback.js';
88
88
  import { TriggerRuntimeScheduler } from './trigger/scheduler.js';
89
89
  import { TargetSessionLockRegistry } from './trigger/session-lock.js';
90
90
  import { DaemonChannel } from './channels/daemon.js';
91
- import { definitionRevision, normalizeTriggerDefinition } from './trigger/validation.js';
91
+ import { definitionRevision, normalizeTriggerDefinition, splitScriptCommand } from './trigger/validation.js';
92
92
  import { applyTriggerPatch } from './trigger/patch.js';
93
93
  import { validateModelSelectionForRole } from './core/model/model-permission.js';
94
94
  import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride, } from './core/role/runtime-policy.js';
95
95
  import { atomicWriteJson } from './core/session/session-fs-store.js';
96
- import { ensureProcessManagedTempDir } from './cli/task-context.js';
96
+ import { DAEMON_RUNTIME_EPOCH_ENV, ensureProcessManagedTempDir } from './cli/task-context.js';
97
97
  import { appendMessageLog, buildOutboundEntry, classifyAunPayloadForLog } from './core/message/message-log.js';
98
98
  import { normalizeAunMentionEntries } from './aun/msg/mention-schema.js';
99
99
  import { MAIN_PACKAGE_NAME } from './product.js';
@@ -1211,6 +1211,9 @@ async function main() {
1211
1211
  : null;
1212
1212
  bindService?.startCleanup();
1213
1213
  const agentDelegationRegistry = new AgentDelegationRegistry();
1214
+ // Replace any inherited marker from a parent/previous daemon before child
1215
+ // runners or in-process IPC clients are created.
1216
+ process.env[DAEMON_RUNTIME_EPOCH_ENV] = agentDelegationRegistry.getRuntimeEpoch();
1214
1217
  // 创建命令处理器
1215
1218
  const cmdHandler = new CommandHandler(sessionManager, agentMap, messageCache, eventBus, primaryRunnerKey);
1216
1219
  let agentApplicationService;
@@ -2305,6 +2308,7 @@ async function main() {
2305
2308
  return { ok: false, code: delegation.code, error: delegation.reason };
2306
2309
  return cmdHandler.handleCtl(cmd, sessionId, delegation.grant);
2307
2310
  });
2311
+ ipcServer.setDaemonRuntimeEpoch(agentDelegationRegistry.getRuntimeEpoch());
2308
2312
  // Register every IPC executor/provider before exposing the endpoint. The
2309
2313
  // function declaration is hoisted, while its invocation remains here so a
2310
2314
  // failed bind still aborts before any AUN connection is attempted.
@@ -2836,7 +2840,7 @@ async function main() {
2836
2840
  for (const runner of new Set(agentMap.values())) {
2837
2841
  if (typeof runner?.evaluatePreToolUse !== 'function')
2838
2842
  continue;
2839
- const result = await runner.evaluatePreToolUse(params.threadId, params.toolName, params.toolInput, params.signal);
2843
+ const result = await runner.evaluatePreToolUse(params.threadId, params.toolName, params.toolInput, params.signal, params.callId);
2840
2844
  if (result.applicable)
2841
2845
  return result;
2842
2846
  }
@@ -4003,17 +4007,59 @@ async function main() {
4003
4007
  const existing = schedulerFor(agentAid).list({ all: true }).find(trigger => trigger.id === cmd.triggerId);
4004
4008
  if (!existing)
4005
4009
  throw new Error(`trigger not found: ${cmd.triggerId}`);
4010
+ const rawPatch = cmd.patch && typeof cmd.patch === 'object' && !Array.isArray(cmd.patch)
4011
+ ? cmd.patch
4012
+ : {};
4013
+ const rawScriptFile = rawPatch.scriptFile;
4014
+ if (rawScriptFile !== undefined && rawScriptFile !== true) {
4015
+ throw new Error('scriptFile must be true');
4016
+ }
4017
+ const patchScriptFile = rawScriptFile === true;
4018
+ const { scriptFile: _scriptFile, ...definitionPatch } = rawPatch;
4019
+ const scriptFileBase64 = cmd.scriptFileBase64;
4020
+ if (patchScriptFile && scriptFileBase64 === undefined) {
4021
+ throw new Error('--script-file requires scriptFileBase64');
4022
+ }
4023
+ if (!patchScriptFile && scriptFileBase64 !== undefined) {
4024
+ throw new Error('scriptFileBase64 requires --script-file');
4025
+ }
4026
+ if (scriptFileBase64 !== undefined) {
4027
+ if (typeof scriptFileBase64 !== 'string') {
4028
+ throw new Error('scriptFileBase64 must be a base64 string');
4029
+ }
4030
+ if (!actor.daemonPrivileged) {
4031
+ throw new Error('updating a script Trigger requires DaemonOwner');
4032
+ }
4033
+ if (existing.execution.type !== 'script' || !existing.execution.script) {
4034
+ throw new Error('--script-file only applies to script Triggers');
4035
+ }
4036
+ if (scriptFileBase64.length > 6 * 1024 * 1024
4037
+ || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(scriptFileBase64)) {
4038
+ throw new Error('scriptFileBase64 is invalid or exceeds the 4 MiB limit');
4039
+ }
4040
+ const scriptBytes = Buffer.from(scriptFileBase64, 'base64');
4041
+ if (scriptBytes.length > 4 * 1024 * 1024)
4042
+ throw new Error('script file exceeds the 4 MiB limit');
4043
+ }
4006
4044
  const currentRevision = definitionRevision(existing);
4007
4045
  if (cmd.expectedRevision !== undefined && cmd.expectedRevision !== currentRevision) {
4008
4046
  throw new Error(`trigger revision conflict: expected ${cmd.expectedRevision}, current ${currentRevision}`);
4009
4047
  }
4010
- let definition = applyTriggerPatch(existing, cmd.patch, { fromPromptFile: cmd.promptFile === true });
4048
+ let definition = Object.keys(definitionPatch).length > 0
4049
+ ? applyTriggerPatch(existing, definitionPatch, { fromPromptFile: cmd.promptFile === true })
4050
+ : existing;
4011
4051
  rejectUnapprovedFullAccessConfiguration(definition, actor);
4012
4052
  definition = withFullAccessConfigurationProvenance(definition, actor);
4013
4053
  validateTriggerDefinitionForActor(definition, actor);
4014
4054
  validateTriggerFeedbackChannels(definition);
4015
4055
  const scheduler = schedulerFor(agentAid);
4016
- const trigger = scheduler.update(cmd.triggerId, definition);
4056
+ const files = scriptFileBase64 !== undefined && existing.execution.script
4057
+ ? [{
4058
+ relativePath: splitScriptCommand(existing.execution.script.path).path,
4059
+ contentBase64: scriptFileBase64,
4060
+ }]
4061
+ : [];
4062
+ const trigger = scheduler.update(cmd.triggerId, definition, files);
4017
4063
  if (trigger.execution.permissionMode === 'fullaccess') {
4018
4064
  auditFullAccessTriggerConfiguration('fullaccess.trigger.configured', trigger, actor, existing.execution.permissionMode === 'fullaccess'
4019
4065
  ? 'fullaccess Trigger updated through IPC'
@@ -4152,6 +4198,19 @@ async function main() {
4152
4198
  await Promise.all([...triggerSchedulers.values()].map(scheduler => scheduler.suspendForDaemonExit()));
4153
4199
  });
4154
4200
  await shutdownStep('interrupt active message queue tasks', async () => { await queueShutdown; });
4201
+ await shutdownStep('dispose agent runners', async () => {
4202
+ const currentInstances = [...agentMap.entries()].map(([key, agent]) => {
4203
+ const split = key.lastIndexOf('::');
4204
+ return {
4205
+ evolagentName: split > 0 ? key.slice(0, split) : '<unknown>',
4206
+ baseagent: split > 0 ? key.slice(split + 2) : 'unknown',
4207
+ agent,
4208
+ };
4209
+ });
4210
+ await disposeAgentInstances(currentInstances, (instance, error) => {
4211
+ logger.warn(`[Shutdown] Failed to dispose runner ${instance.evolagentName}::${instance.baseagent}: ${error instanceof Error ? error.message : String(error)}`);
4212
+ });
4213
+ });
4155
4214
  eventBus.publish({
4156
4215
  type: 'system:shutdown',
4157
4216
  timestamp: Date.now()
package/dist/ipc.js CHANGED
@@ -8,18 +8,20 @@ import path from 'path';
8
8
  import { logger } from './utils/logger.js';
9
9
  import { withMenuProcessingTime, logMenuRequestCompleted, logMenuRequestReceived } from './core/command/menu-protocol.js';
10
10
  import { fileCache } from './core/daemon-file-cache.js';
11
+ import { DAEMON_RUNTIME_EPOCH_ENV } from './cli/task-context.js';
11
12
  import { HANDOFF_QUERY_MAX_LIMIT, HANDOFF_STATES } from './core/handoff/types.js';
12
13
  import { AgentReloadBusyError } from './core/agent-reload-coordinator.js';
13
- import { getWindowsInstanceSocketPathCandidates, resolvePaths } from './paths.js';
14
+ import { getWindowsInstanceSocketPathCandidates, normalizeIpcEndpoint, resolvePaths } from './paths.js';
14
15
  import { getProcessStartTime, isSameOrOlderProcess } from './utils/process-introspect.js';
15
16
  import { classifyProcessTree, ProcessTreeSampler, readProcessMetrics } from './utils/process-tree-stats.js';
16
17
  import { readSystemMemoryUsage } from './utils/system-memory.js';
17
18
  import { hashCurrentDelegatedCommand } from './core/auth/agent-delegation.js';
18
19
  import { normalizeAunMentionEntries } from './aun/msg/mention-schema.js';
19
20
  const isWindows = process.platform === 'win32';
20
- const isNamedPipe = (p) => isWindows && p.startsWith('\\\\.\\pipe\\');
21
+ const isNamedPipe = (p) => isWindows && p.toLowerCase().startsWith('\\\\.\\pipe\\');
21
22
  const CPU_SAMPLE_INTERVAL_MS = 1000;
22
23
  const PROCESS_TREE_SAMPLE_INTERVAL_MS = 5000;
24
+ const MONITOR_LEASE_TTL_MS = 15_000;
23
25
  const PROCESS_TREE_SAMPLE_WARN_MS = 250;
24
26
  const EVENT_LOOP_LAG_WARN_MS = 100;
25
27
  const SAMPLE_WARN_LOG_INTERVAL_MS = 60_000;
@@ -43,6 +45,8 @@ const LOCAL_CONTROL_COMMANDS = new Set([
43
45
  'evolagent.resync',
44
46
  'menu.exec',
45
47
  'monitor-snapshot',
48
+ 'monitor-subscribe',
49
+ 'monitor-unsubscribe',
46
50
  'shutdown',
47
51
  ]);
48
52
  let ipcQuerySequence = 0;
@@ -80,9 +84,9 @@ export function summarizeMonitorAgents(agents, aids) {
80
84
  };
81
85
  }
82
86
  export class IpcServer {
83
- socketPath;
84
87
  getStatus;
85
88
  commandExecutor;
89
+ socketPath;
86
90
  server = null;
87
91
  agentRegistry;
88
92
  agentApplicationService;
@@ -122,6 +126,7 @@ export class IpcServer {
122
126
  wecomContactBindExecutor;
123
127
  wechatContactBindExecutor;
124
128
  controlToken;
129
+ daemonRuntimeEpoch;
125
130
  // CPU 占用追踪:IPC handler 是一次性同步调用,无法在响应里做 200ms 异步采样,
126
131
  // 故用后台 1s interval 累积 process.cpuUsage() 增量,handler 直接读最近值。
127
132
  // procCpuPercent = 本 daemon 进程占单核的百分比(可 >100% 仅当多核,已 clamp 到 100);
@@ -138,6 +143,7 @@ export class IpcServer {
138
143
  processTreeWorkerRequestStartedAt = 0;
139
144
  processTreeWorkerRequestTimer = null;
140
145
  processTreeSamplingInFlight = false;
146
+ processTreeMonitorLeases = new Map();
141
147
  lastCpuTickMono = null;
142
148
  eventLoopLagMs = 0;
143
149
  maxEventLoopLagMs = 0;
@@ -155,9 +161,9 @@ export class IpcServer {
155
161
  lastError: null,
156
162
  };
157
163
  constructor(socketPath, getStatus, commandExecutor) {
158
- this.socketPath = socketPath;
159
164
  this.getStatus = getStatus;
160
165
  this.commandExecutor = commandExecutor;
166
+ this.socketPath = normalizeIpcEndpoint(socketPath);
161
167
  }
162
168
  /** Inject EvolAgentRegistry for evolagent.* IPC handlers */
163
169
  setAgentRegistry(registry) {
@@ -171,6 +177,10 @@ export class IpcServer {
171
177
  setControlToken(token) {
172
178
  this.controlToken = Buffer.from(token);
173
179
  }
180
+ /** Set the process-lifetime marker used to reject stale managed children. */
181
+ setDaemonRuntimeEpoch(epoch) {
182
+ this.daemonRuntimeEpoch = epoch;
183
+ }
174
184
  /** Inject menu.* executor (ECWeb Control proxies menu requests through this) */
175
185
  setMenuExecutor(executor) {
176
186
  this.menuExecutor = executor;
@@ -296,11 +306,10 @@ export class IpcServer {
296
306
  setWechatContactBindExecutor(executor) {
297
307
  this.wechatContactBindExecutor = executor;
298
308
  }
299
- /** Start the background CPU and process-tree sampling loops. Call after start(). */
309
+ /** Start the lightweight background CPU sampling loop. Call after start(). */
300
310
  startCpuTracking() {
301
311
  if (this.cpuTimer)
302
312
  return;
303
- this.sampleProcessTree();
304
313
  this.lastCpuTickMono = performance.now();
305
314
  this.cpuTimer = setInterval(() => {
306
315
  const tickMono = performance.now();
@@ -342,17 +351,30 @@ export class IpcServer {
342
351
  }, CPU_SAMPLE_INTERVAL_MS);
343
352
  // Don't keep the event loop alive for sampling alone.
344
353
  this.cpuTimer.unref?.();
345
- // Process enumeration is substantially more expensive on Windows than
346
- // process.cpuUsage()/os.cpus(), so keep it at a lower refresh rate.
347
- this.processTreeTimer = setInterval(() => this.sampleProcessTree(), PROCESS_TREE_SAMPLE_INTERVAL_MS);
348
- this.processTreeTimer.unref?.();
349
354
  }
350
- /** Stop the CPU and process-tree sampling loops. */
355
+ /** Stop all CPU and process-tree sampling loops. */
351
356
  stopCpuTracking() {
352
357
  if (this.cpuTimer) {
353
358
  clearInterval(this.cpuTimer);
354
359
  this.cpuTimer = null;
355
360
  }
361
+ this.processTreeMonitorLeases.clear();
362
+ this.stopProcessTreeTracking();
363
+ this.lastCpuTickMono = null;
364
+ }
365
+ startProcessTreeTracking() {
366
+ if (this.processTreeTimer)
367
+ return;
368
+ this.sampleProcessTree();
369
+ this.processTreeTimer = setInterval(() => {
370
+ this.pruneExpiredMonitorLeases();
371
+ if (this.processTreeMonitorLeases.size === 0)
372
+ return;
373
+ this.sampleProcessTree();
374
+ }, PROCESS_TREE_SAMPLE_INTERVAL_MS);
375
+ this.processTreeTimer.unref?.();
376
+ }
377
+ stopProcessTreeTracking() {
356
378
  if (this.processTreeTimer) {
357
379
  clearInterval(this.processTreeTimer);
358
380
  this.processTreeTimer = null;
@@ -372,7 +394,31 @@ export class IpcServer {
372
394
  this.processTreeWorker = null;
373
395
  if (worker)
374
396
  void worker.terminate();
375
- this.lastCpuTickMono = null;
397
+ this.processTreeSnapshot = null;
398
+ this.associatedProcessKinds.clear();
399
+ }
400
+ acquireProcessTreeMonitor(leaseId) {
401
+ this.processTreeMonitorLeases.set(leaseId, Date.now() + MONITOR_LEASE_TTL_MS);
402
+ this.startProcessTreeTracking();
403
+ }
404
+ refreshProcessTreeMonitor(leaseId) {
405
+ if (typeof leaseId !== 'string' || !this.processTreeMonitorLeases.has(leaseId))
406
+ return;
407
+ this.processTreeMonitorLeases.set(leaseId, Date.now() + MONITOR_LEASE_TTL_MS);
408
+ }
409
+ releaseProcessTreeMonitor(leaseId) {
410
+ this.processTreeMonitorLeases.delete(leaseId);
411
+ if (this.processTreeMonitorLeases.size === 0)
412
+ this.stopProcessTreeTracking();
413
+ }
414
+ pruneExpiredMonitorLeases() {
415
+ const now = Date.now();
416
+ for (const [leaseId, expiresAt] of this.processTreeMonitorLeases) {
417
+ if (expiresAt <= now)
418
+ this.processTreeMonitorLeases.delete(leaseId);
419
+ }
420
+ if (this.processTreeMonitorLeases.size === 0)
421
+ this.stopProcessTreeTracking();
376
422
  }
377
423
  sampleProcessTree() {
378
424
  if (isWindows) {
@@ -654,6 +700,17 @@ export class IpcServer {
654
700
  if (requiresLocalControlToken(cmd) && !this.hasValidControlToken(cmd.controlToken)) {
655
701
  return { ok: false, code: 'INVALID_CONTROL_TOKEN', error: 'valid local control token is required' };
656
702
  }
703
+ if (typeof cmd.sessionId === 'string' && cmd.sessionId
704
+ && typeof cmd.delegationToken === 'string' && cmd.delegationToken
705
+ && this.daemonRuntimeEpoch
706
+ && cmd.daemonRuntimeEpoch !== this.daemonRuntimeEpoch) {
707
+ logger.warn(`[IPC] stale delegation rejected: session=${ipcLogToken(cmd.sessionId)} type=${ipcLogToken(cmd.type)} cause=daemon_runtime_epoch_mismatch`);
708
+ return {
709
+ ok: false,
710
+ code: 'DAEMON_RESTARTED',
711
+ error: 'The task delegation token belongs to a previous daemon runtime; start a new task authorization and do not retry this command in the current task',
712
+ };
713
+ }
657
714
  switch (cmd.type) {
658
715
  case 'status':
659
716
  return this.getStatus();
@@ -670,6 +727,7 @@ export class IpcServer {
670
727
  threadId: cmd.threadId,
671
728
  toolName: cmd.toolName,
672
729
  toolInput: cmd.toolInput,
730
+ callId: typeof cmd.callId === 'string' ? cmd.callId : undefined,
673
731
  signal: requestSignal,
674
732
  });
675
733
  }
@@ -1328,6 +1386,7 @@ export class IpcServer {
1328
1386
  }
1329
1387
  }
1330
1388
  case 'monitor-snapshot': {
1389
+ this.refreshProcessTreeMonitor(cmd.leaseId);
1331
1390
  // watch web Monitor 页用:进程级 + 系统级运行指标 + 全局 stats + per-agent 汇总。
1332
1391
  const mem = process.memoryUsage();
1333
1392
  const systemMemory = readSystemMemoryUsage();
@@ -1407,6 +1466,20 @@ export class IpcServer {
1407
1466
  },
1408
1467
  };
1409
1468
  }
1469
+ case 'monitor-subscribe': {
1470
+ const leaseId = typeof cmd.leaseId === 'string' ? cmd.leaseId.trim() : '';
1471
+ if (!leaseId || leaseId.length > 128)
1472
+ return { ok: false, error: 'invalid monitor lease id' };
1473
+ this.acquireProcessTreeMonitor(leaseId);
1474
+ return { ok: true, leaseTtlMs: MONITOR_LEASE_TTL_MS };
1475
+ }
1476
+ case 'monitor-unsubscribe': {
1477
+ const leaseId = typeof cmd.leaseId === 'string' ? cmd.leaseId.trim() : '';
1478
+ if (!leaseId || leaseId.length > 128)
1479
+ return { ok: false, error: 'invalid monitor lease id' };
1480
+ this.releaseProcessTreeMonitor(leaseId);
1481
+ return { ok: true };
1482
+ }
1410
1483
  default:
1411
1484
  return { error: `unknown command: ${cmd.type}` };
1412
1485
  }
@@ -1425,14 +1498,15 @@ export class IpcServer {
1425
1498
  */
1426
1499
  /** Pass `null` as timeoutMs to wait without a client-side deadline. */
1427
1500
  export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
1428
- let socketCandidates = [socketPath];
1429
- if (isNamedPipe(socketPath)) {
1501
+ const endpoint = normalizeIpcEndpoint(socketPath);
1502
+ let socketCandidates = [endpoint];
1503
+ if (isNamedPipe(endpoint)) {
1430
1504
  const paths = resolvePaths();
1431
- if (socketPath === paths.socket)
1505
+ if (endpoint === paths.socket)
1432
1506
  socketCandidates = getWindowsInstanceSocketPathCandidates(paths.root);
1433
1507
  }
1434
1508
  if (socketCandidates.length <= 1)
1435
- return ipcQueryOnce(socketPath, cmd, timeoutMs);
1509
+ return ipcQueryOnce(endpoint, cmd, timeoutMs);
1436
1510
  return (async () => {
1437
1511
  const startedAt = Date.now();
1438
1512
  for (const candidate of socketCandidates) {
@@ -1464,7 +1538,13 @@ function ipcQueryOnce(socketPath, cmd, timeoutMs = 3000) {
1464
1538
  && typeof authenticatedCmd.delegationCommandHash !== 'string'
1465
1539
  ? hashCurrentDelegatedCommand()
1466
1540
  : undefined;
1467
- const request = delegationCommandHash ? { ...authenticatedCmd, delegationCommandHash } : authenticatedCmd;
1541
+ const request = {
1542
+ ...(delegationCommandHash ? { ...authenticatedCmd, delegationCommandHash } : authenticatedCmd),
1543
+ ...(process.env[DAEMON_RUNTIME_EPOCH_ENV]
1544
+ && typeof authenticatedCmd.delegationToken === 'string'
1545
+ ? { daemonRuntimeEpoch: process.env[DAEMON_RUNTIME_EPOCH_ENV] }
1546
+ : {}),
1547
+ };
1468
1548
  return new Promise((resolve) => {
1469
1549
  const startedAtMono = performance.now();
1470
1550
  const queryId = nextIpcQueryId(request);
package/dist/paths.js CHANGED
@@ -179,6 +179,24 @@ function windowsSocketPathForRoot(root) {
179
179
  const hash = crypto.createHash('sha1').update(root).digest('hex').slice(0, 12);
180
180
  return `\\\\.\\pipe\\${WINDOWS_PIPE_PREFIX}-${hash}`;
181
181
  }
182
+ /**
183
+ * Normalize an IPC endpoint before it reaches node:net.
184
+ *
185
+ * Windows does not support Unix-domain socket files at ordinary filesystem
186
+ * paths. Callers that construct an ad-hoc `.sock` path (tests and embedded
187
+ * runtimes in particular) must therefore use the same deterministic named
188
+ * pipe on both the server and client sides. Existing named pipes and all
189
+ * non-Windows endpoints are preserved verbatim.
190
+ */
191
+ export function normalizeIpcEndpoint(endpoint) {
192
+ // Windows object-manager paths are case-insensitive. Preserve an explicit
193
+ // named pipe verbatim even when a caller spells the namespace as `PIPE`.
194
+ if (!isWindows || endpoint.toLowerCase().startsWith('\\\\.\\pipe\\'))
195
+ return endpoint;
196
+ const normalized = path.win32.resolve(endpoint).toLowerCase();
197
+ const hash = crypto.createHash('sha1').update(normalized).digest('hex').slice(0, 20);
198
+ return `\\\\.\\pipe\\${WINDOWS_PIPE_PREFIX}-endpoint-${hash}`;
199
+ }
182
200
  /**
183
201
  * Return the canonical and pre-normalization Windows daemon pipe names.
184
202
  *
@@ -197,6 +197,7 @@ export const proactiveFlow = {
197
197
  const queueMsg = queueLen >= 5
198
198
  ? `⚠️ 有 ${queueLen} 条消息未读,请尽快完成当前任务,或使用 ec ctl queue 读取后同步处理。`
199
199
  : `⚠️ 有 ${queueLen} 条消息未读,可使用 ec ctl queue 读取。`;
200
+ ctx.recordReminder?.({ kind: 'queue_unread', text: queueMsg, queueLength: queueLen });
200
201
  ctx.injectToModel(queueMsg);
201
202
  }
202
203
  }
@@ -208,13 +209,17 @@ export const proactiveFlow = {
208
209
  state.toolReportPending = true;
209
210
  const cmdHint = state.chatType === 'group' ? 'ec group send' : 'ec msg send';
210
211
  const target = state.chatType === 'group' ? '群里' : '对方';
211
- ctx.injectToModel(`⚠️ 工具调用已达到 ${state.toolCount} 次,请立即用 ${cmdHint} 向${target}汇报当前情况和下一步意图。`);
212
+ const reportMsg = `⚠️ 工具调用已达到 ${state.toolCount} 次,请立即用 ${cmdHint} 向${target}汇报当前情况和下一步意图。`;
213
+ ctx.recordReminder?.({ kind: 'tool_report', text: reportMsg, toolCount: state.toolCount });
214
+ ctx.injectToModel(reportMsg);
212
215
  }
213
216
  },
214
217
  // ─── onToolResult:成功发送回执后解除门禁 ───
215
218
  onToolResult(ctx) {
216
219
  const state = ctx.state.get(STATE_KEY);
217
- if (!state || !ctx.isSendCommand(ctx.toolName, ctx.toolInput))
220
+ if (!state)
221
+ return;
222
+ if (!ctx.isSendCommand(ctx.toolName, ctx.toolInput))
218
223
  return;
219
224
  if (!hasSuccessfulSendReceipt(ctx.result, ctx.isError || !!ctx.error))
220
225
  return;
@@ -5,6 +5,10 @@
5
5
  import { resolvePriceRow, BILLING_FNS } from './billing.js';
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
+ /** Keep wire/log metadata readable while preserving full precision in storage. */
9
+ export function roundCostForOutput(value) {
10
+ return Number.isFinite(value) ? Number(value.toFixed(4)) : value;
11
+ }
8
12
  // 网关价格表缓存(从用户覆盖层 model-prices-gateway.jsonl 读取)
9
13
  let _gatewayPriceCache = null;
10
14
  let _gatewayPriceCacheTs = 0;