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.
- package/CHANGELOG.md +49 -0
- package/README.md +31 -20
- package/bin/ec-safe-output.js +14 -2
- package/dist/agents/baseagent-availability.js +1 -0
- package/dist/agents/claude-runner.js +32 -46
- package/dist/agents/codex-runner.js +51 -27
- package/dist/agents/ecagent-runner.js +18 -7
- package/dist/agents/gemini-runner.js +1 -2
- package/dist/agents/runner-types.js +28 -6
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/control-aid.js +23 -98
- package/dist/aun/aid/ownership-operation.js +1 -3
- package/dist/aun/group/invite-service.js +6 -0
- package/dist/aun/msg/p2p.js +8 -12
- package/dist/channels/aun.js +66 -48
- package/dist/channels/dingtalk.js +8 -2
- package/dist/channels/feishu.js +6 -1
- package/dist/channels/wecom.js +7 -3
- package/dist/cli/agent-create.js +23 -4
- package/dist/cli/aid-bind-command.js +4 -1
- package/dist/cli/daemon-commands.js +242 -17
- package/dist/cli/index.js +25 -13
- package/dist/cli/memory-command.js +33 -14
- package/dist/cli/restart-monitor.js +35 -13
- package/dist/cli/task-context.js +1 -0
- package/dist/config/config-field-policy.js +1 -0
- package/dist/contracts/ecweb-service-protocol.js +44 -0
- package/dist/core/agent-runtime.js +6 -0
- package/dist/core/auth/agent-delegation.js +7 -4
- package/dist/core/auth/auth-gateway.js +3 -1
- package/dist/core/auth/authorization-audit.js +5 -1
- package/dist/core/auth/debug-context.js +5 -1
- package/dist/core/auth/operation-catalog.js +16 -0
- package/dist/core/command/command-handler.js +7 -1
- package/dist/core/command/menu-catalog.js +48 -12
- package/dist/core/command/menu-handler.js +72 -4
- package/dist/core/command/menu-protocol.js +20 -1
- package/dist/core/command/slash-gate.js +17 -3
- package/dist/core/command/slash-handler.js +90 -7
- package/dist/core/event-catalog.js +2 -0
- package/dist/core/interaction-router.js +2 -1
- package/dist/core/ipc-registration.js +26 -7
- package/dist/core/memory/batch-meta.js +2 -0
- package/dist/core/memory/consolidation-context.js +42 -51
- package/dist/core/memory/consolidation-ops-records.js +39 -7
- package/dist/core/memory/consolidation-trigger.js +71 -64
- package/dist/core/memory/consolidation.js +226 -20
- package/dist/core/memory/memory-writer.js +160 -34
- package/dist/core/memory/relation-idle-monitor.js +287 -0
- package/dist/core/memory/relation-sweep.js +206 -0
- package/dist/core/message/aun-ipc-sender.js +5 -0
- package/dist/core/message/message-bridge.js +64 -55
- package/dist/core/message/message-log.js +17 -0
- package/dist/core/message/message-queue.js +6 -0
- package/dist/core/message/outbound.js +70 -0
- package/dist/core/message/peer-mode.js +7 -2
- package/dist/core/message/response-engine.js +341 -73
- package/dist/core/ownership/owner-resolver.js +384 -178
- package/dist/core/permission/ec-command-parser.js +19 -3
- package/dist/core/permission/tool-policy.js +7 -7
- package/dist/core/role/runtime-policy.js +64 -0
- package/dist/core/session/session-manager.js +25 -5
- package/dist/core/system-session/memory.js +3 -18
- package/dist/ecweb/basic-settings-service.js +709 -0
- package/dist/ecweb/providers-service.js +172 -0
- package/dist/index.js +92 -4
- package/dist/ipc.js +78 -11
- package/dist/stats/db.js +33 -0
- package/dist/stats/index.js +2 -2
- package/dist/stats/query.js +246 -0
- package/dist/stats/writer.js +41 -1
- package/dist/trigger/application-service.js +12 -0
- package/dist/trigger/authorization.js +2 -2
- package/dist/trigger/history.js +36 -0
- package/dist/trigger/scheduler.js +3 -0
- package/dist/utils/ecweb-supervisor.js +31 -10
- package/dist/utils/error-utils.js +8 -4
- package/dist/utils/evolcore-version.js +3 -2
- package/dist/utils/host-info.js +52 -0
- package/ecagent/dist/harness/agent-harness.js +4 -1
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/agent.md +1 -1
- package/kits/docs/evolcore/aid.md +29 -12
- package/kits/docs/evolcore/config.md +9 -7
- package/kits/docs/evolcore/memory-architecture.md +3 -4
- package/kits/docs/evolcore/memory-consolidation.md +28 -7
- package/kits/docs/evolcore/memory-tags.md +1 -1
- package/kits/docs/evolcore/memory.md +1 -1
- package/kits/docs/evolcore/menu-client.md +1 -1
- package/kits/docs/evolcore/menu-protocol.md +6 -0
- package/kits/docs/evolcore/msg.md +1 -1
- package/kits/eck_manifest.json +3 -3
- package/kits/rules/04-relation.md +3 -2
- package/kits/schemas/agent-config.schema.13.json +5 -1
- package/kits/templates/memory-consolidation/generalize.md +32 -4
- package/kits/templates/memory-consolidation/notes.md +2 -2
- package/kits/templates/memory-consolidation/promote.md +10 -2
- package/kits/templates/system-fragments/baseagent.md +5 -0
- package/kits/templates/system-fragments/memory.md +12 -3
- package/kits/templates/system-fragments/session.md +1 -1
- package/kits/{rules/07-memory-and-thread.md → templates/system-fragments/thread.md} +17 -80
- package/package.json +3 -3
package/dist/stats/query.js
CHANGED
|
@@ -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',
|
package/dist/stats/writer.js
CHANGED
|
@@ -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
|
|
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,
|
package/dist/trigger/history.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|