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
@@ -4,6 +4,252 @@
4
4
  */
5
5
  import { openReadonlyDb, getDbPath, listArchivePaths } from './db.js';
6
6
  import { calcCost } from './billing.js'; // 保留用于尚未迁移的函数
7
+ function emptyStatusActivity() {
8
+ return {
9
+ received: null, sent: null, completed: null, errors: null, interrupts: null, avgResponseMs: null, modelCalls: null,
10
+ tokens: { input: null, output: null, cacheRead: null, cacheCreation: null, total: null },
11
+ cost: {
12
+ official: { usd: null, cny: null },
13
+ gateway: { usd: null, cny: null },
14
+ coverage: {
15
+ rows: 0,
16
+ official: { usdPricedRows: 0, cnyPricedRows: 0 },
17
+ gateway: { usdPricedRows: 0, cnyPricedRows: 0 },
18
+ },
19
+ },
20
+ };
21
+ }
22
+ function statusCostValue(sum, rows, pricedRows) {
23
+ if (rows === 0)
24
+ return 0;
25
+ if (pricedRows === 0)
26
+ return null;
27
+ return Number(sum ?? 0);
28
+ }
29
+ /**
30
+ * Read the status activity window from the persisted stats database.
31
+ * Message and model-call counters are intentionally sourced from their
32
+ * dedicated tables; task lifecycle counters come from activity_events.
33
+ * A missing/unreadable database returns null-valued metrics rather than
34
+ * pretending that no activity occurred.
35
+ */
36
+ export function queryStatusActivity(evolcoreHome, fromTs, toTs, agentAid) {
37
+ const result = emptyStatusActivity();
38
+ const db = openReadonlyDb(getDbPath(evolcoreHome));
39
+ if (!db)
40
+ return result;
41
+ const agentClause = agentAid ? ' AND agent_aid = ?' : '';
42
+ const params = agentAid ? [fromTs, toTs, agentAid] : [fromTs, toTs];
43
+ try {
44
+ const msg = db.prepare(`
45
+ SELECT
46
+ COALESCE(SUM(CASE WHEN direction = 'in' THEN 1 ELSE 0 END), 0) AS received,
47
+ COALESCE(SUM(CASE WHEN direction = 'out' THEN 1 ELSE 0 END), 0) AS sent
48
+ FROM message_events WHERE ts >= ? AND ts < ?${agentClause}
49
+ `).get(...params);
50
+ result.received = Number(msg?.received ?? 0);
51
+ result.sent = Number(msg?.sent ?? 0);
52
+ const calls = db.prepare(`
53
+ SELECT COUNT(*) AS calls
54
+ FROM model_calls WHERE ts >= ? AND ts < ?${agentClause}
55
+ `).get(...params);
56
+ result.modelCalls = Number(calls?.calls ?? 0);
57
+ const usage = db.prepare(`
58
+ SELECT
59
+ COUNT(*) AS rows,
60
+ COALESCE(SUM(input_tokens), 0) AS input,
61
+ COALESCE(SUM(output_tokens), 0) AS output,
62
+ COALESCE(SUM(cache_read_tokens), 0) AS cache_read,
63
+ COALESCE(SUM(cache_creation_tokens), 0) AS cache_creation,
64
+ SUM(cost_official_usd) AS official_usd,
65
+ SUM(cost_official_cny) AS official_cny,
66
+ SUM(cost_gateway_usd) AS gateway_usd,
67
+ SUM(cost_gateway_cny) AS gateway_cny,
68
+ COUNT(cost_official_usd) AS official_usd_rows,
69
+ COUNT(cost_official_cny) AS official_cny_rows,
70
+ COUNT(cost_gateway_usd) AS gateway_usd_rows,
71
+ COUNT(cost_gateway_cny) AS gateway_cny_rows
72
+ FROM usage_events WHERE ts >= ? AND ts < ?${agentClause}
73
+ `).get(...params);
74
+ const rows = Number(usage?.rows ?? 0);
75
+ result.tokens = {
76
+ input: Number(usage?.input ?? 0),
77
+ output: Number(usage?.output ?? 0),
78
+ cacheRead: Number(usage?.cache_read ?? 0),
79
+ cacheCreation: Number(usage?.cache_creation ?? 0),
80
+ total: Number(usage?.input ?? 0) + Number(usage?.output ?? 0)
81
+ + Number(usage?.cache_read ?? 0) + Number(usage?.cache_creation ?? 0),
82
+ };
83
+ result.cost = {
84
+ official: {
85
+ usd: statusCostValue(usage?.official_usd, rows, Number(usage?.official_usd_rows ?? 0)),
86
+ cny: statusCostValue(usage?.official_cny, rows, Number(usage?.official_cny_rows ?? 0)),
87
+ },
88
+ gateway: {
89
+ usd: statusCostValue(usage?.gateway_usd, rows, Number(usage?.gateway_usd_rows ?? 0)),
90
+ cny: statusCostValue(usage?.gateway_cny, rows, Number(usage?.gateway_cny_rows ?? 0)),
91
+ },
92
+ coverage: {
93
+ rows,
94
+ official: {
95
+ usdPricedRows: Number(usage?.official_usd_rows ?? 0),
96
+ cnyPricedRows: Number(usage?.official_cny_rows ?? 0),
97
+ },
98
+ gateway: {
99
+ usdPricedRows: Number(usage?.gateway_usd_rows ?? 0),
100
+ cnyPricedRows: Number(usage?.gateway_cny_rows ?? 0),
101
+ },
102
+ },
103
+ };
104
+ const startedAtRow = db.prepare(`SELECT value FROM activity_meta WHERE key = 'activity_events_started_at'`).get();
105
+ const activityStartedAt = Number(startedAtRow?.value);
106
+ if (!Number.isFinite(activityStartedAt) || fromTs < activityStartedAt) {
107
+ return result;
108
+ }
109
+ const activity = db.prepare(`
110
+ SELECT
111
+ SUM(CASE WHEN type = 'completed' THEN 1 ELSE 0 END) AS completed,
112
+ SUM(CASE WHEN type = 'error' THEN 1 ELSE 0 END) AS errors,
113
+ SUM(CASE WHEN type = 'interrupted' THEN 1 ELSE 0 END) AS interrupts,
114
+ AVG(CASE WHEN type = 'completed' THEN duration_ms END) AS avg_response
115
+ FROM activity_events WHERE ts >= ? AND ts < ?${agentClause}
116
+ `).get(...params);
117
+ result.completed = Number(activity?.completed ?? 0);
118
+ result.errors = Number(activity?.errors ?? 0);
119
+ result.interrupts = Number(activity?.interrupts ?? 0);
120
+ result.avgResponseMs = activity?.avg_response == null ? 0 : Number(activity.avg_response);
121
+ return result;
122
+ }
123
+ catch {
124
+ // Older databases may not have activity_events yet. Preserve metrics
125
+ // that can be read from established tables and leave lifecycle fields unknown.
126
+ return result;
127
+ }
128
+ finally {
129
+ db.close();
130
+ }
131
+ }
132
+ /** Batch status query for Agent rows; opens the stats DB once for all AIDs. */
133
+ export function queryStatusActivityByAgent(evolcoreHome, fromTs, toTs, agentAids) {
134
+ const output = new Map(agentAids.map(aid => [aid, emptyStatusActivity()]));
135
+ if (agentAids.length === 0)
136
+ return output;
137
+ const db = openReadonlyDb(getDbPath(evolcoreHome));
138
+ if (!db)
139
+ return output;
140
+ const placeholders = agentAids.map(() => '?').join(', ');
141
+ const params = [fromTs, toTs, ...agentAids];
142
+ const get = (aid) => output.get(aid);
143
+ try {
144
+ // An opened database can prove that an agent has no rows in the queried
145
+ // persistent tables. Keep lifecycle fields unknown until their coverage
146
+ // boundary is verified, but expose confirmed zeroes for message, model,
147
+ // token, and cost aggregates.
148
+ for (const item of output.values()) {
149
+ item.received = 0;
150
+ item.sent = 0;
151
+ item.modelCalls = 0;
152
+ item.tokens = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, total: 0 };
153
+ item.cost = {
154
+ official: { usd: 0, cny: 0 },
155
+ gateway: { usd: 0, cny: 0 },
156
+ coverage: {
157
+ rows: 0,
158
+ official: { usdPricedRows: 0, cnyPricedRows: 0 },
159
+ gateway: { usdPricedRows: 0, cnyPricedRows: 0 },
160
+ },
161
+ };
162
+ }
163
+ for (const row of db.prepare(`
164
+ SELECT agent_aid,
165
+ SUM(CASE WHEN direction = 'in' THEN 1 ELSE 0 END) AS received,
166
+ SUM(CASE WHEN direction = 'out' THEN 1 ELSE 0 END) AS sent
167
+ FROM message_events WHERE ts >= ? AND ts < ? AND agent_aid IN (${placeholders}) GROUP BY agent_aid
168
+ `).all(...params)) {
169
+ const item = get(row.agent_aid);
170
+ item.received = Number(row.received ?? 0);
171
+ item.sent = Number(row.sent ?? 0);
172
+ }
173
+ for (const row of db.prepare(`
174
+ SELECT agent_aid, COUNT(*) AS calls
175
+ FROM model_calls WHERE ts >= ? AND ts < ? AND agent_aid IN (${placeholders}) GROUP BY agent_aid
176
+ `).all(...params))
177
+ get(row.agent_aid).modelCalls = Number(row.calls ?? 0);
178
+ for (const row of db.prepare(`
179
+ SELECT agent_aid, COUNT(*) AS rows,
180
+ SUM(input_tokens) AS input, SUM(output_tokens) AS output,
181
+ SUM(cache_read_tokens) AS cache_read, SUM(cache_creation_tokens) AS cache_creation,
182
+ SUM(cost_official_usd) AS official_usd, SUM(cost_official_cny) AS official_cny,
183
+ SUM(cost_gateway_usd) AS gateway_usd, SUM(cost_gateway_cny) AS gateway_cny,
184
+ COUNT(cost_official_usd) AS official_usd_rows, COUNT(cost_official_cny) AS official_cny_rows,
185
+ COUNT(cost_gateway_usd) AS gateway_usd_rows, COUNT(cost_gateway_cny) AS gateway_cny_rows
186
+ FROM usage_events WHERE ts >= ? AND ts < ? AND agent_aid IN (${placeholders}) GROUP BY agent_aid
187
+ `).all(...params)) {
188
+ const item = get(row.agent_aid);
189
+ item.tokens = {
190
+ input: Number(row.input ?? 0), output: Number(row.output ?? 0),
191
+ cacheRead: Number(row.cache_read ?? 0), cacheCreation: Number(row.cache_creation ?? 0),
192
+ total: Number(row.input ?? 0) + Number(row.output ?? 0) + Number(row.cache_read ?? 0) + Number(row.cache_creation ?? 0),
193
+ };
194
+ const count = Number(row.rows ?? 0);
195
+ item.cost = {
196
+ official: {
197
+ usd: statusCostValue(row.official_usd, count, Number(row.official_usd_rows ?? 0)),
198
+ cny: statusCostValue(row.official_cny, count, Number(row.official_cny_rows ?? 0)),
199
+ },
200
+ gateway: {
201
+ usd: statusCostValue(row.gateway_usd, count, Number(row.gateway_usd_rows ?? 0)),
202
+ cny: statusCostValue(row.gateway_cny, count, Number(row.gateway_cny_rows ?? 0)),
203
+ },
204
+ coverage: {
205
+ rows: count,
206
+ official: {
207
+ usdPricedRows: Number(row.official_usd_rows ?? 0),
208
+ cnyPricedRows: Number(row.official_cny_rows ?? 0),
209
+ },
210
+ gateway: {
211
+ usdPricedRows: Number(row.gateway_usd_rows ?? 0),
212
+ cnyPricedRows: Number(row.gateway_cny_rows ?? 0),
213
+ },
214
+ },
215
+ };
216
+ }
217
+ const startedAtRow = db.prepare(`SELECT value FROM activity_meta WHERE key = 'activity_events_started_at'`).get();
218
+ const activityStartedAt = Number(startedAtRow?.value);
219
+ if (Number.isFinite(activityStartedAt) && fromTs >= activityStartedAt) {
220
+ for (const row of db.prepare(`
221
+ SELECT agent_aid,
222
+ SUM(CASE WHEN type = 'completed' THEN 1 ELSE 0 END) AS completed,
223
+ SUM(CASE WHEN type = 'error' THEN 1 ELSE 0 END) AS errors,
224
+ SUM(CASE WHEN type = 'interrupted' THEN 1 ELSE 0 END) AS interrupts,
225
+ AVG(CASE WHEN type = 'completed' THEN duration_ms END) AS avg_response
226
+ FROM activity_events WHERE ts >= ? AND ts < ? AND agent_aid IN (${placeholders}) GROUP BY agent_aid
227
+ `).all(...params)) {
228
+ const item = get(row.agent_aid);
229
+ item.completed = Number(row.completed ?? 0);
230
+ item.errors = Number(row.errors ?? 0);
231
+ item.interrupts = Number(row.interrupts ?? 0);
232
+ item.avgResponseMs = row.avg_response == null ? 0 : Number(row.avg_response);
233
+ }
234
+ for (const item of output.values()) {
235
+ if (item.completed === null) {
236
+ item.completed = 0;
237
+ item.errors = 0;
238
+ item.interrupts = 0;
239
+ item.avgResponseMs = 0;
240
+ }
241
+ }
242
+ }
243
+ }
244
+ catch {
245
+ // The status response can still report the other metric groups when a
246
+ // partially migrated database lacks an optional table.
247
+ }
248
+ finally {
249
+ db.close();
250
+ }
251
+ return output;
252
+ }
7
253
  const GRAN_FMT = {
8
254
  hour: '%Y-%m-%d %H:00',
9
255
  day: '%Y-%m-%d',
@@ -127,7 +127,21 @@ export function insertContextBreakdown(evolcoreHome, bd) {
127
127
  import('../utils/logger.js').then(({ logger }) => logger.warn(`[StatsWriter] insertContextBreakdown failed: ${e}`));
128
128
  }
129
129
  }
