evolcore 0.0.39 → 0.0.40

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 (102) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +31 -20
  3. package/bin/ec-safe-output.js +14 -2
  4. package/dist/agents/baseagent-availability.js +1 -0
  5. package/dist/agents/claude-runner.js +32 -46
  6. package/dist/agents/codex-runner.js +51 -27
  7. package/dist/agents/ecagent-runner.js +18 -7
  8. package/dist/agents/gemini-runner.js +1 -2
  9. package/dist/agents/runner-types.js +28 -6
  10. package/dist/aun/aid/client.js +1 -1
  11. package/dist/aun/aid/control-aid.js +23 -98
  12. package/dist/aun/aid/ownership-operation.js +1 -3
  13. package/dist/aun/group/invite-service.js +6 -0
  14. package/dist/aun/msg/p2p.js +8 -12
  15. package/dist/channels/aun.js +66 -48
  16. package/dist/channels/dingtalk.js +8 -2
  17. package/dist/channels/feishu.js +6 -1
  18. package/dist/channels/wecom.js +7 -3
  19. package/dist/cli/agent-create.js +23 -4
  20. package/dist/cli/aid-bind-command.js +4 -1
  21. package/dist/cli/daemon-commands.js +242 -17
  22. package/dist/cli/index.js +25 -13
  23. package/dist/cli/memory-command.js +33 -14
  24. package/dist/cli/restart-monitor.js +35 -13
  25. package/dist/cli/task-context.js +1 -0
  26. package/dist/config/config-field-policy.js +1 -0
  27. package/dist/contracts/ecweb-service-protocol.js +44 -0
  28. package/dist/core/agent-runtime.js +6 -0
  29. package/dist/core/auth/agent-delegation.js +7 -4
  30. package/dist/core/auth/auth-gateway.js +3 -1
  31. package/dist/core/auth/authorization-audit.js +5 -1
  32. package/dist/core/auth/debug-context.js +5 -1
  33. package/dist/core/auth/operation-catalog.js +16 -0
  34. package/dist/core/command/command-handler.js +7 -1
  35. package/dist/core/command/menu-catalog.js +48 -12
  36. package/dist/core/command/menu-handler.js +72 -4
  37. package/dist/core/command/menu-protocol.js +20 -1
  38. package/dist/core/command/slash-gate.js +17 -3
  39. package/dist/core/command/slash-handler.js +90 -7
  40. package/dist/core/event-catalog.js +2 -0
  41. package/dist/core/interaction-router.js +2 -1
  42. package/dist/core/ipc-registration.js +26 -7
  43. package/dist/core/memory/batch-meta.js +2 -0
  44. package/dist/core/memory/consolidation-context.js +42 -51
  45. package/dist/core/memory/consolidation-ops-records.js +39 -7
  46. package/dist/core/memory/consolidation-trigger.js +71 -64
  47. package/dist/core/memory/consolidation.js +226 -20
  48. package/dist/core/memory/memory-writer.js +160 -34
  49. package/dist/core/memory/relation-idle-monitor.js +287 -0
  50. package/dist/core/memory/relation-sweep.js +206 -0
  51. package/dist/core/message/aun-ipc-sender.js +5 -0
  52. package/dist/core/message/message-bridge.js +64 -55
  53. package/dist/core/message/message-log.js +17 -0
  54. package/dist/core/message/message-queue.js +6 -0
  55. package/dist/core/message/outbound.js +70 -0
  56. package/dist/core/message/peer-mode.js +7 -2
  57. package/dist/core/message/response-engine.js +341 -73
  58. package/dist/core/ownership/owner-resolver.js +384 -178
  59. package/dist/core/permission/ec-command-parser.js +19 -3
  60. package/dist/core/permission/tool-policy.js +7 -7
  61. package/dist/core/role/runtime-policy.js +64 -0
  62. package/dist/core/session/session-manager.js +25 -5
  63. package/dist/core/system-session/memory.js +3 -18
  64. package/dist/ecweb/basic-settings-service.js +709 -0
  65. package/dist/ecweb/providers-service.js +172 -0
  66. package/dist/index.js +92 -4
  67. package/dist/ipc.js +78 -11
  68. package/dist/stats/db.js +33 -0
  69. package/dist/stats/index.js +2 -2
  70. package/dist/stats/query.js +246 -0
  71. package/dist/stats/writer.js +41 -1
  72. package/dist/trigger/application-service.js +12 -0
  73. package/dist/trigger/authorization.js +2 -2
  74. package/dist/trigger/history.js +36 -0
  75. package/dist/trigger/scheduler.js +3 -0
  76. package/dist/utils/ecweb-supervisor.js +31 -10
  77. package/dist/utils/error-utils.js +8 -4
  78. package/dist/utils/evolcore-version.js +3 -2
  79. package/dist/utils/host-info.js +52 -0
  80. package/ecagent/dist/harness/agent-harness.js +4 -1
  81. package/kits/docs/evolcore/INDEX.md +1 -1
  82. package/kits/docs/evolcore/agent.md +1 -1
  83. package/kits/docs/evolcore/aid.md +29 -12
  84. package/kits/docs/evolcore/config.md +9 -7
  85. package/kits/docs/evolcore/memory-architecture.md +3 -4
  86. package/kits/docs/evolcore/memory-consolidation.md +28 -7
  87. package/kits/docs/evolcore/memory-tags.md +1 -1
  88. package/kits/docs/evolcore/memory.md +1 -1
  89. package/kits/docs/evolcore/menu-client.md +1 -1
  90. package/kits/docs/evolcore/menu-protocol.md +6 -0
  91. package/kits/docs/evolcore/msg.md +1 -1
  92. package/kits/eck_manifest.json +3 -3
  93. package/kits/rules/04-relation.md +3 -2
  94. package/kits/schemas/agent-config.schema.13.json +5 -1
  95. package/kits/templates/memory-consolidation/generalize.md +32 -4
  96. package/kits/templates/memory-consolidation/notes.md +2 -2
  97. package/kits/templates/memory-consolidation/promote.md +10 -2
  98. package/kits/templates/system-fragments/baseagent.md +5 -0
  99. package/kits/templates/system-fragments/memory.md +12 -3
  100. package/kits/templates/system-fragments/session.md +1 -1
  101. package/kits/{rules/07-memory-and-thread.md → templates/system-fragments/thread.md} +17 -80
  102. package/package.json +3 -3
