evolcore 0.0.19 → 0.0.21
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 +43 -1
- package/README.md +60 -9
- package/bin/install-codex-managed-hooks.mjs +61 -0
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +383 -111
- package/dist/agents/codex-app-server-client.js +10 -2
- package/dist/agents/codex-runner.js +405 -137
- package/dist/agents/ecagent-runner.js +174 -63
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +66 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +86 -9
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +26 -11
- package/dist/aun/outbox.js +295 -68
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +1004 -273
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +81 -121
- package/dist/cli/index.js +1 -0
- package/dist/cli/init.js +76 -24
- package/dist/cli/restart-monitor.js +3 -3
- package/dist/cli/task-context.js +46 -0
- package/dist/cli/trigger-command.js +1 -1
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +16 -5
- package/dist/config/config-manager.js +226 -24
- package/dist/config/config-operation-service.js +1 -2
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +29 -16
- package/dist/config/lifecycle.js +16 -5
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +161 -12
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +31 -1
- package/dist/core/auth/auth-gateway.js +33 -4
- package/dist/core/auth/authorization-audit.js +155 -4
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-messages.js +2 -2
- package/dist/core/bootstrap-service.js +27 -38
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/channel-loader.js +0 -2
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +290 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +279 -80
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +350 -32
- package/dist/core/event-catalog.js +5 -0
- package/dist/core/evolagent.js +9 -4
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +10 -0
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +83 -84
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +124 -10
- package/dist/core/message/message-log.js +14 -7
- package/dist/core/message/message-queue.js +206 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +495 -72
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +465 -56
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +205 -13
- package/dist/core/permission/tool-policy.js +673 -62
- package/dist/core/relation/peer-identity.js +18 -0
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +299 -30
- package/dist/core/session/session-renew.js +19 -12
- package/dist/core/session/session-turn-coordinator.js +11 -4
- package/dist/eck/kit-renderer.js +18 -9
- package/dist/index.js +283 -65
- package/dist/ipc.js +374 -24
- package/dist/paths.js +64 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +56 -5
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +3 -2
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +21 -2
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/dist/utils/stats.js +52 -18
- package/dist/utils/welcome.js +2 -2
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/rules/01-overview.md +1 -1
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/05-venue.md +1 -1
- package/kits/schemas/_meta.json +10 -5
- package/kits/schemas/agent-config.schema.10.json +2 -1
- package/kits/schemas/agent-config.schema.11.json +421 -0
- package/kits/schemas/daemon.schema.5.json +135 -0
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +119 -0
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/package.json +7 -3
- package/skills/eclink/SKILL.md +2 -0
package/dist/aun/outbox.js
CHANGED
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
|
-
import { agentOutboxPath } from '../paths.js';
|
|
2
|
+
import { agentActivityOutboxPath, agentOutboxPath } from '../paths.js';
|
|
3
3
|
import { atomicRead, atomicWrite } from '../utils/atomic-write.js';
|
|
4
4
|
import { isDeliveryTarget, isDeliveryTargetForChannel, sameDeliveryTarget } from '../core/message/message-utils.js';
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
export const DEFAULT_OUTBOX_MAX_ENTRIES = 200;
|
|
6
|
+
export const DEFAULT_OUTBOX_TTL_MS = 60 * 60 * 1000;
|
|
7
|
+
export const ACTIVITY_OUTBOX_MAX_ENTRIES = 500;
|
|
8
|
+
export const ACTIVITY_OUTBOX_TTL_MS = 30 * 60 * 1000;
|
|
9
|
+
const MAX_TERMINAL_ENTRIES_PER_AID = 100;
|
|
10
|
+
const QUEUE_CONFIG = {
|
|
11
|
+
default: {
|
|
12
|
+
file: agentOutboxPath,
|
|
13
|
+
maxEntries: DEFAULT_OUTBOX_MAX_ENTRIES,
|
|
14
|
+
defaultTtl: DEFAULT_OUTBOX_TTL_MS,
|
|
15
|
+
},
|
|
16
|
+
activity: {
|
|
17
|
+
file: agentActivityOutboxPath,
|
|
18
|
+
maxEntries: ACTIVITY_OUTBOX_MAX_ENTRIES,
|
|
19
|
+
defaultTtl: ACTIVITY_OUTBOX_TTL_MS,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
function queueConfig(queue = 'default') {
|
|
23
|
+
return QUEUE_CONFIG[queue];
|
|
24
|
+
}
|
|
25
|
+
export function defaultTtl(queue = 'default') {
|
|
26
|
+
return queueConfig(queue).defaultTtl;
|
|
27
|
+
}
|
|
28
|
+
function outboxFile(aid, queue = 'default') {
|
|
29
|
+
return queueConfig(queue).file(aid);
|
|
30
|
+
}
|
|
31
|
+
function entryQueue(entry) {
|
|
32
|
+
return entry.queue ?? 'default';
|
|
9
33
|
}
|
|
10
34
|
function generateId() {
|
|
11
35
|
const ts = Date.now();
|
|
@@ -13,6 +37,11 @@ function generateId() {
|
|
|
13
37
|
return `out-${ts}-${rand}`;
|
|
14
38
|
}
|
|
15
39
|
function isExpired(entry) {
|
|
40
|
+
// Terminal failures are retained as durable diagnostics. They are excluded
|
|
41
|
+
// from `load()`/`hasPending()` and must not disappear merely because the
|
|
42
|
+
// original delivery TTL elapsed.
|
|
43
|
+
if (entry.terminal || entry.deliveryResult)
|
|
44
|
+
return false;
|
|
16
45
|
if (entry.critical)
|
|
17
46
|
return false;
|
|
18
47
|
return Date.now() - entry.ts > entry.ttl;
|
|
@@ -20,8 +49,8 @@ function isExpired(entry) {
|
|
|
20
49
|
export function isDeliveryForChannel(value, channelId) {
|
|
21
50
|
return isDeliveryTargetForChannel(value, channelId);
|
|
22
51
|
}
|
|
23
|
-
function readEntries(aid) {
|
|
24
|
-
const file = outboxFile(aid);
|
|
52
|
+
function readEntries(aid, queue = 'default') {
|
|
53
|
+
const file = outboxFile(aid, queue);
|
|
25
54
|
try {
|
|
26
55
|
const content = atomicRead(file)?.trim();
|
|
27
56
|
if (!content)
|
|
@@ -39,22 +68,45 @@ function readEntries(aid) {
|
|
|
39
68
|
return [];
|
|
40
69
|
}
|
|
41
70
|
}
|
|
42
|
-
function writeEntries(aid, entries) {
|
|
43
|
-
const file = outboxFile(aid);
|
|
44
|
-
const
|
|
45
|
-
|
|
71
|
+
function writeEntries(aid, entries, queue = 'default') {
|
|
72
|
+
const file = outboxFile(aid, queue);
|
|
73
|
+
const boundedEntries = trimFinalEntries(entries);
|
|
74
|
+
const content = boundedEntries.length > 0
|
|
75
|
+
? boundedEntries.map(e => JSON.stringify(e)).join('\n') + '\n'
|
|
46
76
|
: '';
|
|
47
77
|
atomicWrite(file, content);
|
|
48
78
|
}
|
|
79
|
+
function trimFinalEntries(entries) {
|
|
80
|
+
const finalEntries = entries.filter(entry => !!entry.terminal || !!entry.deliveryResult);
|
|
81
|
+
if (finalEntries.length <= MAX_TERMINAL_ENTRIES_PER_AID)
|
|
82
|
+
return entries;
|
|
83
|
+
const retainedFinalIds = new Set(finalEntries
|
|
84
|
+
.sort((left, right) => (left.terminal?.at ?? left.deliveryResult?.at ?? left.ts) - (right.terminal?.at ?? right.deliveryResult?.at ?? right.ts))
|
|
85
|
+
.slice(-MAX_TERMINAL_ENTRIES_PER_AID)
|
|
86
|
+
.map(entry => entry.id));
|
|
87
|
+
return entries.filter(entry => (!entry.terminal && !entry.deliveryResult) || retainedFinalIds.has(entry.id));
|
|
88
|
+
}
|
|
49
89
|
export function enqueue(aid, opts) {
|
|
90
|
+
const queue = opts.queue ?? 'default';
|
|
91
|
+
const config = queueConfig(queue);
|
|
50
92
|
const delivery = opts.delivery;
|
|
51
93
|
if (!isDeliveryForChannel(delivery, opts.channelId)) {
|
|
52
94
|
const code = isDeliveryTarget(delivery) ? 'AUN_OUTBOUND_ROUTE_MISMATCH' : 'AUN_OUTBOUND_ROUTE_REQUIRED';
|
|
53
95
|
throw Object.assign(new Error(`invalid AUN outbox delivery route for channelId=${opts.channelId}`), { code });
|
|
54
96
|
}
|
|
55
|
-
let existingEntries = readEntries(aid);
|
|
97
|
+
let existingEntries = readEntries(aid, queue);
|
|
98
|
+
for (const existing of existingEntries) {
|
|
99
|
+
if (isExpired(existing))
|
|
100
|
+
markExpired(existing);
|
|
101
|
+
}
|
|
56
102
|
if (opts.dedupeKey) {
|
|
57
|
-
|
|
103
|
+
// Terminal results are decisions for this logical operation. Reusing the
|
|
104
|
+
// same route must not silently turn a permanent rejection into a retry;
|
|
105
|
+
// an explicit route correction is the only case that replaces it.
|
|
106
|
+
const existing = existingEntries.find(entry => entry.dedupeKey === opts.dedupeKey
|
|
107
|
+
&& !entry.terminal
|
|
108
|
+
&& !entry.deliveryResult
|
|
109
|
+
&& !isExpired(entry));
|
|
58
110
|
if (existing
|
|
59
111
|
&& existing.channelId === opts.channelId
|
|
60
112
|
&& isDeliveryForChannel(existing.delivery, existing.channelId)
|
|
@@ -71,6 +123,7 @@ export function enqueue(aid, opts) {
|
|
|
71
123
|
id: generateId(),
|
|
72
124
|
ts: Date.now(),
|
|
73
125
|
aid,
|
|
126
|
+
...(queue === 'activity' ? { queue } : {}),
|
|
74
127
|
channelId: opts.channelId,
|
|
75
128
|
delivery,
|
|
76
129
|
dedupeKey: opts.dedupeKey,
|
|
@@ -83,60 +136,160 @@ export function enqueue(aid, opts) {
|
|
|
83
136
|
image: opts.image,
|
|
84
137
|
logText: opts.logText,
|
|
85
138
|
context: opts.context,
|
|
86
|
-
ttl: opts.ttl ??
|
|
139
|
+
ttl: opts.ttl ?? config.defaultTtl,
|
|
87
140
|
postSend: opts.postSend,
|
|
88
141
|
};
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const dropIndex = entries.findIndex(candidate => !candidate.critical);
|
|
93
|
-
if (dropIndex >= 0)
|
|
94
|
-
entries.splice(dropIndex, 1);
|
|
95
|
-
entries = [...entries, entry];
|
|
142
|
+
const pendingCount = existingEntries.filter(candidate => !candidate.critical && !candidate.terminal && !candidate.deliveryResult && !isExpired(candidate)).length;
|
|
143
|
+
if (!opts.critical && pendingCount >= config.maxEntries) {
|
|
144
|
+
throw Object.assign(new Error(`${queue} outbox is full for ${aid} (max ${config.maxEntries})`), { code: 'OUTBOX_FULL', queue, aid, maxEntries: config.maxEntries });
|
|
96
145
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
writeEntries(aid, entries);
|
|
146
|
+
const entries = [...existingEntries, entry];
|
|
147
|
+
writeEntries(aid, entries, queue);
|
|
101
148
|
return entry;
|
|
102
149
|
}
|
|
103
|
-
export function remove(aid, id) {
|
|
104
|
-
const entries = readEntries(aid).filter(e => e.id !== id);
|
|
105
|
-
writeEntries(aid, entries);
|
|
150
|
+
export function remove(aid, id, queue = 'default') {
|
|
151
|
+
const entries = readEntries(aid, queue).filter(e => e.id !== id);
|
|
152
|
+
writeEntries(aid, entries, queue);
|
|
153
|
+
}
|
|
154
|
+
/** Remove an entry only if it still points at the route that was submitted. */
|
|
155
|
+
export function removeIfRouteMatches(aid, submitted, queue = entryQueue(submitted)) {
|
|
156
|
+
const entries = readEntries(aid, queue);
|
|
157
|
+
const index = entries.findIndex(entry => entry.id === submitted.id);
|
|
158
|
+
if (index < 0 || !hasSamePersistedRoute(entries[index], submitted))
|
|
159
|
+
return false;
|
|
160
|
+
entries.splice(index, 1);
|
|
161
|
+
writeEntries(aid, entries, queue);
|
|
162
|
+
return true;
|
|
106
163
|
}
|
|
107
164
|
/** Replace an existing entry without consuming another outbox slot. */
|
|
108
165
|
export function replace(aid, entry) {
|
|
109
|
-
const
|
|
166
|
+
const queue = entryQueue(entry);
|
|
167
|
+
const entries = readEntries(aid, queue);
|
|
110
168
|
const index = entries.findIndex(candidate => candidate.id === entry.id);
|
|
111
169
|
if (index < 0)
|
|
112
170
|
return false;
|
|
113
171
|
entries[index] = entry;
|
|
114
|
-
writeEntries(aid, entries);
|
|
172
|
+
writeEntries(aid, entries, queue);
|
|
115
173
|
return true;
|
|
116
174
|
}
|
|
175
|
+
/** Replace an entry only while its submitted route is still current. */
|
|
176
|
+
export function replaceIfRouteMatches(aid, submitted, replacement, queue = entryQueue(submitted)) {
|
|
177
|
+
const entries = readEntries(aid, queue);
|
|
178
|
+
const index = entries.findIndex(candidate => candidate.id === submitted.id);
|
|
179
|
+
if (index < 0)
|
|
180
|
+
return 'missing';
|
|
181
|
+
if (!hasSamePersistedRoute(entries[index], submitted))
|
|
182
|
+
return 'route-changed';
|
|
183
|
+
entries[index] = replacement;
|
|
184
|
+
writeEntries(aid, entries, queue);
|
|
185
|
+
return 'replaced';
|
|
186
|
+
}
|
|
187
|
+
/** Update only the delivery receipt while preserving concurrent entry changes. */
|
|
188
|
+
export function updateDeliveryReceiptIfRouteMatches(aid, submitted, receipt, queue = entryQueue(submitted)) {
|
|
189
|
+
const entries = readEntries(aid, queue);
|
|
190
|
+
const index = entries.findIndex(candidate => candidate.id === submitted.id);
|
|
191
|
+
if (index < 0)
|
|
192
|
+
return 'missing';
|
|
193
|
+
if (!hasSamePersistedRoute(entries[index], submitted))
|
|
194
|
+
return 'route-changed';
|
|
195
|
+
entries[index].deliveryReceipt = receipt;
|
|
196
|
+
writeEntries(aid, entries, queue);
|
|
197
|
+
return 'replaced';
|
|
198
|
+
}
|
|
117
199
|
/** Remove durable interaction cards that were cancelled before delivery. */
|
|
118
|
-
export function removeInteractionCards(aid, requestId) {
|
|
119
|
-
const entries = readEntries(aid);
|
|
200
|
+
export function removeInteractionCards(aid, requestId, queue = 'default') {
|
|
201
|
+
const entries = readEntries(aid, queue);
|
|
120
202
|
const retained = entries.filter(entry => !(entry.postSend?.type === 'register_interaction_card'
|
|
121
203
|
&& entry.postSend.requestId === requestId));
|
|
122
204
|
const removed = entries.length - retained.length;
|
|
123
205
|
if (removed > 0)
|
|
124
|
-
writeEntries(aid, retained);
|
|
206
|
+
writeEntries(aid, retained, queue);
|
|
125
207
|
return removed;
|
|
126
208
|
}
|
|
127
|
-
export function load(aid) {
|
|
128
|
-
return readEntries(aid).filter(e => !isExpired(e));
|
|
209
|
+
export function load(aid, queue = 'default') {
|
|
210
|
+
return readEntries(aid, queue).filter(e => !e.terminal && !e.deliveryResult && !isExpired(e));
|
|
129
211
|
}
|
|
130
|
-
export function findByDedupeKey(aid, dedupeKey) {
|
|
131
|
-
|
|
212
|
+
export function findByDedupeKey(aid, dedupeKey, opts = {}, queue = 'default') {
|
|
213
|
+
const entries = opts.includeTerminal
|
|
214
|
+
? readEntries(aid, queue).filter(entry => !isExpired(entry))
|
|
215
|
+
: load(aid, queue);
|
|
216
|
+
return entries.find(entry => entry.dedupeKey === dedupeKey);
|
|
132
217
|
}
|
|
133
|
-
export function cleanup(aid) {
|
|
134
|
-
const all = readEntries(aid);
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
218
|
+
export function cleanup(aid, queue = 'default') {
|
|
219
|
+
const all = readEntries(aid, queue);
|
|
220
|
+
let expired = 0;
|
|
221
|
+
for (const entry of all) {
|
|
222
|
+
if (!isExpired(entry))
|
|
223
|
+
continue;
|
|
224
|
+
expired++;
|
|
225
|
+
markExpired(entry);
|
|
226
|
+
}
|
|
227
|
+
if (expired > 0)
|
|
228
|
+
writeEntries(aid, all, queue);
|
|
229
|
+
return expired;
|
|
230
|
+
}
|
|
231
|
+
/** Resolve a durable delivery without treating disappearance as success. */
|
|
232
|
+
export function deliveryState(aid, id, queue = 'default') {
|
|
233
|
+
const entries = readEntries(aid, queue);
|
|
234
|
+
const entry = entries.find(candidate => candidate.id === id);
|
|
235
|
+
if (!entry)
|
|
236
|
+
return { status: 'missing' };
|
|
237
|
+
const deliveredMessageId = entry.deliveryResult?.messageId ?? entry.deliveryReceipt?.messageId;
|
|
238
|
+
if (deliveredMessageId) {
|
|
239
|
+
return { status: 'sent', messageId: deliveredMessageId };
|
|
240
|
+
}
|
|
241
|
+
if (entry.terminal) {
|
|
242
|
+
return {
|
|
243
|
+
status: 'failed',
|
|
244
|
+
error: entry.terminal.error,
|
|
245
|
+
...(entry.terminal.code !== undefined ? { code: entry.terminal.code } : {}),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (isExpired(entry)) {
|
|
249
|
+
markExpired(entry);
|
|
250
|
+
writeEntries(aid, entries, queue);
|
|
251
|
+
return {
|
|
252
|
+
status: 'failed',
|
|
253
|
+
error: entry.terminal.error,
|
|
254
|
+
code: entry.terminal.code,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return { status: 'queued' };
|
|
258
|
+
}
|
|
259
|
+
function markExpired(entry) {
|
|
260
|
+
entry.lastError = 'outbox delivery expired';
|
|
261
|
+
entry.lastErrorCode = 'OUTBOX_EXPIRED';
|
|
262
|
+
entry.terminal = {
|
|
263
|
+
at: Date.now(),
|
|
264
|
+
error: entry.lastError,
|
|
265
|
+
code: entry.lastErrorCode,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/** Mark a durable entry terminal while keeping its reason for diagnostics. */
|
|
269
|
+
export function markTerminal(aid, id, failure, submitted, queue = submitted ? entryQueue(submitted) : 'default') {
|
|
270
|
+
const entries = readEntries(aid, queue);
|
|
271
|
+
const entry = entries.find(candidate => candidate.id === id);
|
|
272
|
+
if (!entry || (submitted && !hasSamePersistedRoute(entry, submitted)))
|
|
273
|
+
return false;
|
|
274
|
+
entry.lastError = failure.error ?? 'permanent send failure';
|
|
275
|
+
if (failure.code !== undefined)
|
|
276
|
+
entry.lastErrorCode = failure.code;
|
|
277
|
+
entry.terminal = { at: Date.now(), error: entry.lastError, ...(failure.code !== undefined ? { code: failure.code } : {}) };
|
|
278
|
+
writeEntries(aid, entries, queue);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
/** Retain a bounded success receipt for queued Trigger history reconciliation. */
|
|
282
|
+
export function markDelivered(aid, id, messageId, submitted, queue = submitted ? entryQueue(submitted) : 'default') {
|
|
283
|
+
const entries = readEntries(aid, queue);
|
|
284
|
+
const entry = entries.find(candidate => candidate.id === id);
|
|
285
|
+
if (!entry || (submitted && !hasSamePersistedRoute(entry, submitted)))
|
|
286
|
+
return false;
|
|
287
|
+
entry.deliveryResult = { status: 'sent', at: Date.now(), messageId };
|
|
288
|
+
delete entry.lastError;
|
|
289
|
+
delete entry.lastErrorCode;
|
|
290
|
+
delete entry.terminal;
|
|
291
|
+
writeEntries(aid, entries, queue);
|
|
292
|
+
return true;
|
|
140
293
|
}
|
|
141
294
|
// Each caller gets its own pass over the current disk state. This prevents
|
|
142
295
|
// duplicate sends while allowing a later drain trigger to pick up entries
|
|
@@ -145,36 +298,95 @@ const activeDrainTails = new Map();
|
|
|
145
298
|
function hasRouteForChannel(entry) {
|
|
146
299
|
return isDeliveryForChannel(entry.delivery, entry.channelId);
|
|
147
300
|
}
|
|
148
|
-
|
|
149
|
-
|
|
301
|
+
function hasSamePersistedRoute(left, right) {
|
|
302
|
+
if (left.channelId !== right.channelId)
|
|
303
|
+
return false;
|
|
304
|
+
if (left.delivery === undefined && right.delivery === undefined)
|
|
305
|
+
return true;
|
|
306
|
+
return isDeliveryTarget(left.delivery)
|
|
307
|
+
&& isDeliveryTarget(right.delivery)
|
|
308
|
+
&& sameDeliveryTarget(left.delivery, right.delivery);
|
|
309
|
+
}
|
|
310
|
+
async function drainOnce(aid, sender, queue) {
|
|
311
|
+
const entries = readEntries(aid, queue).filter(entry => !entry.terminal && !entry.deliveryResult);
|
|
150
312
|
if (entries.length === 0)
|
|
151
313
|
return { sent: 0, expired: 0, failed: 0 };
|
|
152
314
|
const drainedIds = new Set(entries.map(e => e.id));
|
|
315
|
+
const drainedById = new Map(entries.map(entry => [entry.id, entry]));
|
|
153
316
|
let sent = 0;
|
|
154
317
|
let expired = 0;
|
|
155
318
|
let failed = 0;
|
|
319
|
+
let permanent = 0;
|
|
156
320
|
const remaining = [];
|
|
157
321
|
for (const entry of entries) {
|
|
158
322
|
if (isExpired(entry)) {
|
|
159
323
|
expired++;
|
|
324
|
+
markExpired(entry);
|
|
325
|
+
// Keep the terminal record so Trigger history can distinguish an
|
|
326
|
+
// operation that expired from one that was actually sent.
|
|
327
|
+
remaining.push(entry);
|
|
160
328
|
continue;
|
|
161
329
|
}
|
|
162
330
|
if (!hasRouteForChannel(entry)) {
|
|
163
331
|
// A route is security-sensitive metadata. An entry whose groupId does
|
|
164
|
-
// not match its address cannot be repaired from channelId
|
|
165
|
-
// instead of retrying the same malformed
|
|
166
|
-
|
|
332
|
+
// not match its address cannot be repaired from channelId. Keep a
|
|
333
|
+
// terminal diagnostic record instead of retrying the same malformed
|
|
334
|
+
// send forever or silently deleting evidence of the failure.
|
|
335
|
+
permanent++;
|
|
336
|
+
entry.lastError = 'outbox entry has no valid delivery route';
|
|
337
|
+
entry.lastErrorCode = 'AUN_OUTBOUND_ROUTE_REQUIRED';
|
|
338
|
+
entry.terminal = {
|
|
339
|
+
at: Date.now(),
|
|
340
|
+
error: entry.lastError,
|
|
341
|
+
code: entry.lastErrorCode,
|
|
342
|
+
};
|
|
343
|
+
remaining.push(entry);
|
|
167
344
|
continue;
|
|
168
345
|
}
|
|
169
346
|
try {
|
|
170
347
|
entry.attempts = (entry.attempts ?? 0) + 1;
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
sent
|
|
348
|
+
const raw = await sender(entry);
|
|
349
|
+
const result = typeof raw === 'boolean'
|
|
350
|
+
? { status: raw ? 'sent' : 'retry' }
|
|
351
|
+
: raw;
|
|
352
|
+
if (result.status === 'sent') {
|
|
353
|
+
const messageId = result.messageId ?? entry.deliveryReceipt?.messageId;
|
|
354
|
+
if (messageId) {
|
|
355
|
+
sent++;
|
|
356
|
+
entry.deliveryResult = { status: 'sent', at: Date.now(), messageId };
|
|
357
|
+
remaining.push(entry);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
permanent++;
|
|
361
|
+
entry.lastError = 'outbox sender returned sent without a remote message_id';
|
|
362
|
+
entry.lastErrorCode = 'MISSING_MESSAGE_ID';
|
|
363
|
+
entry.terminal = {
|
|
364
|
+
at: Date.now(),
|
|
365
|
+
error: entry.lastError,
|
|
366
|
+
code: entry.lastErrorCode,
|
|
367
|
+
};
|
|
368
|
+
remaining.push(entry);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
else if (result.status === 'permanent') {
|
|
372
|
+
permanent++;
|
|
373
|
+
entry.lastError = result.error ?? 'permanent send failure';
|
|
374
|
+
if (result.code !== undefined)
|
|
375
|
+
entry.lastErrorCode = result.code;
|
|
376
|
+
entry.terminal = {
|
|
377
|
+
at: Date.now(),
|
|
378
|
+
error: entry.lastError,
|
|
379
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
380
|
+
};
|
|
381
|
+
// Keep a terminal record for diagnostics, but exclude it from all
|
|
382
|
+
// pending-send views so the timer cannot submit it again.
|
|
383
|
+
remaining.push(entry);
|
|
174
384
|
}
|
|
175
385
|
else {
|
|
176
386
|
failed++;
|
|
177
|
-
entry.lastError = 'sender returned false';
|
|
387
|
+
entry.lastError = result.error ?? 'sender returned false';
|
|
388
|
+
if (result.code !== undefined)
|
|
389
|
+
entry.lastErrorCode = result.code;
|
|
178
390
|
remaining.push(entry);
|
|
179
391
|
}
|
|
180
392
|
}
|
|
@@ -184,49 +396,64 @@ async function drainOnce(aid, sender) {
|
|
|
184
396
|
remaining.push(entry);
|
|
185
397
|
}
|
|
186
398
|
}
|
|
187
|
-
const current = readEntries(aid);
|
|
399
|
+
const current = readEntries(aid, queue);
|
|
188
400
|
const failedById = new Map(remaining.map(entry => [entry.id, entry]));
|
|
189
401
|
const retained = current.flatMap(entry => {
|
|
190
402
|
if (!drainedIds.has(entry.id))
|
|
191
403
|
return [entry];
|
|
192
404
|
const failedEntry = failedById.get(entry.id);
|
|
193
|
-
if (!failedEntry)
|
|
194
|
-
|
|
405
|
+
if (!failedEntry) {
|
|
406
|
+
const submittedEntry = drainedById.get(entry.id);
|
|
407
|
+
return submittedEntry && !hasSamePersistedRoute(entry, submittedEntry)
|
|
408
|
+
? [entry]
|
|
409
|
+
: [];
|
|
410
|
+
}
|
|
411
|
+
// A failure belongs to the route that was actually submitted. If another
|
|
412
|
+
// writer corrected that route while the RPC was in flight, preserve the
|
|
413
|
+
// corrected entry as pending instead of terminating it with a stale
|
|
414
|
+
// target's permanent error.
|
|
415
|
+
if (!hasSamePersistedRoute(entry, failedEntry))
|
|
416
|
+
return [entry];
|
|
417
|
+
if (entry.terminal && !failedEntry.terminal)
|
|
418
|
+
return [entry];
|
|
195
419
|
return [{
|
|
196
420
|
...failedEntry,
|
|
197
421
|
...entry,
|
|
198
422
|
attempts: failedEntry.attempts,
|
|
199
423
|
lastError: failedEntry.lastError,
|
|
424
|
+
lastErrorCode: failedEntry.lastErrorCode,
|
|
425
|
+
...(failedEntry.terminal ? { terminal: failedEntry.terminal } : {}),
|
|
200
426
|
}];
|
|
201
427
|
});
|
|
202
|
-
writeEntries(aid, retained);
|
|
203
|
-
return { sent, expired, failed };
|
|
428
|
+
writeEntries(aid, retained, queue);
|
|
429
|
+
return { sent, expired, failed, ...(permanent > 0 ? { permanent } : {}) };
|
|
204
430
|
}
|
|
205
|
-
export async function drain(aid, sender) {
|
|
206
|
-
const
|
|
431
|
+
export async function drain(aid, sender, queue = 'default') {
|
|
432
|
+
const drainKey = `${queue}:${aid}`;
|
|
433
|
+
const previous = activeDrainTails.get(drainKey);
|
|
207
434
|
let releaseTurn;
|
|
208
435
|
const turn = new Promise(resolve => { releaseTurn = resolve; });
|
|
209
436
|
const tail = (previous ?? Promise.resolve()).then(() => turn);
|
|
210
|
-
activeDrainTails.set(
|
|
437
|
+
activeDrainTails.set(drainKey, tail);
|
|
211
438
|
if (previous)
|
|
212
439
|
await previous;
|
|
213
440
|
try {
|
|
214
|
-
return await drainOnce(aid, sender);
|
|
441
|
+
return await drainOnce(aid, sender, queue);
|
|
215
442
|
}
|
|
216
443
|
finally {
|
|
217
444
|
releaseTurn();
|
|
218
|
-
if (activeDrainTails.get(
|
|
219
|
-
activeDrainTails.delete(
|
|
445
|
+
if (activeDrainTails.get(drainKey) === tail)
|
|
446
|
+
activeDrainTails.delete(drainKey);
|
|
220
447
|
}
|
|
221
448
|
}
|
|
222
|
-
export function hasPending(aid) {
|
|
223
|
-
return readEntries(aid).some(entry => !isExpired(entry));
|
|
449
|
+
export function hasPending(aid, queue = 'default') {
|
|
450
|
+
return readEntries(aid, queue).some(entry => !entry.terminal && !entry.deliveryResult && !isExpired(entry));
|
|
224
451
|
}
|
|
225
452
|
/**
|
|
226
453
|
* 当前 outbox 中待发送条目数。用于诊断发送管线堵塞:depth 持续增长说明
|
|
227
454
|
* message.send 出队速度跟不上入队速度(网关慢 / 链路抖动 / 卡片洪泛),
|
|
228
455
|
* 是命令「回复慢/无回复」的先行指标。
|
|
229
456
|
*/
|
|
230
|
-
export function pendingCount(aid) {
|
|
231
|
-
return load(aid).length;
|
|
457
|
+
export function pendingCount(aid, queue = 'default') {
|
|
458
|
+
return load(aid, queue).length;
|
|
232
459
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* 所以不能把 client 引用直接交给 ServiceProxyClient;传一个动态解引用的
|
|
12
12
|
* facade,每次 .call()/.authenticate()/._tokenStore 都读 channel 当前 client。
|
|
13
13
|
* serveForever(persistent) 的指数退避会在 client 缺失时自动等待恢复。
|
|
14
|
-
* - endpoint 发现:source='
|
|
14
|
+
* - endpoint 发现:source='instance' 时读服务实例登记文件的 port;source='static' 时使用显式 endpoint。
|
|
15
15
|
* - 失败降级:任何异常只 warn,绝不影响 daemon 主流程。
|
|
16
16
|
*/
|
|
17
17
|
import fs from 'fs';
|
|
@@ -20,16 +20,16 @@ import { ServiceProxyClient, EndpointPolicy } from '@agentunion/fastaun';
|
|
|
20
20
|
import { resolvePaths } from '../paths.js';
|
|
21
21
|
import { logger } from '../utils/logger.js';
|
|
22
22
|
const LOG = '[ServiceProxy]';
|
|
23
|
-
const
|
|
24
|
-
const
|
|
23
|
+
const INSTANCE_DISCOVERY_TIMEOUT_MS = 10_000;
|
|
24
|
+
const INSTANCE_DISCOVERY_INTERVAL_MS = 250;
|
|
25
25
|
function sleep(ms) {
|
|
26
26
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* 读 data/instance/
|
|
29
|
+
* 读 data/instance/ 下指定服务的存活实例端口。
|
|
30
30
|
* 取 startedAt 最新的一条(多实例保护下通常只有一条存活)。
|
|
31
31
|
*/
|
|
32
|
-
function
|
|
32
|
+
function discoverInstancePort(serviceName) {
|
|
33
33
|
const dir = resolvePaths().instanceDir;
|
|
34
34
|
let files;
|
|
35
35
|
try {
|
|
@@ -40,11 +40,25 @@ function discoverEcwebPort() {
|
|
|
40
40
|
}
|
|
41
41
|
const records = [];
|
|
42
42
|
for (const file of files) {
|
|
43
|
-
|
|
43
|
+
const match = serviceName === 'ecweb'
|
|
44
|
+
? file.match(/^(?:ecweb|watch-web)-(\d+)\.json$/)
|
|
45
|
+
: file.match(new RegExp(`^${serviceName.replace(/[^a-z0-9_-]/gi, '')}-(\\d+)\\.json$`));
|
|
46
|
+
if (!match)
|
|
44
47
|
continue;
|
|
48
|
+
const filePid = Number(match[1]);
|
|
45
49
|
try {
|
|
46
50
|
const rec = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf-8'));
|
|
47
|
-
if (rec
|
|
51
|
+
if (rec
|
|
52
|
+
&& Number.isInteger(rec.pid)
|
|
53
|
+
&& rec.pid > 0
|
|
54
|
+
&& rec.pid === filePid
|
|
55
|
+
&& isProcessAlive(rec.pid)
|
|
56
|
+
&& typeof rec.startedAt === 'number'
|
|
57
|
+
&& Number.isFinite(rec.startedAt)
|
|
58
|
+
&& rec.startedAt >= 0
|
|
59
|
+
&& Number.isInteger(rec.port)
|
|
60
|
+
&& rec.port >= 1
|
|
61
|
+
&& rec.port <= 65535) {
|
|
48
62
|
records.push(rec);
|
|
49
63
|
}
|
|
50
64
|
}
|
|
@@ -67,18 +81,18 @@ function isProcessAlive(pid) {
|
|
|
67
81
|
return e?.code === 'EPERM';
|
|
68
82
|
}
|
|
69
83
|
}
|
|
70
|
-
async function
|
|
71
|
-
const deadline = Date.now() +
|
|
84
|
+
async function waitForInstancePort(serviceName) {
|
|
85
|
+
const deadline = Date.now() + INSTANCE_DISCOVERY_TIMEOUT_MS;
|
|
72
86
|
let loggedWait = false;
|
|
73
87
|
while (Date.now() <= deadline) {
|
|
74
|
-
const port =
|
|
88
|
+
const port = discoverInstancePort(serviceName);
|
|
75
89
|
if (port)
|
|
76
90
|
return port;
|
|
77
91
|
if (!loggedWait) {
|
|
78
|
-
logger.info(`${LOG} 服务 "${serviceName}" source=
|
|
92
|
+
logger.info(`${LOG} 服务 "${serviceName}" source=instance,等待实例就绪`);
|
|
79
93
|
loggedWait = true;
|
|
80
94
|
}
|
|
81
|
-
await sleep(
|
|
95
|
+
await sleep(INSTANCE_DISCOVERY_INTERVAL_MS);
|
|
82
96
|
}
|
|
83
97
|
return null;
|
|
84
98
|
}
|
|
@@ -86,18 +100,22 @@ async function waitForEcwebPort(serviceName) {
|
|
|
86
100
|
* 把单个服务配置解析为本地回连 endpoint。无法解析返回 null。
|
|
87
101
|
*/
|
|
88
102
|
async function resolveEndpoint(svc) {
|
|
89
|
-
if (svc.source === '
|
|
90
|
-
const port = await
|
|
103
|
+
if (svc.proxy?.source === 'instance') {
|
|
104
|
+
const port = await waitForInstancePort(svc.name);
|
|
91
105
|
if (!port) {
|
|
92
|
-
logger.warn(`${LOG} 服务 "${svc.name}" source=
|
|
106
|
+
logger.warn(`${LOG} 服务 "${svc.name}" source=instance 等待 ${INSTANCE_DISCOVERY_TIMEOUT_MS}ms 后仍未发现存活实例,跳过`);
|
|
93
107
|
return null;
|
|
94
108
|
}
|
|
95
109
|
return `http://127.0.0.1:${port}`;
|
|
96
110
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
111
|
+
if (svc.proxy?.source === 'static') {
|
|
112
|
+
const endpoint = svc.proxy.endpoint?.trim();
|
|
113
|
+
if (endpoint)
|
|
114
|
+
return endpoint;
|
|
115
|
+
logger.warn(`${LOG} 服务 "${svc.name}" source=static 但未配置 endpoint,跳过`);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
logger.warn(`${LOG} 服务 "${svc.name}" 未配置有效 proxy.source,跳过`);
|
|
101
119
|
return null;
|
|
102
120
|
}
|
|
103
121
|
/**
|
|
@@ -105,8 +123,8 @@ async function resolveEndpoint(svc) {
|
|
|
105
123
|
* 返回 handle 供关停;没有启用服务时返回 null。endpoint 发现和隧道启动在后台执行,
|
|
106
124
|
* 失败只 warn,不抛异常,不阻塞 daemon 主流程。
|
|
107
125
|
*/
|
|
108
|
-
export function startServiceProxy(controlChannel, providerAid,
|
|
109
|
-
const services =
|
|
126
|
+
export function startServiceProxy(controlChannel, providerAid, configuredServices) {
|
|
127
|
+
const services = configuredServices.filter((s) => s.enabled === true && s.proxy?.enabled === true);
|
|
110
128
|
if (services.length === 0) {
|
|
111
129
|
logger.info(`${LOG} 无启用的服务,跳过`);
|
|
112
130
|
return null;
|
|
@@ -177,12 +195,12 @@ export function startServiceProxy(controlChannel, providerAid, config) {
|
|
|
177
195
|
continue;
|
|
178
196
|
try {
|
|
179
197
|
proxyClient.registerService(svc.name, endpoint, {
|
|
180
|
-
serviceType: svc.serviceType ?? 'http',
|
|
181
|
-
visibility: svc.visibility ?? 'private',
|
|
182
|
-
metadata: svc.metadata ?? {},
|
|
198
|
+
serviceType: svc.proxy?.serviceType ?? 'http',
|
|
199
|
+
visibility: svc.proxy?.visibility ?? 'private',
|
|
200
|
+
metadata: svc.proxy?.metadata ?? {},
|
|
183
201
|
});
|
|
184
202
|
registered += 1;
|
|
185
|
-
logger.info(`${LOG} 注册服务 "${svc.name}" → ${endpoint} (visibility=${svc.visibility ?? 'private'})`);
|
|
203
|
+
logger.info(`${LOG} 注册服务 "${svc.name}" → ${endpoint} (type=${svc.proxy?.serviceType ?? 'http'}, visibility=${svc.proxy?.visibility ?? 'private'})`);
|
|
186
204
|
}
|
|
187
205
|
catch (e) {
|
|
188
206
|
logger.warn(`${LOG} 注册服务 "${svc.name}" 失败: ${e?.message || e}`);
|