130
- export function initializeStatsPersistence(evolcoreHome, collector) {
130
+ export function insertActivityEvent(evolcoreHome, event) {
131
+ const db = getDb(evolcoreHome);
132
+ if (!db)
133
+ return;
134
+ try {
135
+ db.prepare(`
136
+ INSERT INTO activity_events (ts, agent_aid, type, duration_ms, error_type)
137
+ VALUES (?, ?, ?, ?, ?)
138
+ `).run(event.ts, event.agent_aid ?? '', event.type, event.duration_ms ?? null, event.error_type ?? null);
139
+ }
140
+ catch (error) {
141
+ logger.warn(`[StatsWriter] insertActivityEvent failed: ${error}`);
142
+ }
143
+ }
144
+ export function initializeStatsPersistence(evolcoreHome, collector, eventBus) {
131
145
  const previousOnMessage = collector.onMessage;
132
146
  const onMessage = (event) => {
133
147
  void Promise.resolve()
@@ -135,6 +149,28 @@ export function initializeStatsPersistence(evolcoreHome, collector) {
135
149
  .catch(() => { });
136
150
  };
137
151
  collector.onMessage = onMessage;
152
+ const activityHandlers = [];
153
+ const subscribeActivity = (type, handler) => {
154
+ if (!eventBus)
155
+ return;
156
+ eventBus.subscribe(type, handler);
157
+ activityHandlers.push([type, handler]);
158
+ };
159
+ subscribeActivity('task:completed', event => {
160
+ void Promise.resolve().then(() => insertActivityEvent(evolcoreHome, {
161
+ ts: event.timestamp ?? Date.now(), agent_aid: event.agentAid, type: 'completed', duration_ms: event.durationMs,
162
+ })).catch(() => { });
163
+ });
164
+ subscribeActivity('task:error', event => {
165
+ void Promise.resolve().then(() => insertActivityEvent(evolcoreHome, {
166
+ ts: event.timestamp ?? Date.now(), agent_aid: event.agentAid, type: 'error', error_type: event.errorType,
167
+ })).catch(() => { });
168
+ });
169
+ subscribeActivity('task:interrupted', event => {
170
+ void Promise.resolve().then(() => insertActivityEvent(evolcoreHome, {
171
+ ts: event.timestamp ?? Date.now(), agent_aid: event.agentAid, type: 'interrupted',
172
+ })).catch(() => { });
173
+ });
138
174
  let stopped = false;
139
175
  let maintenanceTimer;
140
176
  void Promise.resolve()
@@ -147,6 +183,10 @@ export function initializeStatsPersistence(evolcoreHome, collector) {
147
183
  stopped = true;
148
184
  if (collector.onMessage === onMessage)
149
185
  collector.onMessage = previousOnMessage;
186
+ if (eventBus) {
187
+ for (const [type, handler] of activityHandlers)
188
+ eventBus.unsubscribe(type, handler);
189
+ }
150
190
  if (maintenanceTimer)
151
191
  clearInterval(maintenanceTimer);
152
192
  };
@@ -15,6 +15,18 @@ export class TriggerSchedulerRegistry {
15
15
  values() {
16
16
  return this.schedulers.values();
17
17
  }
18
+ statusSummary(fromTs, toTs) {
19
+ let enabled = 0;
20
+ let calls = 0;
21
+ let failures = 0;
22
+ for (const scheduler of this.schedulers.values()) {
23
+ enabled += scheduler.list({ all: true }).filter(definition => definition.enabled).length;
24
+ const summary = scheduler.runSummary(fromTs, toTs);
25
+ calls += summary.calls;
26
+ failures += summary.failures;
27
+ }
28
+ return { enabled, today: { calls, failures } };
29
+ }
18
30
  async replace(agentAid, createScheduler) {
19
31
  await this.schedulers.get(agentAid)?.stop();
20
32
  const scheduler = createScheduler();
@@ -212,7 +212,7 @@ export function isTriggerOperator(definition, actorConversationPeerId) {
212
212
  return triggerOperatorPeerIds(definition).includes(actorConversationPeerId);
213
213
  }
214
214
  export async function authorizeTriggerExecution(definition, dependencies) {
215
- await refreshOwnershipForAuthorization(definition.agentAid);
215
+ await refreshOwnershipForAuthorization(definition.agentAid, { allowTransient: true });
216
216
  if (definition.execution.permissionMode === 'fullaccess') {
217
217
  if (!definition.authorizedBy) {
218
218
  return { allowed: false, reason: 'fullaccess Trigger approval provenance is missing' };
@@ -386,7 +386,7 @@ export async function authenticatedTriggerActor(dependencies, agentAid, actorSes
386
386
  throw new Error('trigger delegation actor does not match session peer');
387
387
  }
388
388
  const actorChatType = triggerOriginChatType(actorOrigin);
389
- await refreshOwnershipForAuthorization(agentAid);
389
+ await refreshOwnershipForAuthorization(agentAid, { allowTransient: true });
390
390
  const actorConversationId = actorChatType === 'group' ? actorOrigin.channelId : actorOrigin.peerId;
391
391
  const roleDetail = resolvePeerRoleDetail({
392
392
  selfAid: agentAid,
@@ -12,6 +12,7 @@ export class TriggerHistoryStore {
12
12
  agentAid;
13
13
  file;
14
14
  fallbackFile;
15
+ runSummaryCache = null;
15
16
  constructor(rootDir, agentAid) {
16
17
  this.rootDir = rootDir;
17
18
  this.agentAid = agentAid;
@@ -74,6 +75,41 @@ export class TriggerHistoryStore {
74
75
  }
75
76
  return out;
76
77
  }
78
+ runSummary(fromTs, toTs) {
79
+ const files = [this.file, this.fallbackFile].filter((value) => !!value);
80
+ const signature = files.map(file => {
81
+ try {
82
+ const stat = fs.statSync(file);
83
+ return `${file}:${stat.size}:${stat.mtimeMs}`;
84
+ }
85
+ catch {
86
+ return `${file}:missing`;
87
+ }
88
+ }).join('|');
89
+ if (this.runSummaryCache
90
+ && this.runSummaryCache.signature === signature
91
+ && this.runSummaryCache.fromTs === fromTs
92
+ && this.runSummaryCache.toTs === toTs) {
93
+ return { calls: this.runSummaryCache.calls, failures: this.runSummaryCache.failures };
94
+ }
95
+ let calls = 0;
96
+ let failures = 0;
97
+ for (const line of this.readLines()) {
98
+ const event = parseHistoryEvent(line, this.agentAid);
99
+ if (!event || !isRunEvent(event))
100
+ continue;
101
+ const finishedAt = event.audit.finishedAt;
102
+ if (finishedAt < fromTs || finishedAt >= toTs)
103
+ continue;
104
+ if (!['completed', 'degraded', 'noop', 'failed'].includes(event.audit.status))
105
+ continue;
106
+ calls++;
107
+ if (event.audit.status === 'failed')
108
+ failures++;
109
+ }
110
+ this.runSummaryCache = { signature, fromTs, toTs, calls, failures };
111
+ return { calls, failures };
112
+ }
77
113
  recentRuns(triggerId, limit = 20) {
78
114
  if (limit <= 0)
79
115
  return [];
@@ -194,6 +194,9 @@ export class TriggerRuntimeScheduler {
194
194
  history(triggerId, limit = 100) {
195
195
  return this.manager.history.events(triggerId, limit);
196
196
  }
197
+ runSummary(fromTs, toTs) {
198
+ return this.manager.history.runSummary(fromTs, toTs);
199
+ }
197
200
  recentRuns(triggerId, limit = 50) {
198
201
  return this.audit.recent(triggerId, limit).map(run => ({
199
202
  runId: run.runId,
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { spawn } from 'child_process';
4
- import { getPackageRoot, resolvePaths } from '../paths.js';
4
+ import { getPackageRoot, isDevMode, resolvePaths } from '../paths.js';
5
5
  import { WEB_PACKAGE_NAME } from '../product.js';
6
6
  import { resolveGlobalPkg } from './npm-ops.js';
7
7
  import { ecwebVersionRequirementError, resolveEcwebLaunchCommand } from './ecweb-utils.js';
@@ -69,10 +69,15 @@ function removeEntriesForPid(pid) {
69
69
  }
70
70
  async function probe(port) {
71
71
  try {
72
- const response = await fetch(`http://127.0.0.1:${port}/`, {
72
+ const response = await fetch(`http://127.0.0.1:${port}/api/health`, {
73
73
  signal: AbortSignal.timeout(1500),
74
74
  });
75
- return response.ok;
75
+ if (!response.ok)
76
+ return false;
77
+ const health = await response.json();
78
+ return health.ok === true
79
+ && health.service === 'ecweb'
80
+ && health.daemonProtocolCompatible === true;
76
81
  }
77
82
  catch {
78
83
  return false;
@@ -95,6 +100,7 @@ export function createEcwebSupervisor(options) {
95
100
  let timer = null;
96
101
  let failureCount = 0;
97
102
  let startInFlight = null;
103
+ let childStartedAt = 0;
98
104
  let stopping = false;
99
105
  const logFile = path.join(resolvePaths().logs, 'ecweb-daemon.log');
100
106
  const setActiveFromEntry = (entry, owned) => {
@@ -182,7 +188,10 @@ export function createEcwebSupervisor(options) {
182
188
  });
183
189
  let cwd = root;
184
190
  let version = globalPkg?.version;
185
- if (!launch && fs.existsSync(localEntry)) {
191
+ // The repository fallback is deliberately development-only. Installed
192
+ // daemons must resolve the separately packaged ec-web entry; otherwise a
193
+ // stale source-tree build could silently replace the installed service.
194
+ if (!launch && isDevMode() && fs.existsSync(localEntry)) {
186
195
  launch = {
187
196
  command: process.execPath,
188
197
  args: [localEntry, '--home', root, '--port', String(options.port)],
@@ -218,15 +227,15 @@ export function createEcwebSupervisor(options) {
218
227
  detached: true,
219
228
  stdio: ['ignore', childLogFd, childLogFd],
220
229
  windowsHide: platform.isWindows,
221
- // ECWeb dynamically imports EvolCore's dist modules for config,
222
- // roles, authorization and session compatibility. Pin that lookup
223
- // to the same package root as this daemon; otherwise a separately
224
- // installed ECWeb can resolve its peer dependency and silently load
225
- // a different (possibly stale) EvolCore copy from node_modules.
230
+ // Keep the runtime home and package root aligned with this daemon.
231
+ // The service protocol is the runtime boundary; package-root pinning
232
+ // remains useful for development-only compatibility code and static
233
+ // asset resolution.
226
234
  env: {
227
235
  ...process.env,
228
236
  EVOLCORE_HOME: root,
229
237
  EVOLCORE_PACKAGE_ROOT: getPackageRoot(),
238
+ EVOLCORE_ECWEB_SUPERVISED: '1',
230
239
  },
231
240
  });
232
241
  }
@@ -237,13 +246,16 @@ export function createEcwebSupervisor(options) {
237
246
  }
238
247
  closeLogStream();
239
248
  child = spawned;
249
+ childStartedAt = Date.now();
240
250
  spawned.once('error', (error) => {
241
251
  options.logger.error(`ECWeb 子进程创建失败: ${error.message}`);
242
252
  });
243
253
  spawned.once('exit', (code, signal) => {
244
254
  options.logger.warn(`ECWeb 进程退出 (PID: ${spawned.pid ?? 'unknown'}, code=${code ?? 'null'}, signal=${signal ?? 'null'})`);
245
- if (child === spawned)
255
+ if (child === spawned) {
246
256
  child = null;
257
+ childStartedAt = 0;
258
+ }
247
259
  if (!stopping) {
248
260
  active = null;
249
261
  failureCount = failureThreshold;
@@ -265,6 +277,14 @@ export function createEcwebSupervisor(options) {
265
277
  const ensureRunning = async () => {
266
278
  if (stopping || startInFlight)
267
279
  return;
280
+ // A supervised ECWeb waits for daemon IPC before it binds and writes its
281
+ // instance record. Do not mistake that startup window for a crashed
282
+ // service and spawn a second child. The child is still subject to a hard
283
+ // timeout so a permanently stuck handshake is eventually replaced.
284
+ if (child?.pid && platform.isProcessRunning(child.pid)) {
285
+ if (Date.now() - childStartedAt < 45_000)
286
+ return;
287
+ }
268
288
  const healthy = await findHealthyInstance();
269
289
  if (healthy) {
270
290
  const changed = !active || active.pid !== healthy.record.pid || active.port !== healthy.record.port;
@@ -334,6 +354,7 @@ export function createEcwebSupervisor(options) {
334
354
  }
335
355
  catch { }
336
356
  child = null;
357
+ childStartedAt = 0;
337
358
  }
338
359
  closeLogStream();
339
360
  },
@@ -95,8 +95,7 @@ export var ErrorType;
95
95
  })(ErrorType || (ErrorType = {}));
96
96
  export function getErrorRawText(error, seen = new Set()) {
97
97
  if (typeof error === 'string') {
98
- const text = error.trim();
99
- return text || undefined;
98
+ return error.trim() ? error : undefined;
100
99
  }
101
100
  if (error === null || error === undefined)
102
101
  return undefined;
@@ -108,7 +107,7 @@ export function getErrorRawText(error, seen = new Set()) {
108
107
  if ('raw' in record) {
109
108
  const raw = record.raw;
110
109
  if (typeof raw === 'string' && raw.trim())
111
- return raw.trim();
110
+ return raw;
112
111
  }
113
112
  const causeText = getErrorRawText(record.cause, seen);
114
113
  if (causeText)
@@ -116,7 +115,7 @@ export function getErrorRawText(error, seen = new Set()) {
116
115
  for (const key of ['message', 'error', 'reason']) {
117
116
  const value = record[key];
118
117
  if (typeof value === 'string' && value.trim())
119
- return value.trim();
118
+ return value;
120
119
  if (value && typeof value === 'object') {
121
120
  const nestedText = getErrorRawText(value, seen);
122
121
  if (nestedText)
@@ -527,6 +526,11 @@ export function isRetryableError(error) {
527
526
  const lower = msg.toLowerCase();
528
527
  if (lower.includes('invalid jsonl session file') || lower.includes('invalid session file'))
529
528
  return false;
529
+ // The idle watchdog emits this exact terminal error when a runner stops
530
+ // producing events. The provider session remains resumable, so the task
531
+ // recovery trigger must be allowed to continue the original session.
532
+ if (lower === 'sdk_timeout')
533
+ return true;
530
534
  // 字典优先 — 命中则直接返回
531
535
  const rule = matchErrorRule(lower);
532
536
  if (rule)
@@ -176,6 +176,7 @@ export function reportEvolcoreStartup(entryUrl, report) {
176
176
  const diskDistDir = path.join(packageRoot, 'dist');
177
177
  const runtimeIsDist = path.resolve(path.dirname(runtimeEntry)) === path.resolve(diskDistDir);
178
178
  const runtimeBuildIdentity = readBuildIdentity(runtimeIsDist ? path.dirname(runtimeEntry) : diskDistDir, readInstalledEvolcoreVersion());
179
+ const diskDistId = runtimeBuildIdentity.id;
179
180
  if (!runtimeIsDist)
180
181
  runtimeBuildIdentity.id = null;
181
182
  const isLinked = fs.existsSync(path.join(packageRoot, '.git'));
@@ -216,9 +217,9 @@ export function reportEvolcoreStartup(entryUrl, report) {
216
217
  console.error(`[EvolCore] 包路径: ${packageRoot}`);
217
218
  console.error(`[EvolCore] 代码时间: ${latestMtime ? formatTime(latestMtime) : '?'}`);
218
219
  console.error('[EvolCore] ✓ Version info printed');
219
- const diskBuildId = computeBuildId(diskDistDir);
220
+ const diskBuildId = diskDistId;
220
221
  console.error(`[EvolCore] 运行 build ID: ${runtimeIsDist ? (runtimeBuildIdentity.id ?? '?') : 'source'}`);
221
222
  console.error(`[EvolCore] 磁盘 dist build ID: ${diskBuildId ?? '?'}`);
222
223
  report(`[Build] runtimeMode=${runtimeIsDist ? 'dist' : 'source'} runtimeBuildId=${runtimeBuildIdentity.id ?? 'source'} diskDistBuildId=${diskBuildId ?? 'unknown'} match=${runtimeIsDist && runtimeBuildIdentity.id !== null && runtimeBuildIdentity.id === diskBuildId}`);
223
- return { runtimeIsDist, runtimeBuildIdentity, diskDistDir };
224
+ return { runtimeIsDist, runtimeBuildIdentity, diskDistDir, diskDistId };
224
225
  }
@@ -0,0 +1,52 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import { readSystemMemoryUsage } from './system-memory.js';
4
+ function readOsRelease() {
5
+ if (process.platform !== 'linux')
6
+ return { id: null, version: null };
7
+ try {
8
+ const values = new Map();
9
+ for (const line of fs.readFileSync('/etc/os-release', 'utf8').split(/\r?\n/u)) {
10
+ const match = line.match(/^([A-Z0-9_]+)=(.*)$/u);
11
+ if (!match)
12
+ continue;
13
+ values.set(match[1], match[2].replace(/^"|"$/gu, ''));
14
+ }
15
+ return { id: values.get('ID') || null, version: values.get('VERSION_ID') || null };
16
+ }
17
+ catch {
18
+ return { id: null, version: null };
19
+ }
20
+ }
21
+ function networkType() {
22
+ try {
23
+ const names = Object.entries(os.networkInterfaces())
24
+ .filter(([, addresses]) => (addresses ?? []).some(address => !address.internal))
25
+ .map(([name]) => name.toLowerCase());
26
+ if (names.some(name => /(?:wwan|rmnet|pdp|cell|usb.*modem)/u.test(name)))
27
+ return 'cellular';
28
+ if (names.some(name => /(?:^wl|wlan|wifi|wireless)/u.test(name)))
29
+ return 'wlan';
30
+ if (names.some(name => /(?:^eth|^en[opsx]|^eno|^ens|^lan)/u.test(name)))
31
+ return 'lan';
32
+ }
33
+ catch {
34
+ // Network classification is best effort and must never fail status.
35
+ }
36
+ return 'unknown';
37
+ }
38
+ export function readHostInfo(evolcoreVersion) {
39
+ const release = readOsRelease();
40
+ return {
41
+ hostname: os.hostname(),
42
+ node: { version: process.version },
43
+ evolcore: { version: evolcoreVersion ?? null },
44
+ os: { platform: process.platform, distributionId: release.id, version: release.version },
45
+ hardware: {
46
+ architecture: process.arch,
47
+ cpuCount: os.cpus().length,
48
+ memoryTotalBytes: readSystemMemoryUsage().memTotal,
49
+ },
50
+ network: { type: networkType() },
51
+ };
52
+ }
@@ -415,7 +415,10 @@ export class AgentHarness {
415
415
  // Persist the same multimodal message that was sent to the provider.
416
416
  // Provider rollouts (Claude/Codex) retain image bytes in their own
417
417
  // session files, so ecagent must do the same for resumable context.
418
- await this.session.appendMessage(event.message);
418
+ const shouldPersist = event.message.role !== "assistant" ||
419
+ (event.message.stopReason !== "error" && event.message.stopReason !== "aborted");
420
+ if (shouldPersist)
421
+ await this.session.appendMessage(event.message);
419
422
  await this.emitAny(event, signal);
420
423
  return;
421
424
  }
@@ -22,7 +22,7 @@
22
22
  |------|------|--------|
23
23
  | `ec --version` / `-v` / `-V` | 查看版本号 | 版本 |
24
24
  | `ec restart` | 重启目标 daemon(承载当前托管会话的实例受生命周期保护) | 重启 |
25
- | `ec status` | 查看运行状态、进程、会话、渠道连接 | 状态/查看状态 |
25
+ | `ec status [--format json]` | 查看运行状态、Agent、队列、渠道连接;JSON 快照 | 状态/查看状态 |
26
26
  | `ec logs` | 查看日志(`--level` `--module` `--raw` `--no-color`) | 日志/查看日志 |
27
27
  | `ec diagnose` | 诊断启动环境 | 诊断/环境诊断 |
28
28