@@ -0,0 +1,172 @@
1
+ import { readProvidersFile, resolveProviderService } from '../config/providers.js';
2
+ import { createProvider, importOrMergeBaseagentProvider, parseProviderImport, previewProviderImport, previewProviderValue, readProvidersSnapshot, resolveProviderDraftService, saveProviderChanges, updateProvider, } from '../config/providers-management.js';
3
+ import { listBaseagentImportSources, providerFromBaseagent } from '../config/providers-import.js';
4
+ import { getProviderCatalog, previewProviderCatalog } from '../core/model/model-catalog.js';
5
+ function isRecord(value) {
6
+ return !!value && typeof value === 'object' && !Array.isArray(value);
7
+ }
8
+ function response(status, body) {
9
+ return { status, body };
10
+ }
11
+ function managementCatalog(snapshot, overrides) {
12
+ const models = snapshot.models.map((model) => ({ ...model, enabled: overrides[model.id]?.enabled !== false, available: true }));
13
+ const listed = new Set(snapshot.models.map((model) => model.id));
14
+ const manualModels = Object.entries(overrides).filter(([id, override]) => override.manual === true && !listed.has(id))
15
+ .map(([id, override]) => ({
16
+ id,
17
+ ...(typeof override.name === 'string' && override.name ? { displayName: override.name } : {}),
18
+ ...(typeof override.imageIn === 'boolean' ? { imageIn: override.imageIn } : {}),
19
+ manual: true,
20
+ enabled: override.enabled !== false,
21
+ available: true,
22
+ }));
23
+ const unseenModels = Object.entries(overrides).filter(([id, override]) => !listed.has(id) && override.manual !== true)
24
+ .map(([id, override]) => ({ id, ...override, enabled: override.enabled !== false, available: false }));
25
+ return { ...snapshot, models: [...models, ...manualModels], unseenModels };
26
+ }
27
+ async function resolveImports(value) {
28
+ if (value === undefined)
29
+ return [];
30
+ if (!Array.isArray(value))
31
+ throw new Error('Provider imports must be an array');
32
+ return value.map(item => {
33
+ if (!isRecord(item) || Object.keys(item).length !== 1)
34
+ throw new Error('Invalid Provider import');
35
+ if (item.baseagent !== undefined) {
36
+ if (typeof item.baseagent !== 'string')
37
+ throw new Error('Invalid Baseagent import');
38
+ return providerFromBaseagent(item.baseagent);
39
+ }
40
+ if (item.text !== undefined)
41
+ return parseProviderImport(item.text);
42
+ if (item.provider !== undefined)
43
+ return item.provider;
44
+ throw new Error('Invalid Provider import');
45
+ });
46
+ }
47
+ function requestBody(value) {
48
+ if (!isRecord(value))
49
+ throw new Error('Provider request must be a JSON object');
50
+ return value;
51
+ }
52
+ export async function handleProvidersService(input) {
53
+ try {
54
+ if (!isRecord(input) || typeof input.method !== 'string' || typeof input.pathname !== 'string'
55
+ || !input.pathname.startsWith('/api/providers')) {
56
+ throw new Error('Invalid Provider service request');
57
+ }
58
+ const parts = input.pathname.replace(/\/+$/u, '').split('/').slice(3).map(decodeURIComponent);
59
+ const query = input.query ?? {};
60
+ if (!isRecord(query) || Object.values(query).some(value => typeof value !== 'string'))
61
+ throw new Error('Invalid Provider query');
62
+ if (input.method === 'GET' && !parts.length) {
63
+ return response(200, { success: true, data: readProvidersSnapshot() });
64
+ }
65
+ if (input.method === 'POST' && !parts.length) {
66
+ const body = requestBody(input.body);
67
+ if (typeof body.revision !== 'string')
68
+ throw new Error('Provider revision is required');
69
+ return response(200, { success: true, data: await createProvider({ revision: body.revision, value: body.provider }) });
70
+ }
71
+ if (input.method === 'PATCH' && !parts.length) {
72
+ const body = requestBody(input.body);
73
+ if (typeof body.revision !== 'string' || !Array.isArray(body.changes))
74
+ throw new Error('Provider revision and changes are required');
75
+ const changes = [];
76
+ for (const raw of body.changes) {
77
+ if (!isRecord(raw) || typeof raw.providerId !== 'string')
78
+ throw new Error('Invalid provider change');
79
+ changes.push({ ...raw, providerId: raw.providerId, imports: await resolveImports(raw.imports) });
80
+ }
81
+ return response(200, { success: true, data: await saveProviderChanges({ revision: body.revision, changes }) });
82
+ }
83
+ if (input.method === 'GET' && parts[0] === 'import' && parts[1] === 'baseagents' && parts.length === 2) {
84
+ return response(200, { success: true, data: { baseagents: listBaseagentImportSources() } });
85
+ }
86
+ if (input.method === 'POST' && parts[0] === 'import' && parts[1] === 'text'
87
+ && (parts.length === 2 || (parts[2] === 'preview' && parts.length === 3))) {
88
+ const body = requestBody(input.body);
89
+ if (parts[2] === 'preview')
90
+ return response(200, { success: true, data: previewProviderImport(body.text) });
91
+ if (typeof body.revision !== 'string')
92
+ throw new Error('Provider revision is required');
93
+ return response(200, { success: true, data: await createProvider({ revision: body.revision, value: parseProviderImport(body.text) }) });
94
+ }
95
+ if (input.method === 'POST' && parts[0] === 'import' && parts[1] === 'baseagent' && parts.length === 2) {
96
+ const body = requestBody(input.body);
97
+ if (typeof body.revision !== 'string')
98
+ throw new Error('Provider revision is required');
99
+ const baseagent = typeof body.baseagent === 'string' ? body.baseagent : '';
100
+ if (body.agentAid !== undefined || body.providerId !== undefined)
101
+ throw new Error('Source Agent AID and Provider ID are not accepted; IDs are generated automatically');
102
+ return response(200, { success: true, data: await importOrMergeBaseagentProvider({ revision: body.revision, value: providerFromBaseagent(baseagent) }) });
103
+ }
104
+ if (input.method === 'POST' && parts[0] === 'import' && parts[1] === 'baseagent' && parts[2] === 'preview' && parts.length === 3) {
105
+ const body = requestBody(input.body);
106
+ if (typeof body.baseagent !== 'string')
107
+ throw new Error('Baseagent is required');
108
+ return response(200, { success: true, data: previewProviderValue(providerFromBaseagent(body.baseagent)).provider });
109
+ }
110
+ const providerId = parts[0];
111
+ if (!providerId || !/^[A-Za-z0-9_-]+$/u.test(providerId))
112
+ throw new Error('Invalid provider identifier');
113
+ if (input.method === 'POST' && parts[1] === 'models' && parts[2] === 'preview' && parts.length === 3) {
114
+ const body = requestBody(input.body);
115
+ if (typeof body.serviceType !== 'string')
116
+ throw new Error('Service type is required');
117
+ if (body.baseagent !== undefined && typeof body.baseagent !== 'string')
118
+ throw new Error('Baseagent must be a string');
119
+ const provider = resolveProviderDraftService({
120
+ providerId,
121
+ serviceType: body.serviceType,
122
+ baseagent: body.baseagent,
123
+ value: body.provider,
124
+ imports: await resolveImports(body.imports),
125
+ });
126
+ const snapshot = await previewProviderCatalog(provider, body.baseagent);
127
+ if (snapshot.freshness !== 'fresh') {
128
+ const diagnostic = snapshot.diagnostics[0];
129
+ return response(502, { success: false, code: 'PROVIDER_MODEL_TEST_FAILED',
130
+ error: `${diagnostic?.message || 'Model list test failed'}${diagnostic?.status ? ` (HTTP ${diagnostic.status})` : ''}` });
131
+ }
132
+ return response(200, { success: true, data: managementCatalog(snapshot, provider.modelOverrides) });
133
+ }
134
+ if (input.method === 'GET' && parts[1] === 'secret' && parts.length === 2) {
135
+ const provider = readProvidersFile().providers[providerId];
136
+ if (!provider)
137
+ return response(404, { success: false, error: 'Provider not found' });
138
+ return response(200, { success: true, data: { apiKey: provider.common.apiKey || '' } });
139
+ }
140
+ if (input.method === 'GET' && parts[1] === 'models' && parts.length === 2) {
141
+ const serviceType = query.serviceType || '';
142
+ const baseagent = query.baseagent || undefined;
143
+ if (query.view === 'selection') {
144
+ const snapshot = await getProviderCatalog(providerId, serviceType, query.refresh === 'true', false, baseagent);
145
+ return response(200, { success: true, data: snapshot });
146
+ }
147
+ const provider = resolveProviderService(providerId, serviceType);
148
+ const snapshot = await getProviderCatalog(providerId, serviceType, query.refresh === 'true', true);
149
+ return response(200, { success: true, data: managementCatalog(snapshot, provider.modelOverrides) });
150
+ }
151
+ if ((input.method === 'PUT' || input.method === 'DELETE') && parts.length === 1) {
152
+ const body = requestBody(input.body);
153
+ if (typeof body.revision !== 'string')
154
+ throw new Error('Provider revision is required');
155
+ return response(200, { success: true, data: await updateProvider({
156
+ providerId,
157
+ revision: body.revision,
158
+ value: body.provider,
159
+ remove: input.method === 'DELETE',
160
+ }) });
161
+ }
162
+ return response(405, { success: false, error: 'Method not allowed' });
163
+ }
164
+ catch (error) {
165
+ return response(error.statusCode || 400, {
166
+ success: false,
167
+ ...(error.code ? { code: error.code } : {}),
168
+ error: error.message || 'Provider operation failed',
169
+ ...(error.data !== undefined ? { data: error.data } : {}),
170
+ });
171
+ }
172
+ }
package/dist/index.js CHANGED
@@ -45,7 +45,7 @@ import { readFastaunVersion, readInstalledEvolcoreVersion, reportEvolcoreStartup
45
45
  import { recoverAllRoleMutationsSync } from './core/command/role-menu.js';
46
46
  import { HandoffRuntime } from './core/handoff/runtime.js';
47
47
  import { startDaemonControlAid } from './core/daemon-control.js';
48
- import { ownerResolver, getDaemonOwners } from './core/ownership/owner-resolver.js';
48
+ import { ownerResolver, getDaemonOwners, getPrimaryOwner } from './core/ownership/owner-resolver.js';
49
49
  import { cleanupStartupHandoffs, recoverStartupHandoffs, resumePendingSessions, createDaemonShutdown, daemonConversationTotalExecutionMs, daemonConversationWatchdogMs, daemonIdleTimeoutMs, } from './core/daemon-lifecycle.js';
50
50
  import { BootstrapService, createBootstrapCompletionHandler, createBootstrapOwnerBoundHandler } from './core/bootstrap-service.js';
51
51
  import { MessageCache } from './core/message/message-cache.js';
@@ -55,7 +55,10 @@ import { AgentApplicationService } from './core/agent-application-service.js';
55
55
  import { EventBus } from './core/event-bus.js';
56
56
  import { detectAvailableBaseagents, runBindBootstrapDaemon } from './core/daemon-bootstrap.js';
57
57
  import { migrateLegacyConsolidationBatches } from './core/memory/consolidation-context.js';
58
- import { ConsolidationQueue, createConsolidationFeedbackSender, createConsolidationSessionFinalizer, registerConsolidationSessionEvents, registerRelationConsolidationEvents, } from './core/memory/consolidation.js';
58
+ import { ConsolidationQueue, consolidationBlockedReason, createConsolidationFeedbackSender, createConsolidationSessionFinalizer, registerConsolidationSessionEvents, registerRelationConsolidationEvents, } from './core/memory/consolidation.js';
59
+ import { RelationIdleMonitor, relationInboundFromLog } from './core/memory/relation-idle-monitor.js';
60
+ import { RelationSweepRunner } from './core/memory/relation-sweep.js';
61
+ import { setInboundLogListener } from './core/message/message-log.js';
59
62
  import { StatsCollector } from './utils/stats.js';
60
63
  import { AidStatsCollector } from './utils/stats.js';
61
64
  import { initializeStatsPersistence } from './stats/writer.js';
@@ -86,6 +89,9 @@ import { DaemonChannel } from './channels/daemon.js';
86
89
  import { DAEMON_RUNTIME_EPOCH_ENV, ensureProcessManagedTempDir } from './cli/task-context.js';
87
90
  import fs from 'fs';
88
91
  import crypto from 'crypto';
92
+ import { getBaseagentSnapshot, loadedBaseagentIds } from './agents/baseagent-availability.js';
93
+ import { readHostInfo } from './utils/host-info.js';
94
+ import { queryStatusActivity, queryStatusActivityByAgent } from './stats/query.js';
89
95
  export { resolvePersistedRestartDelivery } from './core/message/outbound.js';
90
96
  export function readEvolcoreVersion() {
91
97
  return readInstalledEvolcoreVersion();
@@ -94,7 +100,7 @@ async function main() {
94
100
  // Service managers do not necessarily export TMPDIR. Establish the private
95
101
  // managed root before any runner or task runtime is initialized.
96
102
  ensureProcessManagedTempDir();
97
- const { runtimeIsDist, runtimeBuildIdentity, diskDistDir } = reportEvolcoreStartup(import.meta.url, message => logger.info(message));
103
+ const { runtimeIsDist, runtimeBuildIdentity, diskDistDir, diskDistId } = reportEvolcoreStartup(import.meta.url, message => logger.info(message));
98
104
  installChannelSdkConsoleFilter();
99
105
  console.error('[EvolCore] ✓ Console filter installed');
100
106
  logger.info(`EvolCore v${readEvolcoreVersion()} starting... (fastaun v${readFastaunVersion()})`);
@@ -258,7 +264,7 @@ async function main() {
258
264
  const statsCollector = new StatsCollector(eventBus);
259
265
  // Per-AID 消息统计收集器(累计,供 watch aid 实时展示)
260
266
  const aidStatsCollector = new AidStatsCollector(eventBus);
261
- const stopStatsPersistence = initializeStatsPersistence(paths.root, aidStatsCollector);
267
+ const stopStatsPersistence = initializeStatsPersistence(paths.root, aidStatsCollector, eventBus);
262
268
  // 初始化 SessionManager(文件系统后端)
263
269
  const resolveSessionIdentity = createSessionIdentityResolver(agentRegistry);
264
270
  const sessionManager = new SessionManager(paths.sessionsDir, eventBus, resolveSessionIdentity);
@@ -474,6 +480,45 @@ async function main() {
474
480
  const consolidationQueue = new ConsolidationQueue(daemonChannel);
475
481
  const enqueueConsolidation = (params) => consolidationQueue.enqueue(params);
476
482
  cmdHandler.setMemoryConsolidationLauncher(enqueueConsolidation);
483
+ // 关系级记忆整理:入站推后计时,空闲 / 兜底到期跑该关系的整理链(notes → generalize → promote → profile → forget)
484
+ const relationIdleMonitor = new RelationIdleMonitor({
485
+ statePath: path.join(paths.dataDir, 'relation-idle-state.json'),
486
+ getTiming: aid => agentRegistry.get(aid)?.config?.memory,
487
+ canSweep: aid => {
488
+ const agent = agentRegistry.get(aid);
489
+ if (!agent?.baseagent)
490
+ return false;
491
+ if (agent.config?.memory?.enableRelation === false)
492
+ return false;
493
+ return !consolidationBlockedReason(aid);
494
+ },
495
+ startSweep: request => relationSweeps.start(request),
496
+ });
497
+ const relationSweeps = new RelationSweepRunner({
498
+ enqueue: enqueueConsolidation,
499
+ getAgent: aid => agentRegistry.get(aid),
500
+ resolveOwner: async (aid) => {
501
+ await ownerResolver.refresh(aid);
502
+ return getPrimaryOwner(aid);
503
+ },
504
+ onSweepFinished: ({ selfAid, peerKey, touchedPeerKeys }) => {
505
+ relationIdleMonitor.onSweepFinished(selfAid, peerKey);
506
+ // 群链按发言人写入过的关系:它们不一定有自己的入站,各补一轮(不再传播)
507
+ for (const touched of touchedPeerKeys)
508
+ relationIdleMonitor.requestSweep(selfAid, touched, false);
509
+ },
510
+ });
511
+ consolidationQueue.setLaunchFailedListener(params => relationSweeps.onLaunchFailed(params));
512
+ relationIdleMonitor.load();
513
+ setInboundLogListener((chatDir, entry) => {
514
+ const inbound = relationInboundFromLog(chatDir, entry);
515
+ if (inbound)
516
+ relationIdleMonitor.recordInbound(inbound);
517
+ });
518
+ onShutdown(() => {
519
+ setInboundLogListener(undefined);
520
+ relationIdleMonitor.stop();
521
+ });
477
522
  const triggerControlToken = crypto.randomBytes(32).toString('base64url');
478
523
  fs.writeFileSync(resolvePaths().controlToken, triggerControlToken, { mode: 0o600 });
479
524
  try {
@@ -523,6 +568,7 @@ async function main() {
523
568
  releaseSlot: (aid, sessionId) => consolidationQueue.release(aid, sessionId),
524
569
  enqueueConsolidation,
525
570
  sendFeedback: sendConsolidationFeedback,
571
+ onSessionFinalized: info => relationSweeps.onSessionFinalized(info),
526
572
  });
527
573
  consolidationQueue.setFinalizer(finishConsolidationSession);
528
574
  registerConsolidationSessionEvents({
@@ -658,11 +704,42 @@ async function main() {
658
704
  cmdHandler.setDiagService(diagService);
659
705
  let shutdown;
660
706
  let pendingShutdownReason;
707
+ const hostInfo = readHostInfo(readEvolcoreVersion());
708
+ const baseagentSnapshot = await getBaseagentSnapshot({ loaded: loadedBaseagentIds(agentMap.keys()), probe: true });
709
+ const getStatusSnapshot = () => {
710
+ const now = Date.now();
711
+ const todayStart = new Date(new Date(now).getFullYear(), new Date(now).getMonth(), new Date(now).getDate()).getTime();
712
+ const lastHour = queryStatusActivity(paths.root, now - 3_600_000, now);
713
+ const today = queryStatusActivity(paths.root, todayStart, now);
714
+ const agents = agentRegistry.list();
715
+ const usageByAgent = queryStatusActivityByAgent(paths.root, todayStart, now, agents.map(agent => agent.aid));
716
+ return {
717
+ activity: { lastHour, today },
718
+ agents: agents.map(agent => {
719
+ const usage = usageByAgent.get(agent.aid);
720
+ const queueAgentName = agentRegistry.get(agent.aid)?.name ?? agent.name ?? agent.aid;
721
+ return {
722
+ aid: agent.aid,
723
+ name: agent.name ?? agent.aid,
724
+ status: agent.status,
725
+ baseagent: agent.baseagent ?? null,
726
+ model: agent.model ?? null,
727
+ effort: agent.effort ?? null,
728
+ queue: {
729
+ pending: messageQueue.getQueueLengthByAgent(queueAgentName),
730
+ processing: messageQueue.getProcessingCountByAgent(queueAgentName),
731
+ },
732
+ usage: { calls: usage.modelCalls, tokens: usage.tokens, cost: usage.cost },
733
+ };
734
+ }),
735
+ };
736
+ };
661
737
  const ipcServer = new IpcServer(resolvePaths().socket, createIpcStatusProvider({
662
738
  channelInstances,
663
739
  statsCollector,
664
740
  agentRegistry,
665
741
  messageQueue,
742
+ runMode: devMode ? 'dev' : 'install',
666
743
  runtimeIsDist,
667
744
  runtimeBuildIdentity,
668
745
  diskDistDir,
@@ -671,6 +748,15 @@ async function main() {
671
748
  getAgentRuntimeError: () => agentRuntimeError,
672
749
  daemonAid: daemonCfg.aid,
673
750
  getControlAidState: () => controlChannel?.getAidState(),
751
+ host: hostInfo,
752
+ baseagents: baseagentSnapshot,
753
+ diskDistId,
754
+ getTriggerSummary: () => {
755
+ const now = new Date();
756
+ const from = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
757
+ return triggerSchedulers.statusSummary(from, Date.now());
758
+ },
759
+ getStatusSnapshot,
674
760
  }), async (argv, sessionId, delegationToken, delegationCommandHash) => {
675
761
  const delegation = agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
676
762
  if (!delegation.ok)
@@ -794,6 +880,7 @@ async function main() {
794
880
  agentApplicationService,
795
881
  agentInitializationService,
796
882
  enqueueConsolidation,
883
+ requestRelationSweep: (selfAid, peerKey) => relationIdleMonitor.requestSweep(selfAid, peerKey, true),
797
884
  completeAgentBootstrap,
798
885
  contactBinds: {
799
886
  dingtalk: { register: registerPendingDingtalkContactBind, isChannelReady: isContactBindChannelReady },
@@ -854,6 +941,7 @@ async function main() {
854
941
  })),
855
942
  ]);
856
943
  sessionIdleMonitor.activate();
944
+ relationIdleMonitor.start();
857
945
  controlChannel = startDaemonControlAid({
858
946
  daemonCfg,
859
947
  cmdHandler,
package/dist/ipc.js CHANGED
@@ -25,6 +25,7 @@ import { RUNTIME_ENVIRONMENT_NAMES, setRuntimeEnvironmentOverride } from './conf
25
25
  import { MAIN_CLI_BIN } from './product.js';
26
26
  import { ownerResolver } from './core/ownership/owner-resolver.js';
27
27
  import { parseOwnershipCommand, executeOwnershipCommand, ownershipFailure } from './aun/aid/ownership-operation.js';
28
+ import { ECWEB_SERVICE_PROTOCOL, validateEcwebClientProtocolRequirements, } from './contracts/ecweb-service-protocol.js';
28
29
  import { delegationDiagnosticMessage, withDelegationDiagnosticContext, } from './core/auth/delegation-diagnostics.js';
29
30
  const isWindows = process.platform === 'win32';
30
31
  const isNamedPipe = (p) => isWindows && p.toLowerCase().startsWith('\\\\.\\pipe\\');
@@ -53,6 +54,7 @@ function parseRuntimeEnvironmentInput(value) {
53
54
  return { ok: true, values };
54
55
  }
55
56
  const LOCAL_CONTROL_COMMANDS = new Set([
57
+ 'service.handshake',
56
58
  'bind.begin',
57
59
  'bind.status',
58
60
  'bind.cancel',
@@ -70,7 +72,10 @@ const LOCAL_CONTROL_COMMANDS = new Set([
70
72
  'evolagent.load',
71
73
  'evolagent.resync',
72
74
  'menu.exec',
75
+ 'ecweb.providers',
76
+ 'ecweb.basic-settings',
73
77
  'model.catalog',
78
+ 'baseagent.snapshot',
74
79
  'monitor-snapshot',
75
80
  'monitor-subscribe',
76
81
  'monitor-unsubscribe',
@@ -1094,6 +1099,14 @@ export class IpcServer {
1094
1099
  return this.diagExecutor ? this.diagExecutor(cmd) : { ok: false, code: 'NOT_CONFIGURED', message: '诊断入口未配置' };
1095
1100
  case 'menu-client.request':
1096
1101
  return this.menuClientExecutor ? this.menuClientExecutor(cmd) : { ok: false, code: 'NOT_CONFIGURED', error: 'Managed Menu client is not configured' };
1102
+ case 'ecweb.providers': {
1103
+ const { handleProvidersService } = await import('./ecweb/providers-service.js');
1104
+ return handleProvidersService(cmd.request);
1105
+ }
1106
+ case 'ecweb.basic-settings': {
1107
+ const { handleBasicSettingsService } = await import('./ecweb/basic-settings-service.js');
1108
+ return handleBasicSettingsService(cmd.request);
1109
+ }
1097
1110
  case 'model.catalog': {
1098
1111
  if ((cmd.self !== undefined && typeof cmd.self !== 'string')
1099
1112
  || (cmd.baseagent !== undefined && !['claude', 'codex', 'ecagent', 'gemini'].includes(cmd.baseagent))) {
@@ -1113,6 +1126,25 @@ export class IpcServer {
1113
1126
  return { ok: false, error: 'Unable to resolve model catalog configuration' };
1114
1127
  }
1115
1128
  }
1129
+ case 'baseagent.snapshot': {
1130
+ if (cmd.probe !== undefined && typeof cmd.probe !== 'boolean') {
1131
+ return { ok: false, code: 'INVALID_REQUEST', error: 'probe must be a boolean' };
1132
+ }
1133
+ try {
1134
+ const { getBaseagentSnapshot } = await import('./agents/baseagent-availability.js');
1135
+ return {
1136
+ ok: true,
1137
+ snapshot: await getBaseagentSnapshot({ probe: cmd.probe !== false }),
1138
+ };
1139
+ }
1140
+ catch (error) {
1141
+ return {
1142
+ ok: false,
1143
+ code: 'BASEAGENT_SNAPSHOT_UNAVAILABLE',
1144
+ error: error instanceof Error ? error.message : String(error),
1145
+ };
1146
+ }
1147
+ }
1116
1148
  case 'status':
1117
1149
  return this.getStatus();
1118
1150
  case 'codex-pre-tool-use': {
@@ -1152,6 +1184,34 @@ export class IpcServer {
1152
1184
  protocolVersion: 1,
1153
1185
  uptime: Math.round(process.uptime() * 1000),
1154
1186
  };
1187
+ case 'service.handshake': {
1188
+ const protocol = {
1189
+ major: ECWEB_SERVICE_PROTOCOL.major,
1190
+ minor: ECWEB_SERVICE_PROTOCOL.minor,
1191
+ };
1192
+ const requirementsError = validateEcwebClientProtocolRequirements(cmd.requirements);
1193
+ const error = cmd.service !== ECWEB_SERVICE_PROTOCOL.name
1194
+ ? `Unsupported service: ${String(cmd.service ?? 'missing')}`
1195
+ : requirementsError;
1196
+ if (error) {
1197
+ return {
1198
+ ok: false,
1199
+ code: 'SERVICE_PROTOCOL_MISMATCH',
1200
+ service: ECWEB_SERVICE_PROTOCOL.name,
1201
+ protocol,
1202
+ capabilities: ECWEB_SERVICE_PROTOCOL.capabilities,
1203
+ compatible: false,
1204
+ error,
1205
+ };
1206
+ }
1207
+ return {
1208
+ ok: true,
1209
+ service: ECWEB_SERVICE_PROTOCOL.name,
1210
+ protocol,
1211
+ capabilities: ECWEB_SERVICE_PROTOCOL.capabilities,
1212
+ compatible: true,
1213
+ };
1214
+ }
1155
1215
  case 'shutdown': {
1156
1216
  if (!this.shutdownExecutor)
1157
1217
  return { ok: false, error: 'shutdown executor not configured' };
@@ -1849,7 +1909,7 @@ export class IpcServer {
1849
1909
  }
1850
1910
  }
1851
1911
  case 'memory.create': {
1852
- const { sessionId, scope, slug, description, content, tags, keywords, replaces, targetPeerKey, generalizedFrom, promotedFrom } = cmd;
1912
+ const { sessionId, scope, slug, description, content, tags, keywords, replaces, targetPeerKey, generalizedFrom, promotedFrom, reason } = cmd;
1853
1913
  if (!sessionId)
1854
1914
  return { ok: false, code: 'INVALID_REQUEST', error: 'missing sessionId' };
1855
1915
  try {
@@ -1876,12 +1936,12 @@ export class IpcServer {
1876
1936
  catch (e) {
1877
1937
  const batchDir = guard.taskCtx.consolidationBatchDir;
1878
1938
  if (batchDir)
1879
- appendOpsRecord(batchDir, { ts: new Date().toISOString(), op: 'create', scope: scope ?? '', target: slug ?? '', ok: false, error: e?.message });
1939
+ appendOpsRecord(batchDir, { ts: new Date().toISOString(), op: 'create', scope: scope ?? '', target: slug ?? '', ok: false, error: e?.message, ...(reason ? { reason } : {}) });
1880
1940
  throw e;
1881
1941
  }
1882
1942
  const batchDir = guard.taskCtx.consolidationBatchDir;
1883
1943
  if (batchDir)
1884
- appendOpsRecord(batchDir, { ts: new Date().toISOString(), op: 'create', scope: scope ?? '', target: slug ?? '', filePath: createResult.filePath, archivedFiles: createResult.archived.map((dest, i) => ({ source: (Array.isArray(replaces) ? replaces : [])[i] ?? dest, archivedTo: dest })), tags: Array.isArray(tags) ? tags : [], memoryDir: createResult.scopeDirs.memoryDir, ok: true });
1944
+ appendOpsRecord(batchDir, { ts: new Date().toISOString(), op: 'create', scope: scope ?? '', target: slug ?? '', filePath: createResult.filePath, archivedFiles: createResult.archived.map((dest, i) => ({ source: createResult.archivedSources[i] ?? dest, archivedTo: dest })), tags: Array.isArray(tags) ? tags : [], memoryDir: createResult.scopeDirs.memoryDir, ok: true, ...(reason ? { reason } : {}) });
1885
1945
  return { ok: true, filePath: createResult.filePath, archived: createResult.archived };
1886
1946
  }
1887
1947
  catch (e) {
@@ -1889,7 +1949,7 @@ export class IpcServer {
1889
1949
  }
1890
1950
  }
1891
1951
  case 'memory.update': {
1892
- const { sessionId, filePath, tags, touch } = cmd;
1952
+ const { sessionId, filePath, tags, touch, reason } = cmd;
1893
1953
  if (!sessionId)
1894
1954
  return { ok: false, code: 'INVALID_REQUEST', error: 'missing sessionId' };
1895
1955
  try {
@@ -1912,12 +1972,14 @@ export class IpcServer {
1912
1972
  catch (e) {
1913
1973
  const batchDir = guard.taskCtx.consolidationBatchDir;
1914
1974
  if (batchDir)
1915
- appendOpsRecord(batchDir, { ts: new Date().toISOString(), op: 'update', scope: guard.writeScope, target: filePath ?? '', ok: false, error: e?.message });
1975
+ appendOpsRecord(batchDir, { ts: new Date().toISOString(), op: 'update', scope: guard.writeScope, target: filePath ?? '', ok: false, error: e?.message, ...(reason ? { reason } : {}) });
1916
1976
  throw e;
1917
1977
  }
1918
1978
  const batchDirU = guard.taskCtx.consolidationBatchDir;
1979
+ // target 记解析后的绝对路径:回滚直接拿它读写文件,argv 原文(如 consolidated/x.md)
1980
+ // 会相对 daemon 的 cwd 解析而找不到。memoryDir 记文件实际所在层,供收尾重建索引。
1919
1981
  if (batchDirU)
1920
- appendOpsRecord(batchDirU, { ts: new Date().toISOString(), op: 'update', scope: guard.writeScope, target: filePath ?? '', tags: Array.isArray(tags) ? tags : undefined, prevTags: updateResult.prevTags, memoryDir: updateResult.scopeDirs.memoryDir, ok: true });
1982
+ appendOpsRecord(batchDirU, { ts: new Date().toISOString(), op: 'update', scope: guard.writeScope, target: updateResult.filePath, tags: Array.isArray(tags) ? tags : undefined, prevTags: updateResult.prevTags, memoryDir: updateResult.targetMemoryDir, ok: true, ...(reason ? { reason } : {}) });
1921
1983
  return { ok: true, filePath: updateResult.filePath };
1922
1984
  }
1923
1985
  catch (e) {
@@ -1925,14 +1987,14 @@ export class IpcServer {
1925
1987
  }
1926
1988
  }
1927
1989
  case 'memory.archive': {
1928
- const { sessionId, filePaths } = cmd;
1990
+ const { sessionId, filePaths, reason } = cmd;
1929
1991
  if (!sessionId)
1930
1992
  return { ok: false, code: 'INVALID_REQUEST', error: 'missing sessionId' };
1931
1993
  try {
1932
1994
  const guard = await this.assertMemoryWriteSession(sessionId, undefined, 'archive', undefined, cmd.debug === true);
1933
1995
  if ('error' in guard)
1934
1996
  return guard.error;
1935
- const { archiveMemory } = await import('./core/memory/memory-writer.js');
1997
+ const { archiveMemory, sourceMemoryDirOf } = await import('./core/memory/memory-writer.js');
1936
1998
  const { appendOpsRecord } = await import('./core/memory/consolidation-ops-records.js');
1937
1999
  let archiveResult;
1938
2000
  try {
@@ -1946,15 +2008,20 @@ export class IpcServer {
1946
2008
  catch (e) {
1947
2009
  const batchDir = guard.taskCtx.consolidationBatchDir;
1948
2010
  if (batchDir)
1949
- appendOpsRecord(batchDir, { ts: new Date().toISOString(), op: 'archive', scope: guard.writeScope, target: (Array.isArray(filePaths) ? filePaths : []).join(', '), ok: false, error: e?.message });
2011
+ appendOpsRecord(batchDir, { ts: new Date().toISOString(), op: 'archive', scope: guard.writeScope, target: (Array.isArray(filePaths) ? filePaths : []).join(', '), ok: false, error: e?.message, ...(reason ? { reason } : {}) });
1950
2012
  throw e;
1951
2013
  }
1952
2014
  const batchDirA = guard.taskCtx.consolidationBatchDir;
1953
2015
  if (batchDirA) {
1954
2016
  for (let i = 0; i < archiveResult.archived.length; i++) {
1955
2017
  const archivedTo = archiveResult.archived[i];
1956
- const sourceFile = (Array.isArray(filePaths) ? filePaths : [])[i] ?? archivedTo;
1957
- appendOpsRecord(batchDirA, { ts: new Date().toISOString(), op: 'archive', scope: guard.writeScope, target: archivedTo, sourceFile, archivedTo, memoryDir: archiveResult.scopeDirs.memoryDir, ok: true });
2018
+ // 用解析后的绝对路径,不用 argv 原文:回滚直接拿 sourceFile 做 renameSync,
2019
+ // 相对路径会相对 daemon 的 cwd 解析,跨关系回滚会落到不相干的位置。
2020
+ const sourceFile = archiveResult.archivedSources[i] ?? archivedTo;
2021
+ // memoryDir 取**源文件所属层**,不是本作用域:collectAffectedScopes 靠它
2022
+ // 决定收尾重建哪几层索引,写成本作用域会导致关系级索引不重建。
2023
+ const srcMemoryDir = sourceMemoryDirOf(sourceFile);
2024
+ appendOpsRecord(batchDirA, { ts: new Date().toISOString(), op: 'archive', scope: guard.writeScope, target: archivedTo, sourceFile, archivedTo, memoryDir: srcMemoryDir, ok: true, ...(reason ? { reason } : {}) });
1958
2025
  }
1959
2026
  }
1960
2027
  return { ok: true, archived: archiveResult.archived };
package/dist/stats/db.js CHANGED
@@ -28,6 +28,10 @@ export function getStatsDir(evolcoreHome) {
28
28
  export function getDbPath(evolcoreHome) {
29
29
  return path.join(getStatsDir(evolcoreHome), 'usage.db');
30
30
  }
31
+ const ACTIVITY_EVENT_RETENTION_MS = 48 * 60 * 60 * 1000;
32
+ function pruneActivityEvents(db, now = Date.now()) {
33
+ db.prepare('DELETE FROM activity_events WHERE ts < ?').run(now - ACTIVITY_EVENT_RETENTION_MS);
34
+ }
31
35
  export function closeStatsDb() {
32
36
  if (!_db)
33
37
  return;
@@ -73,6 +77,12 @@ export function startDailyRollupMaintenance(evolcoreHome) {
73
77
  const db = getDb(evolcoreHome);
74
78
  if (!db)
75
79
  return undefined;
80
+ try {
81
+ pruneActivityEvents(db);
82
+ }
83
+ catch (error) {
84
+ logger.warn(`[Stats] activity_events 清理失败(非致命): ${error}`);
85
+ }
76
86
  try {
77
87
  const daily = db.prepare('SELECT COUNT(*) AS n FROM usage_daily').get();
78
88
  const events = db.prepare('SELECT COUNT(*) AS n FROM usage_events').get();
@@ -85,6 +95,12 @@ export function startDailyRollupMaintenance(evolcoreHome) {
85
95
  logger.warn(`[Stats] usage_daily 回填检测失败(非致命): ${error}`);
86
96
  }
87
97
  return setInterval(() => {
98
+ try {
99
+ pruneActivityEvents(db);
100
+ }
101
+ catch (error) {
102
+ logger.warn(`[Stats] activity_events 清理失败(非致命): ${error}`);
103
+ }
88
104
  try {
89
105
  rebuildDailyRollup(evolcoreHome);
90
106
  }
@@ -179,6 +195,22 @@ function _initTables(db) {
179
195
  CREATE INDEX IF NOT EXISTS idx_me_agent_ts ON message_events(agent_aid, ts);
180
196
  CREATE INDEX IF NOT EXISTS idx_me_peer_ts ON message_events(agent_aid, peer_key, ts);
181
197
 
198
+ -- Persisted task lifecycle counters used by status activity windows.
199
+ CREATE TABLE IF NOT EXISTS activity_events (
200
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
201
+ ts INTEGER NOT NULL,
202
+ agent_aid TEXT NOT NULL DEFAULT '',
203
+ type TEXT NOT NULL,
204
+ duration_ms INTEGER,
205
+ error_type TEXT
206
+ );
207
+ CREATE INDEX IF NOT EXISTS idx_ae_ts ON activity_events(ts);
208
+ CREATE INDEX IF NOT EXISTS idx_ae_agent_ts ON activity_events(agent_aid, ts);
209
+ CREATE TABLE IF NOT EXISTS activity_meta (
210
+ key TEXT PRIMARY KEY,
211
+ value TEXT NOT NULL
212
+ );
213
+
182
214
  -- 按天预聚合表:grain = 天 × agent × peer × session × model × billing_fn。
183
215
  -- 由 writer.ts 写时增量 UPSERT 维护,rebuildDailyRollup() 全量重建纠偏。
184
216
  -- 保留 model+billing_fn 维度以便查询时仍按 calcCost 现算成本。
@@ -240,6 +272,7 @@ function _initTables(db) {
240
272
  CREATE INDEX IF NOT EXISTS idx_mc_agentsession ON model_calls(agent_session_id);
241
273
  CREATE INDEX IF NOT EXISTS idx_mc_ts ON model_calls(ts);
242
274
  `);
275
+ db.prepare(`INSERT OR IGNORE INTO activity_meta (key, value) VALUES ('activity_events_started_at', ?)`).run(String(Date.now()));
243
276
  // ── Schema Migration: 添加 cost 列 ───────────────────────────────────────
244
277
  try {
245
278
  const cols = db.prepare(`PRAGMA table_info(usage_events)`).all();
@@ -3,9 +3,9 @@
3
3
  */
4
4
  export { getDb, openReadonlyDb, getStatsDir, getDbPath, closeStatsDb, archiveOldData, listArchivePaths, rebuildDailyRollup } from './db.js';
5
5
  export { normalizeUsage } from './normalizer.js';
6
- export { initializeStatsPersistence, insertUsageEvent, insertContextBreakdown, insertMessageEvent, insertModelCalls } from './writer.js';
6
+ export { initializeStatsPersistence, insertUsageEvent, insertContextBreakdown, insertMessageEvent, insertActivityEvent, insertModelCalls } from './writer.js';
7
7
  export { calcCost, resolvePriceRow, resolveCanonicalModel, registerBillingFn } from './billing.js';
8
- export { queryAggregated, queryTodaySummary, querySessionTurns, queryContextBreakdown, queryTopPeers, queryTopModels, queryMessageAggregated, queryMessageTodaySummary, queryPeerList, querySummary, queryPeerDaily, queryTaskModelCalls, querySessionModelCalls } from './query.js';
8
+ export { queryAggregated, queryTodaySummary, queryStatusActivity, queryStatusActivityByAgent, querySessionTurns, queryContextBreakdown, queryTopPeers, queryTopModels, queryMessageAggregated, queryMessageTodaySummary, queryPeerList, querySummary, queryPeerDaily, queryTaskModelCalls, querySessionModelCalls } from './query.js';
9
9
  export { getBudgetStatus } from './budget.js';
10
10
  export { getRoleBudgetStatus, formatUsageSubjectKey } from './role-budget.js';
11
11
  export { buildSystemVars, buildTurnVars } from './eck-vars.js';